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

C++C++17Placement New

C++ Problem Overview


C++17 adds std::destroy_at, but there isn't any std::construct_at counterpart. Why is that? Couldn't it be implemented as simply as the following?

template <typename T, typename... Args>
T* construct_at(void* addr, Args&&... args) {
  return new (addr) T(std::forward<Args>(args)...);
}

Which would enable to avoid that not-entirely-natural placement new syntax:

auto ptr = construct_at<int>(buf, 1);  // instead of 'auto ptr = new (buf) int(1);'
std::cout << *ptr;
std::destroy_at(ptr);

C++ Solutions


Solution 1 - C++

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.

Solution 2 - C++

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

Solution 3 - C++

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

Solution 4 - C++

There is std::allocator_traits::construct. There used to be one more in std::allocator, but that got removed, rationale in standards committee paper D0174R0.

Solution 5 - C++

I think there should be a standard construct-function. In fact libc++ has one as an implementation detail in the file 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)...); }
...
}

I think is it something useful to have because it allows to make "placement new" a friend. This is a great customization point for a move-only type that need uninitialized_copy into the default heap (from an std::initializer_list element for example.)


I have my own container library that reimplements a detail::uninitialized_copy (of a range) to use a custom 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)...);
	}
}

Which is declared a friend of a move-only class to allow copy only in the context of placement 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;
    ...
};

Solution 6 - C++

construct does not seem to provide any syntactic sugar. Moreover it is less efficient than a placement new. Binding to reference arguments cause temporary materialization and extra move/copy construction:

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!

Unfortunately there isn't any way to avoid these extra copy/move construction when calling a function. Forwarding is almost perfect.

On the other hand, construct_at in the library could complete the standard library vocabulary.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionDaniel LangrView Question on Stackoverflow
Solution 1 - C++Nicol BolasView Answer on Stackoverflow
Solution 2 - C++David StoneView Answer on Stackoverflow
Solution 3 - C++Marek RView Answer on Stackoverflow
Solution 4 - C++user10316011View Answer on Stackoverflow
Solution 5 - C++alfCView Answer on Stackoverflow
Solution 6 - C++OlivView Answer on Stackoverflow