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

std::destroy_at provides two objective improvements over a direct destructor call:

  1. It reduces redundancy:

     T *ptr = new T;
     //Insert 1000 lines of code here.
     ptr->~T(); //What type was that again?
    

    Sure, we'd all prefer to just wrap it in a unique_ptr and be done with it, but if that can't happen for some reason, putting T there is an element of redundancy. If we change the type to U, we now have to change the destructor call or things break. Using std::destroy_at(ptr) removes the need to change the same thing in two places.

    DRY is good.

  2. It makes this easy:

     auto ptr = allocates_an_object(...);
     //Insert code here
     ptr->~???; //What type is that again?
    

    If we deduced the type of the pointer, then deleting it becomes kind of hard. You can't do ptr->~decltype(ptr)(); since the C++ parser doesn't work that way. Not only that, decltype deduces the type as a pointer, so you'd need to remove a pointer indirection from the deduced type. Leading you to:

     auto ptr = allocates_an_object(...);
     //Insert code here
     using delete_type = std::remove_pointer_t<decltype(ptr)>;
     ptr->~delete_type();
    

    And who wants to type that?

By contrast, your hypothetical std::construct_at provides no objective improvements over placement new. You have to state the type you're creating in both cases. The parameters to the constructor have to be provided in both cases. The pointer to the memory has to be provided in both cases.

So there is no need being solved by your hypothetical std::construct_at.

And it is objectively less capable than placement new. You can do this:

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

These are different. In the first case, the object is default-initialized, which may leave it uninitialized. In the second case, the object is value-initialized.

Your hypothetical std::construct_at cannot allow you to pick which one you want. It can have code that performs default initialization if you provide no parameters, but it would then be unable to provide a version for value initialization. And it could value initialize with no parameters, but then you couldn't default initialize the object.


Note that C++20 added std::construct_at. But it did so for reasons other than consistency. They're there to support compile-time memory allocation and construction.

You can call the "replaceable" global new operators in a constant expression (so long as you haven't actually replaced it). But placement-new isn't a "replaceable" function, so you can't call it there.

Earlier versions of the proposal for constexpr allocation relied on std::allocator_traits<std::allocator<T>>::construct/destruct. They later moved to std::construct_at as the constexpr construction function, which construct would refer to.

So construct_at was added when objective improvements over placement-new could be provided.


There is such a thing, but not named like you might expect:

  • uninitialized_copy copies a range of objects to an uninitialized area of memory

  • uninitialized_copy_n (C++11) copies a number of objects to an uninitialized area of memory (function template)

  • uninitialized_fill copies an object to an uninitialized area of memory, defined by a range (function template)

  • uninitialized_fill_n copies an object to an uninitialized area of memory, defined by a start and a count (function template)
  • uninitialized_move (C++17) moves a range of objects to an uninitialized area of memory (function template)
  • uninitialized_move_n (C++17) moves a number of objects to an uninitialized area of memory (function template)
  • uninitialized_default_construct (C++17) constructs objects by default-initialization in an uninitialized area of memory, defined by a range (function template)
  • uninitialized_default_construct_n (C++17) constructs objects by default-initialization in an uninitialized area of memory, defined by a start and a count (function template)
  • uninitialized_value_construct (C++17) constructs objects by value-initialization in an uninitialized area of memory, defined by a range (function template)
  • uninitialized_value_construct_n (C++17) constructs objects by value-initialization in an uninitialized area of memory, defined by a start and a count

std::construct_at has been added to C++20. The paper that did so is More constexpr containers. Presumably, this was not seen to have enough advantages over placement new in C++17, but C++20 changes things.

The purpose of the proposal that added this feature is to support constexpr memory allocations, including std::vector. This requires the ability to construct objects into allocated storage. However, just plain placement new deals in terms of void *, not T *. constexpr evaluation currently has no ability to access the raw storage, and the committee wants to keep it that way. The library function std::construct_at adds a typed interface constexpr T * construct_at(T *, Args && ...).

This also has the advantage of not requiring the user to specify the type being constructed; it is deduced from the type of the pointer. The syntax to correctly call placement new is kind of horrendous and counter-intuitive. Compare std::construct_at(ptr, args...) with ::new(static_cast<void *>(ptr)) std::decay_t<decltype(*ptr)>(args...).