为什么 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 改进:
它减少了冗余:
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 很好。
这很容易:
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...)
.
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 改进:
它减少了冗余:
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 很好。
这很容易:
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...)
.