Reputation: 195
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.
Upvotes: 3
Views: 464
Reputation: 22162
The std::byte
object that storage
in
reinterpret_cast<Base*>(storage)
will point to after array-to-pointer decay, is not pointer-interconvertible with any Base
object located at that address. This is never the case between an element of an array providing storage and the object it provides storage for.
Pointer-interconvertibility basically only applies if you are casting pointers between standard-layout classes and their members/bases (and only in special cases). These are the only cases where std::launder
is not required.
So in general, for your use case where you try to obtain a pointer to an object from the array which provides the storage for the object, you always need to apply std::launder
after reinterpret_cast
.
Therefore you must always use std::launder
in all cases in which you are using reinterpret_cast
at the moment. E.g.:
reinterpret_cast<Base*>(storage)->~Base();
should be
std::launder(reinterpret_cast<Base*>(storage))->~Base();
Note however that from a C++ standard's perspective what you are trying to do still isn't guaranteed to work and there is no standard way of enforcing it to work.
Your class Base
is required to have a virtual destructor. That means Base
and all classes deriving from it are not standard-layout. A class that is not standard-layout has practically no guarantees on its layout. That means that you have no guarantee that the address of the Derived
object is equal to the address of the Base
subobject, no matter how you let Derived
inherit from Base
.
If the addresses don't match up, std::launder
will have undefined behavior because there won't be a Base
object at that address after you did new(storage) Derived
.
So you need to rely on the ABI specification to make sure that the address of the Base
subobject will equal the address of the Derived
object.
Upvotes: 2