Я делаю несколько нетривиальный проект на 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
, что означает, что все эти накладные расходы совершенно не нужны.