Constexpr替代new放置,以便能够将对象保留在内存中而未初始化?

时间:2018-10-31 13:35:52

标签: c++ c++17

我正在尝试创建一个静态容器,该容器具有基于堆栈的内存,并且可以容纳N个T实例。非常相似std::vector我希望当前未使用的内存不包含T的初始化项。通常可以通过放置来解决新的,但是在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=的类型,则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:此方法在上一个代码段的set()方法中有效,因为它本身不会更改活动成员,但会重新分配整个联合。这个技巧似乎无法在赋值运算符中使用,但是因为这会导致无限递归...

我在这里错过了什么吗,或者这真的是将联合作为constexpr中的新放置替代品的死胡同吗?

我完全想念的还有其他替代品吗?

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

2 个答案:

答案 0 :(得分:5)

在C ++ 17中,您不能这样做。

关于current restrictions不能在常量表达式中执行的操作的内容包括:

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

  •   
  • 一个 new-expression ;

  •   

真的没有办法解决。


在C ++ 20中,您将能够,但可能不是您想的那样。由于P0784的出现,后一种限制在C ++ 20中将得到放宽,例如:

  
      
  • new-expression (8.3.4),除非所选分配功能是可替换的全局分配功能(21.6.2.1,21.6.2.2);
  •   

也就是说,new T可以正常使用,但仍然不允许new (ptr) T。使std::vector constexpr友好的一部分,我们需要能够管理“原始”内存-但是我们仍然无法真正管理真正的 raw 内存。一切仍然必须输入。处理原始字节将无法正常工作。

但是std::allocator不会完全处理原始字节。 allocate(n)给您一个T*,而construct则以T*作为位置和一堆参数,并在该位置创建一个新对象。您可能会想知道这与新的刊登位置有什么不同-唯一的区别是坚持std::allocator,我们留在T*的土地上-但新的刊登位置使用{{1} }。这种区别非常关键。

不幸的是,这会产生有趣的结果,即您的void*版本“分配”了内存(但是它分配了编译器内存,必要时会将其提升为静态存储-这样就可以了)-但是您的纯运行时版本肯定不希望分配内存,实际上整个要点是它不需要。为此,您将必须使用constexpr在恒定评估时间的分配与运行时间的非分配之间切换。公认这不是很漂亮,但是应该可以。

答案 1 :(得分:-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]{};

这将允许您创建{em> non -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