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

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

本文关键字:construct at std C++17 为什么      更新时间:2023-10-16

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);

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

  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)消除了在两个地方更改相同内容的需要。

    干燥是好的。

  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没有提供放置new客观改进。在这两种情况下,您都必须声明要创建的类型。在这两种情况下,都必须提供构造函数的参数。在这两种情况下,都必须提供指向内存的指针。

所以没有必要通过你假设的std::construct_at来解决.

客观上,它的能力不如放置新。您可以这样做:

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

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

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


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

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

早期版本的 constexpr 分配提案依赖于std::allocator_traits<std::allocator<T>>::construct/destruct.他们后来搬到std::construct_at作为constexpr建筑功能,construct会提到。

因此,当可以提供对新放置的客观改进时,添加了construct_at

std::construct_at已添加到C++20。这样做的论文是More constexpr容器。据推测,与 C++17 中的新放置相比,这并没有被认为有足够的优势,但 C++20 改变了事情。

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

这也具有不要求用户指定正在构造的类型的优点;它是从指针的类型推导出来的。正确调用放置 new 的语法有点可怕且违反直觉。将std::construct_at(ptr, args...)::new(static_cast<void *>(ptr)) std::decay_t<decltype(*ptr)>(args...)进行比较。

有这样的事情,但不像你想象的那样命名:

  • 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) 默认在内存的未初始化区域中构造对象,由 start 和 count 定义 (函数模板)
  • uninitialized_value_construct(C++17) 通过在未初始化的内存区域中进行值初始化来构造对象,该区域由范围定义 (函数模板)
  • uninitialized_value_construct_n(C++17) 通过在未初始化的内存区域中进行值初始化来构造对象,该区域由 start 和 count 定义

std::allocator_traits::construct.曾经在std::allocator中还有一个,但在标准委员会文件D0174R0中被删除了。

我认为应该有一个标准的构造函数。 事实上,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)...); }
...
}

我认为拥有它是否有用,因为它可以让"新安置"成为朋友。 对于需要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)...);
}
}

它被声明为仅移动类的友元,以仅允许在放置 new 的上下文中进行复制。

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;
...
};

construct似乎没有提供任何语法糖。此外,它的效率低于新放置。绑定到引用参数会导致临时具体化和额外的移动/复制构造:

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!

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

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