25

It appears that in C++20, we're getting some additional utility functions for smart pointers, including:

template<class T> unique_ptr<T> make_unique_for_overwrite();
template<class T> unique_ptr<T> make_unique_for_overwrite(size_t n);

and the same for std::make_shared with std::shared_ptr. Why aren't the existing functions:

template<class T, class... Args> unique_ptr<T> make_unique(Args&&... args); // with empty Args
template<class T> unique_ptr<T> make_unique(size_t n);

enough? Don't the existing ones use the default constructor for the object?

Note: In earlier proposals of these functions, the name was make_unique_default_init().

einpoklum
  • 118,144
  • 57
  • 340
  • 684

2 Answers2

20

These new functions are different:

  • Original make_XYZ: Always initializes the pointed-to value ("explicit initialization", see § class.expl.init in the standard).
  • New make_XYZ_for_overwrite: Performs "default initialization" of the pointed-to value (see § dcl.init, paragraph 7 in the standard); on typical machines, this means effectively no initialization for non-class, non-array types. (Yes, the term is a bit confusing; please read the paragraph at the link.)

This is a feature of plain vanilla pointers which was not available with the smart pointer utility functions: With regular pointers you can just allocate without actually initializing the pointed-to value:

new int

For unique/shared pointers you could only achieve this by wrapping an existing pointer, as in:

std::unique_ptr<int[]>(new int[n])

now we have a wrapper function for that.

Note: See the relevant ISO C++ WG21 proposal as well as this SO answer

einpoklum
  • 118,144
  • 57
  • 340
  • 684
  • @LightnessRacesinOrbit: I edited to say "on typical machines". – einpoklum Sep 23 '19 at 13:18
  • 3
    I don't know why you don't just use the right terms for what it is :( – Lightness Races in Orbit Sep 23 '19 at 13:31
  • @LightnessRacesinOrbit: Because they're confusing. I want to explain that if we use `_default_init` no time will be wasted initializing arrays of `int`s. – einpoklum Sep 23 '19 at 14:07
  • They're especially confusing when you use them to mean the opposite of what they actually are! – Lightness Races in Orbit Sep 23 '19 at 15:04
  • @LightnessRacesinOrbit: Now I'm confused. – einpoklum Sep 23 '19 at 15:18
  • 1
    @LightnessRacesinOrbit: Can you either edit to clarify, or suggest how I should rephrase what I've written? – einpoklum Dec 28 '20 at 09:30
  • *"... this means effectively no initialization for non-class, non-array types."* Just to (hopefully) add some clarification: array types **are** still eligible for the optimization of not being initialized unnecessarily. Their elements are also default-initialized, which in turn leads to no initialization if the elements are of non-class, non-array types. – 303 May 19 '22 at 14:06
16

allocate_shared, make_shared, and make_unique all initialize the underlying object by performning something equivalent to new T(args...). In the zero-argument case, that reduces to new T() - which is to say, it performs value initialization. Value initialization in many cases (including scalar types like int and char, arrays of them, and aggregates of them) performs zero initialization - which is to say, that is actual work being done to zero out a bunch of data.

Maybe you want that and that is important to your application, maybe you don't. From P1020R1, the paper that introduced the functions originally named make_unique_default_init, make_shared_default_init, and allocate_shared_default_init (these were renamed from meow_default_init to meow_for_overwrite during the national ballot commenting process for C++20):

It is not uncommon for arrays of built-in types such as unsigned char or double to be immediately initialized by the user in their entirety after allocation. In these cases, the value initialization performed by allocate_shared, make_shared, and make_unique is redundant and hurts performance, and a way to choose default initialization is needed.

That is, if you were writing code like:

auto buffer = std::make_unique<char[]>(100);
read_data_into(buffer.get());

The value initialization performed by make_unique, which would zero out those 100 bytes, is completely unnecessary since you're immediately overwriting it anyway.

The new meow_for_overwrite functions instead perform default initialization since the memory used will be immediately overwritten anyway (hence the name) - which is to say the equivalent of doing new T (without any parentheses or braces). Default initialization in those cases I mentioned earlier (like int and char, arrays of them, and aggregates of them) performs no initialization, which saves time.


For class types that have a user-provided default constructor, there is no difference between value initialization and default initialization: both would just invoke the default constructor. But for many other types, there can be a large difference.

Barry
  • 286,269
  • 29
  • 621
  • 977
  • 1
    The redundant initialization I couldn't care less about. The problem (for me) with default initialization is that that instantiates the memory pages on the socket of core zero. If you're doing multithreaded numerical calculations (for instance with OpenMP) that means that threads running on the other sockets will constantly be pulling NUMA memory from socket zero. On a simple demo program (heat equation) I've seen a factor of 2 performance degradation from this. – Victor Eijkhout Aug 08 '21 at 16:32
  • @VictorEijkhout - interesting comment - I don't get why the solution to that problem isn't that the threads running on other sockets allocate the memory themselves for those unique_ptrs? Is it some kind of OpenMP thing that they can't? Or is it that code just isn't usually written that way? – davidbak Jan 05 '22 at 00:49
  • @davidbak The unique pointer is created by one thread, and then the default initialization is also done by that one thread, so all pages are instantiated in the memory of the socket where that thread is running. (This is pretty obscure stuff. Do you know what NUMA is? That's basically the source of teh problem.) – Victor Eijkhout Jan 05 '22 at 01:44
  • @VictorEijkhout - yes I know what NUMA is, I just don't get why you can't create placeholder empty unique ptrs, pass _those_ to your threads, and then those threads themselves allocate local-node memory to put in those unique_ptrs. – davidbak Jan 05 '22 at 01:55
  • @davidbak Unique pointers into what? – Victor Eijkhout Jan 05 '22 at 05:06