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);
答案 0 :(得分:40)
std::destroy_at
在直接析构函数调用方面提供了两个客观的改进:
它减少了冗余:
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)
消除了在两个地方更改同一件事的需要。
干好。
这很容易:
auto ptr = allocates_an_object(...);
//Insert code here
ptr->~???; //What type is that again?
如果我们推断出指针的类型,那么删除它会变得很困难。您不能做ptr->~decltype(ptr)()
;因为C ++解析器无法正常工作。不仅如此,decltype
将类型推导为 pointer ,因此您需要从推导的类型中删除指针间接。引导您:
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
不能允许您选择想要的那个。如果不提供任何参数,它可以具有执行默认初始化的代码,但随后将无法提供用于值初始化的版本。而且它可以不使用任何参数进行值初始化,但是您无法默认初始化该对象。
答案 1 :(得分:13)
有这样的事情,但是not named like you might expect:
未初始化的副本 将一系列对象复制到未初始化的内存区域
uninitialized_copy_n (C ++ 11) 将许多对象复制到未初始化的内存区域 (功能模板)
未初始化的填充 将对象复制到未初始化的内存区域(由范围定义) (功能模板)
答案 2 :(得分:8)
有std::allocator_traits::construct
。 std::allocator
中曾经有一个,但是standards committee paper D0174R0中的一个被删除了。
答案 3 :(得分:4)
std::construct_at
已添加到C ++ 20。这样做的论文是More constexpr containers。据推测,与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...)
进行比较。
答案 4 :(得分:0)
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
可以完成标准库词汇。
答案 5 :(得分:0)
我认为应该有一个标准的构造函数。
实际上,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;
...
};