Last Update:
C++ Smart Pointers and Arrays
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 forT[]
forunique_ptr
is supported since C++11, butmake_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 forT[]
forshared_ptr
is supported since C++17, butmake_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, sostd::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 avector
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 ofstd::vector
(by using 8 bools in every byte), which is a catastrophe for something like HDF5, which doesn’t care about that compression. Withstd::vector<bool>
, HDF5 was eventually reading garbage because of that compression. Guess who was there for the rescue, in a case wherestd::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, becausestd::array
,std::vector
,std::string
are virtually always better data structure choices than raw arrays. About the only situation I can conceive of when astd::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.