Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Custom initialize array with std::make_unique

Tags:

c++

unique-ptr

Say I would like to create a std::unique_ptr<int[]>, but I would like to initialize the created array to custom values: {1,2,3,4,5}.

I can use new and pass the raw pointer to std::unique_ptr constructor which will then own and manage it.

std::unique_ptr<int[]> ptr{ new int[5]{1,2,3,4,5} };

My question is, can the same somehow be done with std::make_unique?

like image 809
Killzone Kid Avatar asked Apr 08 '18 23:04

Killzone Kid


2 Answers

There are 3 overloads for std::make_unique:

template< class T, class... Args >
unique_ptr<T> make_unique( Args&&... args );     // (1) for non-array type

template< class T >
unique_ptr<T> make_unique( std::size_t size );   // (2) for array type with unknown bounds

template< class T, class... Args >
/* unspecified */ make_unique( Args&&... args ) = delete; // (3) for array type with known bounds.

None of them supports the behavior you want (note that the third function is marked as delete).

You can use (2) and initialize the elements of array separately, or switch to std::vector and use (1).

like image 196
hgminh Avatar answered Sep 30 '22 12:09

hgminh


hgminh's answer (in particular the part recommending vector if possible) is correct, but I just wanted to add another option.

If the array bounds are known and fixed, not unknown bound variable length C-style arrays, you could switch from C-style arrays to std::array to achieve this. With optimizations turned on at all, the runtime work is equivalent (at -O1 with g++, it correctly determines that it can inline the whole thing, making it a plain allocation, followed by populating the individual elements in the newly allocated memory directly, rather than trying to make an array on the stack, then passing it as the argument to make_unique, which would eventually invoke the move constructor, effectively doubling the work for std::array<POD type>). You'd just change:

std::unique_ptr<int[]> ptr{ new int[5]{1,2,3,4,5} };

to:

auto ptr = std::make_unique<std::array<int, 5>>(std::array<int, 5>{1,2,3,4,5});

Sadly, with current non-experimental feature set, this does require repeatedly specifying the type being pointed to (once to construct it, once to define the templated type of make_unique), because make_unique doesn't accept initializer lists, so you have to construct a temporary syntactically, even if the optimizer avoids it. For this particular case, you could use experimental features to avoid repeating yourself, but it's not much prettier (and if you don't use using statements to avoid specifying the namespaces, actually longer):

auto ptr = std::make_unique<std::array<int, 5>>(std::experimental::make_array(1,2,3,4,5));

The main advantage and disadvantage to std::array over C-style arrays is that, either way, the end result is std::unique_ptr<std::array<int, 5>>, not std::unique_ptr<int[]>; on the one hand, the size of the array being pointed to can never change (you couldn't later replace the unique_ptr contents with a pointer to std::array<int, 6>), but on the other hand, the size is baked in at compile time so both you and the compiler know the size.

Since the compiler knows the size, when calling functions that are templated on their argument type, you don't have to manually pass along both pointer and size. The template will be compile-time specialized to your precise size (which allows the compiler to make better optimization choices on loop unrolling or using constant loop bounds) without passing the size at all.

For functions that aren't templated and expect C-style arguments (e.g. they expect an array, and receive int* of first element and size_t for length), you just pass &ptr[0] as the pointer and ptr->size() as the length. Since the size is a compile time constant, this gets you DRY for free (no repeating the size of the array in multiple places, nor are you defining fairly useless named constants solely to avoid DRY; the size is part of the type definition, used inline, with obvious meaning in context), with no performance overhead (it should inline to the compile time size exactly as if you typed the size yourself, but without the risk of numbers getting out of sync if the array's size is changed later).

Again, to be absolutely clear, the correct answer here is almost always "Use std::vector<int>", which is similar to a std::unique_ptr<int[]> that:

  1. Automatically resizes the int[] as needed
  2. Simplifies common use cases dramatically (e.g. initializing, copying, moving, etc.)

When the size is not being actively changed, std::vector can be used with C-style array APIs just fine (passing vec.data()/&vec[0]/&vec.at(0) as the pointer and vec.size() as the length), and you don't need to worry about managing resizing/reallocations (which are a pain in C++ when you can't use realloc without giving up access to delete[]). It can theoretically be a tiny bit slower, but in 99% of cases, it will be faster than anything that has to reimplement vector-like behaviors from scratch (because vector is tuned to "just work" at maximum speed, while your own code is unlikely to be a carefully tuned).

like image 36
ShadowRanger Avatar answered Sep 30 '22 12:09

ShadowRanger