Smart pointers are very versatile and can hold pointers not only to single instances but also to arrays. Is that only a theoretical use case? or maybe they might be handy in some cases? Let’s have a look.

Smart pointers for T[]

At C++ Stories, you can find lots of information about smart pointers - see this separate tag for this area. For completeness, I’d like to mention an interesting issue on smart pointers and arrays.

If you want to create a unique_ptr, you can write:

class Object { };

// unique_ptr
auto ptr = std::make_unique<Object>();
auto intPtr = std::make_unique<int>();

// or shared_ptr
auto shared = std::make_shared<Object>();
auto intShared = std::make_shared<int>();

In the example, you see pointers to a single instance of Object or an integer.

Similarly, if you want to have a pointer to an array of objects then you’re happy to do it in C++:

auto objects = std::make_unique<Object[]>(10);
auto ptr = std::make_unique<int[]>(10);
std::cout << ptr[0] << '\n';
std::cout << ptr[9] << '\n';      

In the above example, make_unique returns a pointer to an array of 10 elements.

The specialization for T[] for unique_ptr is supported since C++11, but make_unique for arrays is available since C++14.

And for shared pointers:

auto shared = std::make_shared<int[]>(10);
std::cout << shared[0] << '\n';
std::cout << shared[9] << '\n';
The specialization for T[] for shared_ptr is supported since C++17, but make_shared for arrays is available since C++20.

If your compiler doesn’t support make_shared<T[]> you can write:

std::shared_ptr<int[]> shared(new int[10]());

Play with code @Compiler Explorer

But be warned! The following code will also work:

std::shared_ptr<int> shared(new int[10]());

But what happens when the memory is about to be deleted? Will the proper delete operator will be called?

That’s why it’s essential to make sure the pointer’s declaration matches the initializing expression.

Improvements in C++20

In C++20, we have a bunch of new functions with the suffix _for_overwrite. In short, they create arrays without performing value initialization. Compare this code:

new T[]()
// vs
new T[]
  • The first is “value initialization.” For arrays, initialize each element to zero (for built-in types) or call their default ctors.
  • The latter is called default initialization and, for built-in types, generates indeterminate values or calls default ctor.

make_unique uses the first option, while make_unique_for_overwrite uses the second approach.

auto ptr = std::make_unique_for_overwrite<int[]>(COUNT);

And after that line, the values inside ptr are indeterminate, so you should make sure you initialize buffer later.

Would you like to see more?
The _for_overwrite functions allow for even 20x init speed up of the initialization code! See my premium article with a benchmark which is available for C++ Stories Premium/Patreon members. See all Premium benefits here.

Ok, we covered how to create pointers and even saw some recent updates in C++20… but should we even use it?

How about using a proper container?

I guess it’s much more convenient to use std::vector<int> and pass it around than passing unique_ptr<int[]>?

Let’s have a look at some possible use cases.

Why is unique_ptr<T[]> useful?

When I wrote my early blog post on smart pointers - C++ Smart Pointers Gotchas - C++ Stories, back in 2013, I also asked a question at Stack Overflow.

c++ - Is there any use for unique_ptr with array? - Stack Overflow

And so far, it’s one of my most voted questions :)

I got several interesting answers, and we can summarize them with the following quote:

Some people do not have the luxury of using std::vector, even with allocators. Some people need a dynamically sized array, so std::array is out. And some people get their arrays from other code that is known to return an array; and that code isn’t going to be rewritten to return a vector or something.

And here’s a nice comparison for each technique:

