68

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);
Daniel Langr
  • 22,196
  • 3
  • 50
  • 93
  • 23
    in my ignorance i rather wonder what `destroy_at` is good for ;) – 463035818_is_not_an_ai Oct 24 '18 at 10:27
  • 4
    @user463035818 - How would one do a pseudo destructor call for a `std::string` object? Don't get me wrong, I'm not saying it's impossible, I just want you to imagine the required expression vividly. – StoryTeller - Unslander Monica Oct 24 '18 at 10:29
  • @user463035818 this is useful when implementing advanced low level stuff like for example [std::any](https://en.cppreference.com/w/cpp/utility/any) or [std::optional](https://en.cppreference.com/w/cpp/utility/optional). In everyday code you will not use that. – Marek R Oct 24 '18 at 10:45
  • 5
    @StoryTeller Let me be another one naive, but what is wrong with `auto s = new string{"test"}; s->~string();`? Am I missing premise of the question? I guess calling pseudo destructors on compound template types would be syntax nightmare, but `typedef` suffices. Is `destroy_at` solving same problems as `make_*` wrappers? – luk32 Oct 24 '18 at 10:48
  • @luk32 - Try it without a using declaration in sight. Fully qualify `std::string`. – StoryTeller - Unslander Monica Oct 24 '18 at 10:52
  • 1
    Ok, I think I got it. I edited the comment. It's same as `make_Stuff` wrappers around c'tors, yup? – luk32 Oct 24 '18 at 10:54
  • 6
    @luk32 There is no destructor called `~string()`. You would have to invoke `s->~basic_string();`. `std::destroy_at(s);` is ok. – Daniel Langr Oct 24 '18 at 10:54
  • @luk32 - An alias can be a solution, yes. But in this case an alias is also the problem. So you know, it's less than a stellar situation. – StoryTeller - Unslander Monica Oct 24 '18 at 10:56
  • 2
    @DanielLangr Using type alias for dtor works as good as for ctor, at least here: https://godbolt.org/z/V_Hf7Y – luk32 Oct 24 '18 at 10:56
  • 4
    @luk32 It worked because you used `using namespace std;`. Otherwise, you cannot write something as `s->~std::string();`, or, at least, compilers don't compile it (see, e.g., here: https://stackoverflow.com/q/24593942/580083 for related discussion). That's where `std::destroy_at` helps. – Daniel Langr Oct 24 '18 at 11:00
  • 3
    The committee reflectors just had a discussion about adding `construct_at` yesterday :) (It's for the container constexprization effort.) – T.C. Oct 24 '18 at 15:39
  • 1
    I think it is a good idea, because once this function is standard you can declare it a friend of a class with a protected copy constructor, for which you want to allow copy-placement-new. (see my answer). – alfC Nov 01 '18 at 07:41

6 Answers6

52

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.

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
  • 6
    It's a pity that the Standards committee didn't just add support for the syntax `ptr->~auto();`... but perhaps there's a most-vexing-parse sort of issue there. – Sneftel Oct 24 '18 at 15:15
  • 3
    @Sneftel I doubt it's an MVP issue. I imagine it's a combination of 1) the committee's conservative approach to new syntax and 2) the question of if `auto` refers to the type of the pointer or the type it was constructed with (or the type of iterator if `ptr` is actually an iterator!). – jonspaceharper Oct 24 '18 at 16:35
  • 2
    @Sneftel: The problem really is that "allocate and construct" and "construct in memory" both use the same keyword and general syntax: `new`. By contrast, "destroy and deallocate" and "destroy" use different syntax and different keywords: `delete` vs. calling the destructor. So while `ptr->auto()` would solve this problem, the foundational problem is that C++ is incongruous with this. It'd be better for consistency to have a placement-`delete`-type thing. – Nicol Bolas Oct 25 '18 at 01:28
  • Can one make "placement new" a friend of a class? If not, then there could be a legitimate use a for a standard `std::construct`, because then you can allow selective copy-placement-new for a move only class. `std::construct` could be similarly to the already existing libc++'s `_Construct`. (see my answer below). – alfC Nov 01 '18 at 08:01
  • @alfC: Placement-`new` does not in any way interact with your class. It doesn't construct your `T`; it's a no-op function that returns the pointer it is given. What constructs your `T` is the fact that you provided it to a `new`-expression. Furthermore, using access controls like this is typically not the best way to go. You can't even friend `make_shared/unique` because there's no guarantee that `make_*` functions don't delegate the constructing duties to some internal function. It's better to use private key-types for access controls of this nature. – Nicol Bolas Nov 01 '18 at 13:17
  • @alfC: A "private key-type" is an empty class that can only be constructed by friends of the class you want to control. Non-friends can copy one, but they can't create it. This allows code which is given access to delegate that access to others. – Nicol Bolas Nov 01 '18 at 13:20
  • I agree about the limitations produced by delegating to some internal function. Having `std::construct` at least gives a concrete name to a very likely delegated function. That is why I suggest that `_Construct` should be standard. Question: so, are selected functions the friends declared in the "private key-type"? Can you point to an example that uses this technique? – alfC Nov 01 '18 at 17:57
  • @alfC: It's not a difficult thing to write. You make an empty class with a private default constructor, but *public* copy/move constructors/operators (defaulted). You have your main class take one of these objects as a parameter to any "private" constructor. And presumably your main class is a friend of the key class, so that it can call its own constructors, but it can also pass the key object along to others. I see no reason for standardizing `_Construct` when you could just call it `std::construct` with the same limitations you would impose on `_Construct` (whatever those are). – Nicol Bolas Nov 01 '18 at 18:32
  • @NicolBolas, sorry, I don't understand your point yet. Feel free to comment in my answer below. – alfC Nov 06 '18 at 08:35
  • Placement new does construct your object. That is the entire point! It's not for constructing an object in memory that you called `new` on (as `new` already constructs the object), it is for constructing an object in suitable storage. For instance, containers like `std::vector` call placement new (through `std::allocator::construct`) to construct your object in their storage. – David Stone Mar 21 '19 at 04:16
  • @DavidStone: "*Placement new does construct your object.*" Placement `new` *syntax* constructs your object; the actual `operator new` overload called by that syntax *does not*. So making the `operator new` for placement `new` a friend will accomplish nothing. – Nicol Bolas Mar 21 '19 at 13:33
  • Is there any reason they didn't simply enable placement-new for constant expressions? Because aside from that part, palcement-new is still more capable, right? – Deduplicator Jan 02 '21 at 03:46
