C++ Deep Dive: Why make_unique Fails on Arrays & How to Fix
Discover why `std::make_unique<T[N]>` was a C++ pitfall before C++20. Learn the correct way to create `std::unique_ptr` for arrays and avoid memory leaks.
Adrian Volkov
Senior C++ Developer specializing in modern C++ standards, performance, and memory management.
Introduction: The Familiar Compiler Error
If you've been working with modern C++, you know that `std::make_unique` is the preferred way to create `std::unique_ptr` instances. It's clean, concise, and exception-safe. So, it's only natural to assume that creating a dynamic array would be as simple as this:
// This will NOT compile in C++14 or C++17
auto my_array = std::make_unique<int[]>(10);
Instead of a shiny new smart pointer, you're greeted with a wall of cryptic compiler errors. Messages like "no matching function call to 'make_unique'" or "template argument deduction/substitution failed" are common. This isn't a bug in your compiler; it's a well-known, and for a long time, frustrating, gap in the C++ standard library. This deep dive will explore exactly why this fails, how developers worked around it for years, and how C++20 finally provided the elegant solution we always wanted.
The Core Mismatch: Why `unique_ptr` Has Two Personalities
To understand the problem with `make_unique`, we first need to understand `std::unique_ptr` itself. It's not a single template, but rather two different template specializations:
- `std::unique_ptr<T>` (for single objects): This is the primary template. When it goes out of scope, it calls `delete` on the raw pointer it manages.
- `std::unique_ptr<T[]>` (for arrays): This is a partial template specialization. When it goes out of scope, it correctly calls `delete[]` on the raw pointer. It also provides an `operator[]` for element access.
This distinction is critical. Calling `delete` on a pointer allocated with `new[]`, or `delete[]` on a pointer allocated with `new`, results in undefined behavior. The C++ memory model requires perfect symmetry between allocation and deallocation. The `unique_ptr` specializations handle this for you automatically, which is a key part of their value.
The C++14 Gap: Why `make_unique` Initially Lacked Array Support
`std::make_unique` was officially added in C++14. However, the proposal (N3656) only included the version for single objects:
template<class T, class... Args>
unique_ptr<T> make_unique(Args&&... args);
The committee was aware of the need for an array version, but it was left out of the initial standard. The reasons are complex, but it boils down to a desire to ship a simple, universally agreed-upon feature quickly. Adding array support introduced questions: Should it support arrays of unknown bound (`T[]`)? What about arrays of known bound (`T[N]`)? How should arguments be passed for initialization? To avoid getting bogged down, the simpler, single-object version was standardized first.
This left developers in an awkward position. We had a smart pointer designed for arrays (`unique_ptr
The Pre-C++20 Fix: The "Correct" but Verbose Way
Before C++20, the standard-compliant and correct way to create a `unique_ptr` to a dynamic array was to use `new[]` and pass the resulting raw pointer to the `unique_ptr` constructor.
// The correct way in C++11, C++14, and C++17
std::unique_ptr<int[]> my_array(new int[10]);
// You can then use it like a regular array
for (int i = 0; i < 10; ++i) {
my_array[i] = i * 2;
}
// Memory is automatically freed with delete[] when my_array goes out of scope.
This works perfectly fine. It correctly allocates the array and ensures `delete[]` is called upon destruction. The main drawback is the verbosity and the direct use of `new`, which modern C++ guidelines advise against.
The Exception Safety Caveat
The primary reason to prefer `make_...` functions is exception safety. Consider this contrived example:
void process_data(std::unique_ptr<Widget> w, int priority);
// Potentially unsafe call
process_data(std::unique_ptr<Widget>(new Widget()), calculate_priority());
The compiler is free to reorder these operations. It could execute them in this order:
- `new Widget()`
- `calculate_priority()`
- `std::unique_ptr` constructor
If `calculate_priority()` throws an exception, the memory allocated by `new Widget()` is never passed to the `unique_ptr` constructor, and the memory is leaked. Using `std::make_unique` solves this:
// Exception-safe call
process_data(std::make_unique<Widget>(), calculate_priority());
Here, the allocation and construction are bundled inside the `make_unique` call, preventing any leaks if `calculate_priority()` throws. While this specific scenario is less common with array allocations, the principle remains: factory functions are generally safer and lead to cleaner code.
The C++20 Savior: `std::make_unique` for Arrays Arrives
After years of developers writing their own helper functions or using the verbose `new[]` syntax, C++20 finally closed the gap. It introduced the overloads for `std::make_unique` that we always wanted.
Specifically, it added an overload for arrays of unknown bound:
// The C++20 way - clean, safe, and modern!
auto my_array = std::make_unique<int[]>(10);
my_array[5] = 100;
// All 10 integers are value-initialized (to 0 in this case).
This new overload does exactly what you'd expect: it allocates an array of the specified size using `new T[size]` and wraps it in a `std::unique_ptr
An overload for arrays of known bound (`T[N]`) was also added, though it is less commonly used:
// C++20 also supports arrays of known bound
auto my_fixed_array = std::make_unique<int[10]>(); // Size is part of the type
A Performance Boost: `std::make_unique_for_overwrite`
C++20 didn't just stop at fixing `make_unique`. It also introduced a new, performance-oriented factory function: `std::make_unique_for_overwrite`.
// C++20 - for when you don't need zero-initialization
auto my_buffer = std::make_unique_for_overwrite<char[]>(4096);
// The contents of my_buffer are indeterminate. You must write to them before reading.
The key difference is in initialization. While `make_unique
Comparison: Array Allocation Methods
Method | Syntax | C++ Version | Initialization | Exception Safety | Best For |
---|---|---|---|---|---|
unique_ptr<T[]>(new T[N]) | Verbose, direct new | C++11+ | Value-initialization | Good, but less robust in complex expressions | Legacy code or pre-C++20 environments. |
std::make_unique<T[]>(N) | Clean, modern | C++20+ | Value-initialization (e.g., zeroed) | Excellent | General purpose, safe array allocation. The default choice in C++20. |
std::make_unique_for_overwrite<T[]>(N) | Clean, modern | C++20+ | Default-initialization (indeterminate values for primitives) | Excellent | High-performance scenarios where the array will be immediately populated. |
Conclusion: Embracing Modern C++ for Cleaner Code
The journey of `std::make_unique` and its support for arrays is a perfect case study in the evolution of C++. What started as an inconvenient gap in C++14 has been fully and elegantly resolved in C++20. For developers today, the choice is clear: if you are using C++20 or later, `std::make_unique
By understanding the history—from the `delete` vs. `delete[]` distinction to the exception-safety benefits of factory functions—you can write more robust, readable, and efficient C++ code. And for those moments when every microsecond counts, C++20 even provides `make_unique_for_overwrite` as a powerful optimization tool.