Constexpr替代了新的放置方式,可以让内存中的对象保持未初始化状态

Constexpr alternative to placement new to be able to leave objects in memory uninitialized?

本文关键字:对象 内存 初始化 状态 方式 Constexpr      更新时间:2023-10-16

我正在尝试创建一个静态容器,该容器具有基于堆栈的内存,可以容纳T的N个实例。与std::vector非常相似,我希望当前未使用的内存不包含T的初始化项。这通常通过放置new来解决,但这不可能在constexpr中使用。

使用并集我发现了一个技巧,可以使用并集如下:

template <typename value_type>
union container_storage_type
{
struct empty{};
constexpr container_storage_type(): uninitialized{}{}
constexpr container_storage_type(value_type v): value(v){}
constexpr void set(value_type v)
{
*this = literal_container_storage_type{v};
}
empty uninitialized;
value_type value;
};

这允许您通过设置empty成员来存储未初始化的项,并且这绕过了constexpr中所有成员都必须初始化的限制。

现在这种方法的问题是,如果value_type是实现operator=的类型,则联合的规则是:

如果并集包含具有非平凡特殊成员函数(复制/移动构造函数、复制/移动赋值或析构函数(的非静态数据成员,则该函数在并集中默认会被删除,并且需要由程序员显式定义。

这意味着为了能够使用这个技巧,我也需要在联合中实现operator=,但这看起来怎么样?

constexpr container_storage_type& operator=(const container_storage_type& other)
{           
value = other.value; //ATTEMPT #1
//*this = container_storage_type(other.value);ATTEMPT #2
return *this;
}

尝试#1:这似乎是不可能的,因为编译器抱怨在常量表达式中根本不允许更改联合的活动成员。尝试#2:这适用于上一个代码段中的set()方法,因为它本身不会更改活动成员,而是重新分配整个联合。然而,这个技巧似乎无法在赋值运算符中使用,因为这会导致无休止的递归。。。

我是不是遗漏了什么,或者这真的是在constexpr中使用工会作为一种新的安置方式的死胡同?

除了我完全错过的新职位,还有其他选择吗?

https://godbolt.org/z/km0nTY说明问题的代码

在C++17中,您不能。

当前对常量表达式中不能执行的操作的限制包括:

  • 一个赋值表达式([expr.ass](或赋值运算符的调用([class.copy.assign](,它将更改联合的活动成员;

  • 一个新表达式

真的没有办法。


在C++20中,你将能够做到,但可能不是你想象的那样。后一个限制将在C++20中放宽,因为P0784类似于:

  • 一个新表达式(8.3.4(,除非所选分配函数是可替换的全局分配函数(21.6.2.1,21.6.2.2(

也就是说,new T将变好,但new (ptr) T仍然不被允许。作为使std::vectorconstexpr友好的一部分,我们需要能够管理";生的";内存-但我们仍然无法真正管理原始内存。所有的东西仍然需要打字。处理原始字节是行不通的。

但是std::allocator并不完全处理原始字节。allocate(n)给您一个T*constructT*作为一个位置和一组参数,并在该位置创建一个新对象。在这一点上,你可能想知道这与placement new有什么不同——唯一的区别是,坚持使用std::allocator,我们留在T*的土地上——但placement new使用void*。事实证明,这种区别是至关重要的。

不幸的是,这具有您的constexpr版本";分配";内存(但它分配编译器内存,编译器内存将根据需要提升到静态存储,所以这是你想要的(-但你的纯运行时版本肯定不想分配内存,事实上,关键是它不想分配。为此,您必须使用is_constant_evaluated()在恒定评估时间分配和运行时不分配之间切换。诚然,这并不美丽,但它应该起作用。

您的存储可以是这样的:

// For trivial objects
using data_t = const array<remove_const_t<T>, Capacity>>;
alignas(alignof(T)) data_t data_{};
// For non-trivial objects
alignas(alignof(T)) aligned_storage_t<T> data_[Capacity]{};

这将允许您创建-const对象的const数组。然后构建对象将看起来像这样:

// Not real code, for trivial objects
data_[idx] = T(forward<Args>(args)...);
// For non-trivial objects
new (end()) T(forward<Args>(args)...);

此处必须进行新的安置。您可以在编译时拥有存储,但不能在编译时为非平凡对象构造存储。

您还需要考虑您的容器是否为零大小,等等。我建议您查看固定大小向量的现有实现,甚至还有一些针对constexpr固定大小向量(如p0843r1(的建议。