18

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

David Stone
  • 26,872
  • 14
  • 68
  • 84
14

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
muru
  • 4,723
  • 1
  • 34
  • 78
Marek R
  • 32,568
  • 6
  • 55
  • 140
  • 1
    No `emplace`-style parameter-forwarding function though. – Quentin Oct 24 '18 at 10:35
  • 1
    Is any of these functions able to forward arbitrary arguments to constructor? – Daniel Langr Oct 24 '18 at 10:35
  • @Quentin they all operate on ranges. If you have one set of params and one location you can just placement-new – Caleth Oct 24 '18 at 10:42
  • 3
    @MarekR How would you use `std::uninitialized_move` to construct a single object while forwarding arguments? What would you use as a 3rd argument of `std::uninitialized_move`, which represents a destination iterator? – Daniel Langr Oct 24 '18 at 11:08
  • @DanielLangr: Why would you need to? Why can't you just use placement-`new`? – Nicol Bolas Oct 24 '18 at 13:25
  • @NicolBolas I can, of course. Just using `std::construct_at` as a counterpart for `std::destroy_at` seems to me more _symmetric_ and _pretty_. It's just a matter of code readibility. – Daniel Langr Oct 24 '18 at 13:52
  • 7
    This makes no sense, -1. Those are all range algorithms. In the same way that we have [`std::destroy()`](https://en.cppreference.com/w/cpp/memory/destroy) which is the range destruction algorithm. We're talking about a single construction - there are not N variations of construction. Or rather, there are variations (list-init, paren-init, default-init) but an algorithm wouldn't capture that. See Nicol's answer. – Barry Oct 24 '18 at 14:48
  • @Barry, I agree with you that these are not exacct substitutes for `std::construct(_at)`. Perhaps the answer means that if you need you can pretend that a call `std::uninitialized_copy_n(&other, 1, place_ptr)` behaves like a hypothetical `std::construct_at(place_ptr, other)`. – alfC Nov 01 '18 at 18:01
9

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.

Konrad Rudolph
  • 530,221
  • 131
  • 937
  • 1,214
  • 1
    There is also `std::allocator_traits::destroy` and still, there is `std::destroy_at` as well. – Daniel Langr Oct 24 '18 at 10:37
  • @DanielLangr IMHO it’s a better question who this redundancy exists. Whoever suggested `std::destroy_at` for inclusion probably had a specific use-case in mind but I’m not sure why the committee went along. – Konrad Rudolph Oct 24 '18 at 13:28
  • @KonradRudolph: `allocator_traits::destroy` is for destroying an object *allocated through an allocator*. That means you have to pass it an allocator object that can destroy it. `destroy_at` is for destroying an object by calling its destructor. – Nicol Bolas Oct 24 '18 at 13:57
  • @NicolBolas Just pass an appropriate allocator. I agree though that this leads to excessive syntactic noise. I honestly don’t understand the design rationale of the changed `` interface at all. The old allocator API was flawed, sure, but the new one doesn’t strike me as better. – Konrad Rudolph Oct 24 '18 at 14:00
  • @KonradRudolph: "*Just pass an appropriate allocator.*" But there *isn't one* in this case, because the object was *not* created through an allocator's `allocator_traits::construct` call. – Nicol Bolas Oct 24 '18 at 14:11
  • @NicolBolas You can *create* such an allocator (but the default `std::allocator` will also work). Check the documentation. All it needs to do, in fact, is to *not* declare a `destroy` overload because in that case `allocator_traits::destroy` will invoke `p->~T()`. – Konrad Rudolph Oct 24 '18 at 14:26
  • 2
    @KonradRudolph: Or you could just call `std::destroy_at`. – Nicol Bolas Oct 24 '18 at 14:29
  • @NicolBolas Of course. See my previous comment regarding this option. The point is: why does this redundancy exist — but only for deallocation, not for allocation? – Konrad Rudolph Oct 24 '18 at 14:30
  • 2
    @KonradRudolph: My point is that there is no redundancy. They're doing two different things, via two different mechanisms, for two different reasons... that happen to work out to be the same thing in this *particular* case. – Nicol Bolas Oct 24 '18 at 14:31
  • @NicolBolas That isn’t true. `std::destroy_at` is strictly a special case and a wrapper for a special case of the more general `std::allocator_traits` solution (different API, but *same* mechanism). I think it’s arguably a *valid* addition but only because the allocator API is so messed up. – Konrad Rudolph Oct 24 '18 at 14:33
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/182435/discussion-between-nicol-bolas-and-konrad-rudolph). – Nicol Bolas Oct 24 '18 at 14:34
1

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;
    ...
};
alfC
  • 14,261
  • 4
  • 67
  • 118
  • "*it allows to make "placement new" a friend.*" Why would you need to make "placement new" a friend? The purpose of making constructors private is to prevent people from constructing a class in some way. If you make such a low-level construct a friend, you're not actually preventing people from doing anything. *Anyone* can copy your "non-copyable" type; they just have to spell it slightly differently. This is different from making `make_shared/unique` a friend, since those actually add value beyond constructing their object. – Nicol Bolas Nov 06 '18 at 14:19
  • It should also be noted that making `std::construct` a friend would make this possible: `optional t(some_instance);`, depending on the implementation of `optional`. How exactly is this a "move only type" if you can copy it so directly and so easily? – Nicol Bolas Nov 06 '18 at 14:27