Use case std::array std::vector std::unique_ptr<T[]>
Initial size the size to be specified at compile time runtime runtime
Resizing does not allow resizing can grow, shrink, change does not allow resizing (unless you recreate the whole thing)
Storage stores data directly in the object outside, usually on the heap outside, usually on the heap
Copying allows copying allows copying does not allow
Swap/move O(n) time swap and move operations, where n is the number of elements in the array O(1) time swap and move operations O(1) time swap and move operations
Pointer/reference/iterator invalidation ensures pointers, references and iterators will never be invalidated while the object is live, even on swap() vector may invalidate pointers and iterators when you have a reallocation has no pointers, iterators, so you can only invalidate it by swap
Compatibility with concepts and algorithms is a regular container is a regular container is not a container, so it doesn’t work with standard algorithms(*)

Update on algorithms

(*) update: unique_ptr<T[]> is not a container, so it won’t work with standard algorithms… at least out of the box. But you can get the pointer to the array and since a pointer is an iterator, you can pass it into an algorithm.

Pointed out by this comment at r/cpp.

Have a look:

auto ptr = std::make_unique<int[]>(10);
// init elements...

std::sort(ptr.begin(), ptr.end());  // compiler error! p is not a container

// but this works
std::sort(ptr.get(), ptr.get() + COUNT, ...);

See more code:

std::random_device rd;
std::mt19937 g(rd());

auto print = [](int* p, size_t count) {
    for (size_t i = 0; i < count; ++i)
        std::cout << p[i] << ", ";
    std::cout << "\n";
};

const size_t COUNT = 10;
auto ptr = std::make_unique<int[]>(COUNT);
std::iota(ptr.get(), ptr.get() + COUNT, 0);
print(ptr.get(), COUNT);

std::shuffle(ptr.get(), ptr.get() + COUNT, g);
print(ptr.get(), COUNT);

std::sort(ptr.get(), ptr.get() + COUNT);
print(ptr.get(), COUNT);

Play with code @Compiler Explorer

But is that “natural”?

Why handy?

In summary, I’d say that smart pointers for arrays are handy in two cases:

  • When you want to have complete control over the memory block. Standard containers might reallocate internal buffers, and your requirements and restrictions might not allow that. For example, when you’re in a hot path or some embedded environment.
  • To interoperate with C-Style APIs. Usually, such functions require raw pointers, and sometimes it might be more convenient to use a smart pointer to array.

Let’s have a look at some real-life stories.

Use Cases

Here’s what we can read in the comments and answers:

Memory-pool

I have used unique_ptr<char[]> to implement preallocated memory pools used in a game engine.

connecting with C-style functions

A common pattern can be found in some Windows Win32 API calls, in which the use of std::unique_ptr<T[]>

As an alternative to vector<bool>

I faced a case where I had to use std::unique_ptr<bool[]>, which was in the HDF5 library (A library for efficient binary data storage, used a lot in science). Some compilers (Visual Studio 2015 in my case) provide compression of std::vector (by using 8 bools in every byte), which is a catastrophe for something like HDF5, which doesn’t care about that compression. With std::vector<bool>, HDF5 was eventually reading garbage because of that compression. Guess who was there for the rescue, in a case where std::vector didn’t work, and I needed to allocate a dynamic array cleanly? :-)

If you're interested in smart pointers - have a look at my handy reference card. It covers everything you need to know about unique_ptr, shared_ptr and weak_ptr, wrapped in a beautiful PDF:

Summary

In this blog post, we had a look at ways to create and initialize smart pointers for arrays. You also learned that in C++20, we have a set of functions for skipping the value initialization part to achieve better performance.

Are such pointers helpful?

While they might have limited use, as it’s usually better to rely on standard containers, they might be handy in restricted environments. For example, when you want complete control over the memory buffer and when you interop with C-style API.

We can also summarize it with a quote from Effective Modern C++ by Scott Meyers

The existence of std::unique_ptr for arrays should be of only intellectual interest to you, because std::array, std::vector, std::string are virtually always better data structure choices than raw arrays. About the only situation I can conceive of when a std::unique_ptr<T[]> would make sense would be when you’re using a C-like API that returns a raw pointer to a heap array that you assume ownership of.

Back to you

Have you used smart pointers for arrays?

Share your story in the comments below the article.