Consexpr 替代放置 new 以使内存中的对象未初始化?

Posted

技术标签:

【中文标题】Consexpr 替代放置 new 以使内存中的对象未初始化?【英文标题】:Constexpr alternative to placement new to be able to leave objects in memory uninitialized? 【发布时间】:2018-10-31 13:35:51 【问题描述】:

我正在尝试创建一个静态容器,它具有基于堆栈的内存并且可以容纳 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_typev;
    

    empty uninitialized;
    value_type value;
;

这使您可以通过设置 empty 成员来存储未初始化的项目,这可以解决 constexpr 中的所有成员都必须初始化的限制。

现在这种方法的问题是,如果value_type是实现operator=的类型,rule for unions says:

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

这意味着为了能够使用这个技巧,我也需要在联合中实现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:这适用于之前 sn-p 的 set() 方法,因为它本身不会更改活动成员,但会重新分配整个联合。然而,这个技巧似乎无法在赋值运算符中使用,因为这会导致无限递归......

我在这里遗漏了什么吗,或者这真的是在 constexpr 中使用联合作为新布局替代方案的死胡同吗?

除了我完全错过的新展示位置之外,还有其他替代方法吗?

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

【问题讨论】:

为什么std::array 不足以满足您的用例? 我的用例是实现一个static_vector&lt;T, 10&gt;,其中 10 是容量,并且向量本身会跟踪当前的条目数。 std::array 单独无法留下一些条目,而 T 的构造函数未运行。 std::array&lt;optional&lt;T&gt;, 10&gt; ? @Jarod42 我尝试了使我的static_vector 成为std::array&lt;std::optional&lt;T&gt;, 10&gt; 的包装器以及计数的方法,但std::optional 仅部分启用了constexpr。例如,operator=.emplace 或任何其他设置非初始化值的方式不是 constexpr @Tobias AFAIK,在成员不重要的情况下更改工会的活动成员的唯一方法是使用新位置。原因是需要在该存储上调用构造函数来启动它的生命周期,如果没有placement new 表达式,您将无法做到这一点。 【参考方案1】:

在 C++17 中,你不能。

current restrictions 在常量表达式中不能做什么包括:

赋值表达式 ([expr.ass]) 或调用赋值运算符 ([class.copy.assign]) 会改变联合的活动成员;

一个新表达式;

真的没有办法。


在 C++20 中,您可以做到,但可能不是您想的那样。由于P0784,后一个限制将在 C++20 中放宽为:

一个new-expression (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() 在恒定评估时间分配和运行时不分配之间切换。诚然,这并不漂亮,但它应该可以工作。

【讨论】:

感谢您的全面回答。太糟糕了 c++ 没有提供一个干净的方法来做到这一点,但至少在 c++20 中它是可能的。 这不愧是“本月最佳答案”。【参考方案2】:

您的存储可能如下所示:

// 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 固定大小向量的建议,例如@987654321 @。

【讨论】:

我不确定我是否理解,非 const 对象的 const 数组如何不意味着最初在每个项目上调用 T 的构造函数? 如果您创建 array&lt;remove_const_t&lt;T&gt;, Capacity&gt;&gt;,它将初始化 OP 不想要的成员 @Tobias 你说你希望能够在运行时使用它。对于非平凡的对象,您将需要使用 non-constexpr emplace_back,它将使用placement new。 @NathanOliver 你不能一边吃蛋糕一边吃。在编译时可能有未初始化的存储,但您也不能在编译时构造这些对象(使用放置新)。我的代码只显示了 trivial 对象的情况。 但是data_t data_ 确实初始化了一些琐碎的对象,因为array 是一个聚合。它们都将被初始化为零。

以上是关于Consexpr 替代放置 new 以使内存中的对象未初始化?的主要内容,如果未能解决你的问题,请参考以下文章

成员函数指针值上的 Consexpr - 未定义的行为?

Consexpr 与宏

JAVA 声明new 过多临时对像会导致内存溢出,怎么解决?

UIScrollView 中的 UIView 子类如何处理内存

程序运行过程的内存分析。

C++ new 和 delete