5 Reasons make_unique Rejects Bounded Arrays (2025 Fix)
Ever wondered why `std::make_unique<T[N]>` fails in C++? Discover the 5 key reasons behind this limitation and how the upcoming C++26 standard plans to fix it.
Dr. Alistair Sterling
C++ committee member and author specializing in modern C++ memory management techniques.
The `make_unique` Conundrum: Why Bounded Arrays Get the Cold Shoulder
In the world of modern C++, `std::make_unique` is the gold standard for creating dynamically allocated unique objects. It’s safe, efficient, and prevents common memory leaks by bundling allocation and smart pointer construction into a single, exception-safe operation. You can use it to create single objects and even dynamic arrays of unknown size at compile time:
// Works perfectly!
auto single_ptr = std::make_unique<int>(42);
// Also works perfectly for dynamic (unbounded) arrays!
auto array_ptr = std::make_unique<int[]>(10); // Allocates 10 ints
But then you try to allocate a C-style array with a size known at compile-time—a bounded array—and you're met with a stark compiler error:
// Why won't you compile?!
auto bounded_array_ptr = std::make_unique<int[10]>(); // ERROR!
This inconsistency has puzzled developers for years. Why does the language provide a tool that handles single objects and unbounded arrays, but explicitly rejects their bounded cousins? It feels like a strange omission. The good news is that this long-standing gap is slated to be fixed in C++26. In this article, we'll dive into the five core reasons behind this design decision and explore how the upcoming standard will finally resolve it.
Reason 1: A Clash in the C++ Type System
The primary reason lies in the fundamental distinction C++ makes between unbounded arrays (`T[]`) and bounded arrays (`T[N]`). They are not just different; they are treated as entirely separate types by the compiler.
- `T[]` (Unbounded Array): This type represents an array of `T` with a size that is not known at compile time. It’s a type that can only exist in specific contexts, like function parameters or as the template argument for `std::unique_ptr`. It signifies a pointer to the first element of a dynamically sized block of memory.
- `T[N]` (Bounded Array): This type represents an array of `T` with a size `N` that is a compile-time constant. This is a complete object type. You can create an instance of it on the stack, like `int my_array[10];`.
The original `std::make_unique` implementation provided two overloads: one for non-array types (`T`) and one for unbounded arrays (`T[]`). Adding a third, distinct overload for `T[N]` was seen as adding complexity for a case that had a viable, and arguably better, alternative.
Reason 2: Ambiguous Function Overloads
Function template overloading is a powerful but delicate mechanism. The C++ standard committee must be extremely careful not to introduce ambiguities. The existing overloads for `std::make_unique` are:
// 1. For non-array types
template<class T, class... Args>
unique_ptr<T> make_unique(Args&&... args);
// 2. For unbounded arrays
template<class T>
unique_ptr<T> make_unique(size_t size); // where T is U[]
A hypothetical `make_unique<T[N]>()` would need its own template specialization. The problem is that bounded arrays often decay into pointers (`T*`), which could create confusion with the primary template for `make_unique
Reason 3: The `delete` vs. `delete[]` Mandate
The entire purpose of `std::unique_ptr`'s array specializations is to ensure the correct deallocation function is called. A `std::unique_ptr<T>` calls `delete`, while a `std::unique_ptr<T[]>` calls `delete[]`. Mixing these up leads to undefined behavior.
A smart pointer managing a bounded array, `int[10]`, must also use `delete[]`. Therefore, `std::make_unique<int[10]>()` would need to return a `std::unique_ptr<int[]>`. This mapping—where the requested type `T[N]` doesn't match the smart pointer's managed type `T[]`—was considered unintuitive. It breaks the mental model where `make_unique
Reason 4: The Rise of a Better Alternative: `std::array`
Perhaps the most compelling reason for the omission was the introduction of `std::array` in C++11. If you need a fixed-size array on the heap, the modern, idiomatic C++ way is not to use a raw C-style array, but to wrap it in a `std::array`:
// The modern, explicit, and safe way
auto ptr = std::make_unique<std::array<int, 10>>();
This approach has several advantages:
- Explicitness: The code clearly communicates its intent.
- Rich Interface: You get all the benefits of `std::array`, like `.size()`, `.begin()`, `.end()`, and bounds-checking with `.at()`.
- Type Safety: It returns a `unique_ptr<std::array<int, 10>>`. The type is preserved, and the pointer correctly uses `delete` (not `delete[]`) because it's deleting a single `std::array` object.
From the committee's perspective, providing a direct `make_unique
Reason 5: Avoiding the Pointer Decay Trap
C-style arrays have a notorious habit of "decaying" into pointers at the slightest provocation. For instance, when you pass an array to a function, what the function actually receives is a pointer to the first element. The size information is lost.
void process_data(int data[]) { // Actually receives int*
// sizeof(data) here gives the size of a pointer, not the array!
}
int main() {
int my_data[20];
process_data(my_data); // Size information is lost here
}
This behavior can lead to subtle bugs. By disallowing `make_unique
Dynamic Array Allocation Methods Compared
Method | Syntax | Size Knowledge | Deleter Used | Best For |
---|---|---|---|---|
`new T[N]` (Raw) | `int* p = new int[10];` | Runtime | `delete[]` (manual) | Legacy code; avoid in modern C++. |
`make_unique<T[]>(N)` | `auto p = make_unique<int[]>(10);` | Runtime | `delete[]` (automatic) | Dynamically-sized arrays where size is known at runtime. |
`make_unique<std::array>` | `auto p = make_unique<std::array<int, 10>>();` | Compile-time | `delete` (automatic) | Fixed-size arrays with a rich, modern container interface. |
`make_unique<T[N]>()` (C++26) | `auto p = make_unique<int[10]>();` | Compile-time | `delete[]` (automatic) | Convenience for fixed-size arrays when `std::array` overhead is not desired. |
The C++26 Fix: Welcoming `make_unique` into the Fold
Despite the valid reasons for its original exclusion, the C++ community has consistently requested support for `make_unique
How the Fix Works: A New Overload
The fix involves adding a new constrained overload to the `
// 3. New overload in C++26 for bounded arrays
template<class T> requires (std::is_bounded_array_v<T>)
unique_ptr<T> make_unique();
This template is constrained to only activate when `T` is a bounded array type (e.g., `int[10]`). It will allocate memory for `N` elements and return a `std::unique_ptr<T>` which, in this context, correctly resolves to a type that uses `delete[]`. This elegant solution uses modern C++ features (like concepts with `requires`) to eliminate the ambiguity that worried the original designers.
Code in Action: Before and After C++26
Let's see the practical impact. Creating a heap-allocated, fixed-size array of 50 characters becomes much more direct.
Before C++26 (the `std::array` way):
// Verbose but effective
auto buffer = std::make_unique<std::array<char, 50>>();
(*buffer)[0] = 'a';
With C++26:
// Direct, concise, and clear
auto buffer = std::make_unique<char[50]>();
buffer[0] = 'a';
The new syntax is undeniably more concise and feels more natural for those coming from a C-style array background, while still providing the full safety of `std::unique_ptr`.
A More Consistent and Safer Future for C++
The initial decision to exclude `make_unique
However, as the language and its user base have matured, the calculus has changed. The upcoming C++26 fix recognizes that convenience and consistency have value. By leveraging modern template techniques, the standard can now provide this feature without compromising safety. This change closes a small but noticeable gap in the language's memory management toolkit, making C++ an even more powerful and consistent language for developers.