0

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.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Oliv
  • 17,610
  • 1
  • 29
  • 72
  • 1
    That could be easily incorporated into `construct_at` as well, by returning a `T*` pointer from placement new. That's not a reason why such a function could not exist. – Daniel Langr Oct 24 '18 at 12:01
  • By the way, how would you implement a `std::vector`? When you `push_back`/`emplace_back`, you need to use placement new to create an object in vector's buffer. And then, you don't return this particular pointer (returned by placement new) in `operator[]()`. You simply return a pointer from some member variable incremetned by some index. Which is the same as `ptr` in the code above. – Daniel Langr Oct 24 '18 at 12:04
  • @DanielLangr It is a well known problem, std::vector is not implementable (or easily, I think I have a solution) if one only try to use core language. – Oliv Oct 24 '18 at 12:10
  • @DanielLangr So construct would be declared `template[...] T* construct_at(void*,Args&&...)`, it would be very similar to placement new, and we would loose some copy-construction elision due to the biding to `Args&&...` arguments. Those bindings would force temporary materialization. – Oliv Oct 24 '18 at 12:13
  • @DanielLangr Because your edit just made my previous answer obsolete, I erased it. – Oliv Oct 24 '18 at 12:29
  • Thanks for pointing out that there was a problem in my code. However, I asked about generic `construct_at` question, which can be used, e.g., with static buffer as well (`std::aligned_storage`). So my question is not bound to that specific scenario. – Daniel Langr Oct 24 '18 at 12:55
  • 1
    @DanielLangr I think we are close to the opion based question/answer. Because I think that `construct_at` could complete the standard library vocabulary. – Oliv Oct 24 '18 at 13:10