std :: отмываемый контейнер с полиморфами c - PullRequest
3 голосов
/ 29 марта 2020

Я делаю несколько нетривиальный проект на C ++ для Game Boy Advance, и, будучи такой ограниченной платформой без какого-либо управления памятью, я стараюсь избегать вызовов malloc и динамического c выделения. Для этого я реализовал достаточное количество, как называется, «inplace polymorphi c Containers», которые хранят объект типа, производного от класса Base (параметризованного в шаблоне типа), и затем я имею функции, которые new объект и используют совершенную пересылку для вызова соответствующего конструктора. Один из этих контейнеров, например, показан ниже (и также доступен здесь ):

//--------------------------------------------------------------------------------
// 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();
    }
};

После прочтения некоторых статей о std::launder я все еще сомневаюсь, но Я предполагаю, что эти строки кода могут вызвать проблемы:

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); }

Особенно, если рассматриваемые Derived (или сам Base) имеют const члены или ссылки. То, что я спрашиваю, касается общего руководства, а не только для этого (и другого) контейнера, по использованию std::launder. Что вы думаете здесь?


Итак, одним из предложенных решений является добавление указателя, который бы получал содержимое new (storage) Derived(std::forward<Ts>(ts)...);, как показано ниже:

//--------------------------------------------------------------------------------
// 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();
    }
};

Но это означало бы, по существу, издержки sizeof(void*) байтов (в рассматриваемой архитектуре 4) для каждого PointerInterfaceContainer, присутствующего в коде. Кажется, это не так уж много, но если я захочу, скажем, 1024 контейнера, каждый из которых имеет 128 байтов, эти накладные расходы могут сложиться. Кроме того, для доступа к указателю потребуется второй доступ к памяти, и, учитывая, что в 99% случаев Derived будет иметь Base в качестве основного базового класса (это означает, что static_cast<Derved*>(curObject) и curObject являются в том же месте), это будет означать, что указатель всегда будет указывать на storage, что означает, что все эти накладные расходы совершенно не нужны.

1 Ответ

2 голосов
/ 30 марта 2020

Объект std::byte, на который storage в

reinterpret_cast<Base*>(storage)

будет указывать после затухания массива в указатель, не является взаимозаменяемым указателем с любым Base объектом расположен по этому адресу. Это никогда не относится к элементу массива , предоставляющему хранилище , и объекту, для которого он предоставляет хранилище.

взаимопревращаемость указателей в основном только применяется, если вы применяете указатели между классами стандартной компоновки и их членами / базами (и только в особых случаях). Это единственные случаи, когда std::launder не требуется.

Итак, в общем случае для вашего случая использования, когда вы пытаетесь получить указатель на объект из массива, который предоставляет хранилище для объекта, вы всегда необходимо применять std::launder после reinterpret_cast.

Поэтому вы должны всегда использовать std::launder во всех случаях, когда вы используете reinterpret_cast на момент. Например:

reinterpret_cast<Base*>(storage)->~Base();

должно быть

std::launder(reinterpret_cast<Base*>(storage))->~Base();

Обратите внимание, однако, что с точки зрения стандарта C ++ то, что вы пытаетесь сделать, не гарантированно работает, и нет стандартный способ заставить его работать.

В вашем классе Base должен быть виртуальный деструктор. Это означает, что Base и все производные от него классы не являются стандартным макетом . Класс, который не является стандартным макетом, практически не имеет гарантий в отношении своего макета. Это означает, что вы не можете гарантировать, что адрес объекта Derived равен адресу подобъекта Base, независимо от того, как вы разрешаете Derived наследовать от Base.

Если адреса не совпадают, std::launder будет иметь неопределенное поведение, потому что после этого new(storage) Derived.

по этому адресу не будет объекта Base. Поэтому вам нужно полагаться на спецификацию ABI чтобы убедиться, что адрес подобъекта Base будет равен адресу объекта Derived.

...