3

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 Deriveds 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.

JoaoBapt
  • 195
  • 1
  • 11
  • @rsjaffe Your link is a mirror of [this answer](https://stackoverflow.com/a/39382728/), and I've already read it. I'm almost sure I need to use `std::launder` here, but I want to double-check with the professionals. Thank you! – JoaoBapt Mar 29 '20 at 20:20
  • Yes, I found out it was a mirror of the answer, which is why I deleted it. Sorry to bother you with that. – rsjaffe Mar 29 '20 at 20:21
  • An interesting paper, if you haven't seen it yet: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0532r0.pdf . – rsjaffe Mar 29 '20 at 20:24
  • As far as I know, you need to use launder at all reinterpret_cast occasions in your code, as you cast non-pointer-interconvertible pointers. – geza Mar 29 '20 at 20:24

1 Answers1

2

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.

walnut
  • 21,629
  • 4
  • 23
  • 59
  • So essentially my quest to avoid `malloc` is a fruitless one? – JoaoBapt Mar 29 '20 at 21:23
  • @JoaoBapt If you know the ABI specification you can rely on that. It will not be portable, but that shouldn't be a problem for something so specialized. – walnut Mar 29 '20 at 21:24
  • It's the ARM Embedded ABI `arm-none-eabi`, so I can possibly rely on that. I'm making a (bold) assumption that a `Derived` class that derives "firstly" from `Base` will have the same address. Another possibility would to include a `Base* curObj;` and set it to the value returned by the `new (storage) Derived(std::forward(ts)...)`, but that means I'd waste `sizeof(void*)` bytes for each object used. – JoaoBapt Mar 29 '20 at 21:30
  • @JoaoBapt Yes I was just about to recommend that second alternative. I don't know what the ABI says, but I guess there is a good chance that the layout will be as needed if `Base` is the first specified base class. – walnut Mar 29 '20 at 21:32
  • Or then just throw away all that fussing with polymorphic objects and use data-oriented design / ECS on it – JoaoBapt Mar 29 '20 at 21:42
  • As defined in [the Itanium C++ ABI](http://itanium-cxx-abi.github.io/cxx-abi/abi.html#class-types) (which the ARM EABI references), for memory layout, the primary base class (aka the one that is at offset zero on the pointer) "is the first (in direct base class order) non-virtual dynamic base class, if one exists". That means it is the first class that appears in the class declaration. *However*, this is something defined in the ABI, and I cannot rely on it, because that might mean GCC will want to play with the layout for optimization. – JoaoBapt Mar 29 '20 at 22:09
  • @walnut: According to modern compiler design philosophy, if the Standard says something is Undefined, that takes precedence over anything else in the universe, including an ABI spec, that would otherwise define the behavior. – supercat Mar 30 '20 at 20:55