std::launder 使用就地多态容器
std::launder with inplace polymorphic containers
我正在用 C++ 为 Game Boy Advance 做一个有点不平凡的项目,并且作为一个完全没有内存管理的有限平台,我试图避免调用 malloc
和动态分配。为此,我已经实现了相当多的调用,"inplace polymorphic containers",它存储从 Base
class 派生的类型的对象(在类型模板中参数化),并且然后我有 new
对象的函数,并使用完美转发来调用适当的构造函数。例如,其中一个容器如下所示(也可以访问 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();
}
};
看了一些关于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();
}
};
但这基本上意味着代码中每个 PointerInterfaceContainer
字节的开销 sizeof(void*)
字节(在所讨论的体系结构中为 4)。这似乎不是很多,但如果我想填充 1024 个容器,每个容器有 128 个字节,这个开销会加起来。另外,它需要第二次内存访问才能访问指针,并且假设在 99% 的情况下,Derived
将以 Base
作为主要基础 class(这意味着 static_cast<Derved*>(curObject)
和 curObject
是相同的位置),这意味着指针将始终指向 storage
,这意味着所有开销都是完全不必要的。
storage
在
中的std::byte
对象
reinterpret_cast<Base*>(storage)
将在数组到指针衰减后指向,不是 指针可相互转换的 与位于该地址的任何 Base
对象。数组元素 提供存储 和它为其提供存储的对象之间绝不会出现这种情况。
Pointer-interconvertibility 基本上只适用于在标准布局 classes 和它们的 members/bases 之间转换指针(并且仅在特殊情况下)。这些是唯一不需要 std::launder
的情况。
所以一般来说,对于您尝试从为对象提供存储的数组中获取指向对象的指针的用例,您始终需要应用std::launder
在 reinterpret_cast
.
之后
因此,您必须 始终 在您目前使用 reinterpret_cast
的所有情况下使用 std::launder
。例如:
reinterpret_cast<Base*>(storage)->~Base();
应该是
std::launder(reinterpret_cast<Base*>(storage))->~Base();
但是请注意,从 C++ 标准的角度来看,您尝试做的事情仍然不能保证有效,并且没有强制执行它的标准方法。
你的 class Base
需要一个虚拟析构函数。这意味着 Base
和所有从它派生的 class 不是 standard-layout。非标准布局的 class 实际上无法保证其布局。这意味着您无法保证 Derived
对象的地址等于 Base
子对象的地址,无论您如何让 Derived
继承自 Base
。
如果地址不匹配,std::launder
将有未定义的行为,因为在您完成 new(storage) Derived
后该地址将不会有 Base
对象。
因此您需要依靠 ABI 规范来确保 Base
子对象的地址将等于 Derived
对象的地址。
我正在用 C++ 为 Game Boy Advance 做一个有点不平凡的项目,并且作为一个完全没有内存管理的有限平台,我试图避免调用 malloc
和动态分配。为此,我已经实现了相当多的调用,"inplace polymorphic containers",它存储从 Base
class 派生的类型的对象(在类型模板中参数化),并且然后我有 new
对象的函数,并使用完美转发来调用适当的构造函数。例如,其中一个容器如下所示(也可以访问 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();
}
};
看了一些关于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();
}
};
但这基本上意味着代码中每个 PointerInterfaceContainer
字节的开销 sizeof(void*)
字节(在所讨论的体系结构中为 4)。这似乎不是很多,但如果我想填充 1024 个容器,每个容器有 128 个字节,这个开销会加起来。另外,它需要第二次内存访问才能访问指针,并且假设在 99% 的情况下,Derived
将以 Base
作为主要基础 class(这意味着 static_cast<Derved*>(curObject)
和 curObject
是相同的位置),这意味着指针将始终指向 storage
,这意味着所有开销都是完全不必要的。
storage
在
std::byte
对象
reinterpret_cast<Base*>(storage)
将在数组到指针衰减后指向,不是 指针可相互转换的 与位于该地址的任何 Base
对象。数组元素 提供存储 和它为其提供存储的对象之间绝不会出现这种情况。
Pointer-interconvertibility 基本上只适用于在标准布局 classes 和它们的 members/bases 之间转换指针(并且仅在特殊情况下)。这些是唯一不需要 std::launder
的情况。
所以一般来说,对于您尝试从为对象提供存储的数组中获取指向对象的指针的用例,您始终需要应用std::launder
在 reinterpret_cast
.
因此,您必须 始终 在您目前使用 reinterpret_cast
的所有情况下使用 std::launder
。例如:
reinterpret_cast<Base*>(storage)->~Base();
应该是
std::launder(reinterpret_cast<Base*>(storage))->~Base();
但是请注意,从 C++ 标准的角度来看,您尝试做的事情仍然不能保证有效,并且没有强制执行它的标准方法。
你的 class Base
需要一个虚拟析构函数。这意味着 Base
和所有从它派生的 class 不是 standard-layout。非标准布局的 class 实际上无法保证其布局。这意味着您无法保证 Derived
对象的地址等于 Base
子对象的地址,无论您如何让 Derived
继承自 Base
。
如果地址不匹配,std::launder
将有未定义的行为,因为在您完成 new(storage) Derived
后该地址将不会有 Base
对象。
因此您需要依靠 ABI 规范来确保 Base
子对象的地址将等于 Derived
对象的地址。