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

时间:2018-10-24 10:23:04

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

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

6 个答案:

答案 0 :(得分:40)

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将类型推导为 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) 将许多对象复制到未初始化的内存区域 (功能模板)

  • 未初始化的填充 将对象复制到未初始化的内存区域(由范围定义) (功能模板)

  • 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) 通过值初始化在未初始化的内存区域中构造对象,该内存由开始和计数定义

答案 2 :(得分:8)

std::allocator_traits::constructstd::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;
    ...
};