I am doing a somewhat nontrivial project in C++ for the Game Boy Advance, and, being such a limited platform with no memory management at all, I am trying to avoid calls to malloc
and dynamic allocation. For this, I have implemented a fair amount of, what a call, "inplace polymorphic containers", that store an object of a type derived from a Base
class (parametrized in the type template), and then I have functions that new
the object and use perfect forwarding to call the appropriate constructor. One of those containers, as example, is shown below (and is also accessible here):
//--------------------------------------------------------------------------------
// PointerInterfaceContainer.hpp
//--------------------------------------------------------------------------------
// Provides a class that can effectively allocate objects derived from a
// base class and expose them as pointers from that base
//--------------------------------------------------------------------------------
#pragma once
#include <cstdint>
#include <cstddef>
#include <algorithm>
#include "type_traits.hpp"
template <typename Base, std::size_t Size>
class alignas(max_align_t) PointerInterfaceContainer
{
static_assert(std::is_default_constructible_v<Base>,
"PointerInterfaceContainer will not work without a Base that is default constructible!");
static_assert(std::has_virtual_destructor_v<Base>,
"PointerInterfaceContainer will not work properly without virtual destructors!");
static_assert(sizeof(Base) >= sizeof(std::intptr_t),
"PointerInterfaceContainer must not be smaller than a pointer");
std::byte storage[Size];
public:
PointerInterfaceContainer() { new (storage) Base(); }
template <typename Derived, typename... Ts>
void assign(Ts&&... ts)
{
static_assert(std::is_base_of_v<Base, Derived>,
"The Derived class must be derived from Base!");
static_assert(sizeof(Derived) <= Size,
"The Derived class is too big to fit in that PointerInterfaceContainer");
static_assert(!is_virtual_base_of_v<Base, Derived>,
"PointerInterfaceContainer does not work properly with virtual base classes!");
reinterpret_cast<Base*>(storage)->~Base();
new (storage) Derived(std::forward<Ts>(ts)...);
}
void clear() { assign<Base>(); }
PointerInterfaceContainer(const PointerInterfaceContainer&) = delete;
PointerInterfaceContainer(PointerInterfaceContainer&&) = delete;
PointerInterfaceContainer &operator=(PointerInterfaceContainer) = delete;
Base* operator->() { return reinterpret_cast<Base*>(storage); }
const Base* operator->() const { return reinterpret_cast<const Base*>(storage); }
Base& operator*() { return *reinterpret_cast<Base*>(storage); }
const Base& operator*() const { return *reinterpret_cast<const Base*>(storage); }
~PointerInterfaceContainer()
{
reinterpret_cast<Base*>(storage)->~Base();
}
};
After reading some articles about std::launder
, I am still in doubt, but I guess those lines of code might cause a problem:
Base* operator->() { return reinterpret_cast<Base*>(storage); }
const Base* operator->() const { return reinterpret_cast<const Base*>(storage); }
Base& operator*() { return *reinterpret_cast<Base*>(storage); }
const Base& operator*() const { return *reinterpret_cast<const Base*>(storage); }
Especially if the Derived
s in question (or the Base
itself) have const
members or references. What I am asking is about a general guideline, not only for this (and the other) container, about the use of std::launder
. What do you think here?
So, one of the proposed solutions is to add a pointer that would receive the contents of new (storage) Derived(std::forward<Ts>(ts)...);
, like shown:
//--------------------------------------------------------------------------------
// PointerInterfaceContainer.hpp
//--------------------------------------------------------------------------------
// Provides a class that can effectively allocate objects derived from a
// base class and expose them as pointers from that base
//--------------------------------------------------------------------------------
#pragma once
#include <cstdint>
#include <cstddef>
#include <algorithm>
#include <utility>
#include "type_traits.hpp"
template <typename Base, std::size_t Size>
class alignas(max_align_t) PointerInterfaceContainer
{
static_assert(std::is_default_constructible_v<Base>,
"PointerInterfaceContainer will not work without a Base that is default constructible!");
static_assert(std::has_virtual_destructor_v<Base>,
"PointerInterfaceContainer will not work properly without virtual destructors!");
static_assert(sizeof(Base) >= sizeof(std::intptr_t),
"PointerInterfaceContainer must not be smaller than a pointer");
// This pointer will, in 100% of the cases, point to storage
// because the codebase won't have any Derived from which Base
// isn't the primary base class, but it needs to be there because
// casting storage to Base* is undefined behavior
Base *curObject;
std::byte storage[Size];
public:
PointerInterfaceContainer() { curObject = new (storage) Base(); }
template <typename Derived, typename... Ts>
void assign(Ts&&... ts)
{
static_assert(std::is_base_of_v<Base, Derived>,
"The Derived class must be derived from Base!");
static_assert(sizeof(Derived) <= Size,
"The Derived class is too big to fit in that PointerInterfaceContainer");
static_assert(!is_virtual_base_of_v<Base, Derived>,
"PointerInterfaceContainer does not work properly with virtual base classes!");
curObject->~Base();
curObject = new (storage) Derived(std::forward<Ts>(ts)...);
}
void clear() { assign<Base>(); }
PointerInterfaceContainer(const PointerInterfaceContainer&) = delete;
PointerInterfaceContainer(PointerInterfaceContainer&&) = delete;
PointerInterfaceContainer &operator=(PointerInterfaceContainer) = delete;
Base* operator->() { return curObject; }
const Base* operator->() const { return curObject; }
Base& operator*() { return *curObject; }
const Base& operator*() const { return *curObject; }
~PointerInterfaceContainer()
{
curObject->~Base();
}
};
But that would mean essentially an overhead of sizeof(void*)
bytes (in the architecture in question, 4) for each PointerInterfaceContainer
present in the code. That seems not to be a lot, but if I want to cram, say, 1024 containers, each having 128 bytes, this overhead can add up. Plus, it would require a second memory access to access the pointer and, given that, in 99% of the cases, Derived
will have Base
as a primary base class (that means static_cast<Derved*>(curObject)
and curObject
are the same location), this would mean that pointer would always point to storage
, meaning all that overhead is completely unnecessary.