为什么 C++17 中没有 std::construct_at?

Why isn't there a std::construct_at in C++17?

C++17 添加了 std::destroy_at,但没有任何 std::construct_at 对应项。这是为什么?难道不能像下面这样简单实现吗?

template <typename T, typename... Args>
T* construct_at(void* addr, Args&&... args) {
  return new (addr) T(std::forward<Args>(args)...);
}

这可以避免不完全自然的放置新语法:

auto ptr = construct_at<int>(buf, 1);  // instead of 'auto ptr = new (buf) int(1);'
std::cout << *ptr;
std::destroy_at(ptr);

有这么回事,但是not named like you might expect:

  • uninitialized_copy 将一系列对象复制到未初始化的内存区域

  • uninitialized_copy_n (C++11) 将多个对象复制到未初始化的内存区域 (函数模板)

  • uninitialized_fill 将对象复制到由范围定义的未初始化内存区域 (函数模板)

  • uninitialized_fill_n 将对象复制到未初始化的内存区域,由开始和计数定义 (函数模板)
  • uninitialized_move (C++17) 将一系列对象移动到未初始化的内存区域 (函数模板)
  • uninitialized_move_n (C++17) 将多个对象移动到未初始化的内存区域 (函数模板)
  • uninitialized_default_construct (C++17) 在由范围定义的未初始化内存区域中通过默认初始化构造对象 (函数模板)
  • uninitialized_default_construct_n (C++17) 在未初始化的内存区域中通过默认初始化构造对象,由开始和计数定义 (函数模板)
  • uninitialized_value_construct (C++17) 在未初始化的内存区域中通过值初始化构造对象,由范围定义 (函数模板)
  • uninitialized_value_construct_n (C++17) 在未初始化的内存区域中通过值初始化构造对象,由开始和计数定义

std::allocator_traits::construct. There used to be one more in std::allocator, but that got removed, rationale in standards committee paper D0174R0.

construct 似乎没有提供任何语法糖。此外,它的效率低于新安置。绑定到引用参数会导致临时实现和额外的 move/copy 构造:

struct heavy{
   unsigned char[4096];
   heavy(const heavy&);
};
heavy make_heavy(); // Return a pr-value
auto loc = ::operator new(sizeof(heavy));
// Equivalently: unsigned char loc[sizeof(heavy)];

auto p = construct<heavy>(loc,make_heavy()); // The pr-value returned by
         // make_heavy is bound to the second argument,
         // and then this arugment is copied in the body of construct.

auto p2 = new(loc) auto(make_heavy()); // Heavy is directly constructed at loc
       //... and this is simpler to write!

不幸的是,在调用函数时没有任何方法可以避免这些额外的 copy/move 构造。转发几乎完美。

另一方面,库中的construct_at可以完成标准库词汇。

std::destroy_at 对直接析构函数调用提供了两个 objective 改进:

  1. 它减少了冗余:

     T *ptr = new T;
     //Insert 1000 lines of code here.
     ptr->~T(); //What type was that again?
    

    当然,我们都更愿意将它包装在 unique_ptr 中并完成它,但如果由于某种原因不能发生这种情况,请将 T 放在一个元素中的冗余。如果我们将类型更改为 U,我们现在必须更改析构函数调用,否则事情就会中断。使用 std::destroy_at(ptr) 无需在两个地方更改相同的内容。

    DRY 很好。

  2. 这很容易:

     auto ptr = allocates_an_object(...);
     //Insert code here
     ptr->~???; //What type is that again?
    

    如果我们推导出指针的类型,那么删除它就有点困难了。你不能做 ptr->~decltype(ptr)();因为 C++ 解析器不是那样工作的。不仅如此,decltype 将类型推导为 指针 ,因此您需要从推导的类型中删除指针间接。带你去:

     auto ptr = allocates_an_object(...);
     //Insert code here
     using delete_type = std::remove_pointer_t<decltype(ptr)>;
     ptr->~delete_type();
    

    谁想输入 那个

相比之下,您假设的 std::construct_at 没有 objective 相对于放置 new 的改进。在这两种情况下,您都必须说明要创建的类型。在这两种情况下都必须提供构造函数的参数。在这两种情况下都必须提供指向内存的指针。

所以没有必要被你的假设std::construct_at.

解决

而且它 objective仅 能力 低于新位置。你可以这样做:

auto ptr1 = new(mem1) T;
auto ptr2 = new(mem2) T{};

这些 不同。在第一种情况下,对象是默认初始化的,这可能会使它处于未初始化状态。在第二种情况下,对象被值初始化。

你假设的std::construct_at不能让你选择你想要的。如果您不提供任何参数,它可以具有执行默认初始化的代码,但是它将无法提供用于值初始化的版本。它可以在没有参数的情况下进行值初始化,但是你不能默认初始化对象。


请注意,C++20 添加了 std::construct_at。但它这样做是出于一致性以外的原因。他们在那里支持编译时内存分配和构造。

您可以在常量表达式中调用“可替换”全局 new 运算符(只要您实际上没有 替换 它)。但是 placement-new 不是一个“可替换”函数,所以你不能在那里调用它。

constexpr 分配提案的早期版本relied on std::allocator_traits<std::allocator<T>>::construct/destruct。他们后来移动到 std::construct_at 作为 constexpr 构造函数,construct 将引用它。

所以 construct_at 是在可以提供对 placement-new 的 objective 改进时添加的。

我认为应该有一个标准的构造函数。 事实上 libc++ 在文件 stl_construct.h.

中有一个实现细节
namespace std{
...
  template<typename _T1, typename... _Args>
    inline void
    _Construct(_T1* __p, _Args&&... __args)
    { ::new(static_cast<void*>(__p)) _T1(std::forward<_Args>(__args)...); }
...
}

我认为它是有用的东西,因为它可以让 "placement new" 成为朋友。 对于需要 uninitialized_copy 到默认堆(例如从 std::initializer_list 元素)的仅移动类型,这是一个很好的自定义点。)


我有自己的容器库,它重新实现了 detail::uninitialized_copy(范围)以使用自定义 detail::construct:

namespace detail{
    template<typename T, typename... As>
    inline void construct(T* p, As&&... as){
        ::new(static_cast<void*>(p)) T(std::forward<As>(as)...);
    }
}

它被声明为 move-only class 的友元,以允许仅在放置新的上下文中进行复制。

template<class T>
class my_move_only_class{
    my_move_only_class(my_move_only_class const&) = default;
    friend template<class TT, class...As> friend void detail::construct(TT*, As&&...);
public:
    my_move_only_class(my_move_only_class&&) = default;
    ...
};

std::construct_at 已添加到 C++20。这样做的论文是More constexpr containers。据推测,这与 C++17 中的 placement new 相比没有足够的优势,但 C++20 改变了一些事情。

添加此功能的提案的目的是支持 constexpr 内存分配,包括 std::vector。这需要能够将对象构造到分配的存储中。但是,就 void * 而不是 T * 而言,只是简单的放置新交易。 constexpr 评估目前无法访问原始存储,委员会希望保持这种状态。库函数 std::construct_at 添加类型化接口 constexpr T * construct_at(T *, Args && ...).

这还有一个好处,就是不需要用户指定正在构造的类型;它是从指针的类型推导出来的。正确调用 placement new 的语法有点可怕且违反直觉。比较 std::construct_at(ptr, args...)::new(static_cast<void *>(ptr)) std::decay_t<decltype(*ptr)>(args...).