【问题标题】:Why isn't there a std::construct_at in C++17?为什么 C++17 中没有 std::construct_at ?
【发布时间】:2019-03-28 17:24:24
【问题描述】:

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

【问题讨论】:

  • 在我的无知中,我很想知道destroy_at 有什么好处;)
  • @user463035818 - 如何对std::string 对象进行伪析构函数调用?不要误会,我不是说不可能,我只是想让你形象地想象所需的表达方式。
  • @StoryTeller 让我再天真一点,但auto s = new string{"test"}; s-&gt;~string(); 有什么问题?我错过了问题的前提吗?我想在复合模板类型上调用伪析构函数将是语法噩梦,但typedef 就足够了。 destroy_at 是否解决了与 make_* 包装器相同的问题?
  • @luk32 没有称为~string() 的析构函数。您必须调用s-&gt;~basic_string();std::destroy_at(s); 没问题。
  • @luk32 因为你使用了using namespace std;,所以它起作用了。否则,你不能写成s-&gt;~std::string();,或者,至少,编译器不会编译它(参见,例如,这里:stackoverflow.com/q/24593942/580083 的相关讨论)。这就是std::destroy_at 提供帮助的地方。

标签: c++ c++17 placement-new


【解决方案1】:

有这样的事情,但是not named like you might expect

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

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

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

  • uninitialized_fill_n 将对象复制到由 start 和 count 定义的未初始化内存区域 (函数模板)
  • 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 定义

【讨论】:

  • 虽然没有emplace-style 参数转发功能。
  • 这些函数是否能够将任意参数转发给构造函数?
  • @Quentin 他们都在范围内操作。如果您有一组参数和一个位置,则只需放置新
  • @MarekR 您如何在转发参数时使用std::uninitialized_move 构造单个对象?你会用什么作为std::uninitialized_move 的第三个参数,它代表一个目标迭代器?
  • 这毫无意义,-1。这些都是范围算法。就像我们有std::destroy() 一样,这是范围破坏算法。我们谈论的是一个单一的结构 - 没有 N 个结构的变体。或者更确切地说,存在变体(list-init、paren-init、default-init),但算法无法捕捉到这一点。请参阅 Nicol 的回答。
【解决方案2】:

std::allocator_traits::constructstd::allocator 中曾经还有一个,但被删除了,原因在 standards committee paper D0174R0

【讨论】:

  • 还有std::allocator_traits::destroy,还有std::destroy_at
  • @DanielLangr 恕我直言,这是一个更好的问题,这种冗余存在于谁。建议将std::destroy_at 纳入其中的人可能有一个特定的用例,但我不确定委员会为什么会同意。
  • @KonradRudolph:allocator_traits::destroy 用于销毁通过分配器分配的对象。这意味着您必须向它传递一个可以销毁它的分配器对象。 destroy_at 用于通过调用析构函数来销毁对象。
  • @KonradRudolph:或者你可以打电话给std::destroy_at
  • @KonradRudolph:我的意思是没有冗余。他们通过两种不同的机制做两件不同的事情,出于两个不同的原因......在这个特定案例中恰好是同一件事。
【解决方案3】:

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可以补全标准库词汇。

【讨论】:

  • 这也可以很容易地合并到construct_at中,方法是从placement new返回一个T*指针。这不是不能存在这样一个功能的原因。
  • 顺便问一下,你将如何实现std::vector?当您push_back/emplace_back 时,您需要使用placement new 在vector 的缓冲区中创建一个对象。然后,您不会在operator[]() 中返回这个特定的指针(由placement new 返回)。您只需从某个索引递增的某个成员变量返回一个指针。与上面代码中的ptr 相同。
  • @DanielLangr 这是一个众所周知的问题,如果只尝试使用核心语言,std::vector 是不可实现的(或者很容易,我想我有一个解决方案)。
  • @DanielLangr 所以构造将被声明为template[...] T* construct_at(void*,Args&amp;&amp;...),它与placement new 非常相似,并且由于对Args&amp;&amp;... 参数的约束,我们会丢失一些复制构造省略。这些绑定将强制临时实现。
  • @DanielLangr 我认为我们已经接近基于选项的问题/答案。因为我觉得construct_at可以补全标准库词汇。
【解决方案4】:

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-&gt;~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 运算符(只要您实际上没有替换它)。但是placement-new不是一个“可替换”的函数,所以你不能在那里调用它。

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

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

【讨论】:

  • 很遗憾,标准委员会不只是添加对语法 ptr-&gt;~auto();... 的支持,但那里可能存在最棘手的解析问题。
  • @Sneftel 我怀疑这是 MVP 问题。我想它是 1)委员会对新语法的保守方法和 2)auto 是否指指针的类型或其构造的类型(或迭代器的类型,如果ptr 是实际上是一个迭代器!)。
  • @Sneftel:问题在于“分配和构造”和“在内存中构造”都使用相同的关键字和通用语法:new。相比之下,“destroy and deallocate”和“destroy”使用不同的语法和不同的关键字:delete 与调用析构函数。所以虽然ptr-&gt;auto() 可以解决这个问题,但基本问题是 C++ 与此不一致。最好有一个placement-delete-type的东西。
  • 可以让“安置新”成为班级的朋友吗?如果不是,那么标准std::construct 可能有合法用途,因为您可以允许选择性复制放置新用于仅移动类。 std::construct 可能类似于已经存在的 libc++ 的 _Construct。 (见下面我的回答)。
  • @alfC: Placement-new 不会以任何方式与您的班级互动。它不会构造你的T;它是一个无操作函数,返回给定的指针。构造您的T 的原因是您将它提供给new 表达式。此外,使用这样的访问控制通常不是最好的方法。你甚至不能成为make_shared/unique 的朋友,因为不能保证make_* 函数不会将构建职责委托给某些内部函数。最好使用私钥类型进行这种性质的访问控制。
【解决方案5】:

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

它被声明为只允许移动类的朋友,只允许在放置新的上下文中复制。

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

【讨论】:

  • "它允许让“新位置”成为朋友。" 为什么需要让“新位置”成为朋友?将构造函数设为私有的目的是防止人们以某种​​方式构造类。如果你让这样一个低级的构造成为朋友,你实际上并没有阻止人们做任何事情。 任何人都可以复制您的“不可复制”类型;他们只需要稍微不同的拼写。这与让make_shared/unique 成为朋友不同,因为它们实际上在构造对象之外增加了价值。
  • 还应注意,将std::construct 设为朋友将使这成为可能:optional&lt;my_move_only_class&gt; t(some_instance);,具体取决于optional 的实现。如果您可以如此直接且如此轻松地复制它,这究竟是一种“仅移动类型”?
【解决方案6】:

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

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2019-05-21
    • 2020-12-31
    • 1970-01-01
    • 2017-03-18
    • 2016-01-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多