std::destroy_at
provides two objective improvements over a direct destructor call:
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.
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.