C++ Programming

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.

A

Adrian Volkov

Senior C++ Developer specializing in modern C++ standards, performance, and memory management.

7 min read4 views

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:

  1. `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.
  2. `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`) but no corresponding smart factory function. This meant we had to fall back to using `new` directly.

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:

  1. `new Widget()`
  2. `calculate_priority()`
  3. `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`. The elements are value-initialized, which for fundamental types like `int` or `double` means they are zero-initialized. For class types, their default constructor is called.

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` value-initializes every element (e.g., setting all bytes to zero), `make_unique_for_overwrite` performs default-initialization. For fundamental types like `int` or `char`, this means their initial value is indeterminate. This can provide a significant performance boost when allocating large arrays, as the compiler can skip the step of clearing the memory, especially if you plan to immediately overwrite the contents anyway (e.g., reading data from a file or network socket into a buffer).

Comparison: Array Allocation Methods

C++ Smart Pointer Array Allocation Comparison
MethodSyntaxC++ VersionInitializationException SafetyBest For
unique_ptr<T[]>(new T[N])Verbose, direct newC++11+Value-initializationGood, but less robust in complex expressionsLegacy code or pre-C++20 environments.
std::make_unique<T[]>(N)Clean, modernC++20+Value-initialization (e.g., zeroed)ExcellentGeneral purpose, safe array allocation. The default choice in C++20.
std::make_unique_for_overwrite<T[]>(N)Clean, modernC++20+Default-initialization (indeterminate values for primitives)ExcellentHigh-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(size)` should be your default tool for creating dynamically sized arrays managed by a smart pointer.

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.