The Ultimate 2025 Fix: std::make_unique & Bounded Arrays
Unlock the full power of C++20! Discover why `std::make_unique` for bounded arrays was broken and how a simple fix makes your code safer and more modern in 2025.
Adrian Volkov
Senior C++ developer specializing in performance optimization and modern language features.
Introduction: The Annoying Papercut in Modern C++
For years, C++ developers have embraced the principles of modern C++: RAII (Resource Acquisition Is Initialization), smart pointers, and a general avoidance of raw `new` and `delete`. The introduction of std::unique_ptr
in C++11 was a game-changer, and its helper function, std::make_unique
(standardized in C++14), made exception-safe memory management clean and concise. It felt like we had finally solved memory leaks.
Almost. There was a subtle, frustrating, and surprisingly persistent inconsistency that felt like a papercut. You could create a smart pointer to a single object with ease:
// Works perfectly!
auto ptr = std::make_unique<MyClass>();
You could even create a smart pointer to a dynamic array of unknown size at compile time:
// Also works perfectly!
size_t n = 10;
auto arr_ptr = std::make_unique<MyClass[]>(n);
But if you tried to create a smart pointer to an array of a known size at compile time (a bounded array), you were met with a wall of compiler errors:
// Fails to compile before C++20!
auto bounded_arr_ptr = std::make_unique<MyClass[10]>();
This gap in functionality forced developers back into the world of raw `new`, breaking the clean, modern C++ paradigm. Thankfully, C++20 delivered the fix. As we look towards 2025, it's time to ensure this old, unsafe pattern is fully eradicated from our codebases. This post dives deep into what was broken, how C++20 fixed it, and why it's a critical update for every C++ developer.
The Pre-C++20 Dilemma: A Tale of Two Arrays
To understand the fix, we must first understand the problem. The issue revolved around the different type signatures for arrays of known and unknown bounds.
The Two Flavors of `unique_ptr` Arrays
std::unique_ptr
has two specializations for arrays:
std::unique_ptr<T[]>
: For arrays of unknown bound. It correctly callsdelete[]
upon destruction.std::unique_ptr<T[N]>
: For arrays of known bound. This was the source of the trouble.
The C++14 standard provided a version of std::make_unique
for the first case, std::unique_ptr<T[]>
. It worked beautifully. However, the standard explicitly deleted the overload for the second case, std::unique_ptr<T[N]>
. The rationale was complex, related to preventing ambiguity and subtle errors in function template argument deduction.
The Unsafe Workaround
Because std::make_unique<T[N]>()
was forbidden, developers had to fall back on this less-safe pattern:
// The old, clunky, and less-safe way
std::unique_ptr<int[]> p(new int[10]);
Why is this considered unsafe? It separates resource allocation (`new int[10]`) from resource management (`std::unique_ptr`'s constructor). If an exception were to occur between these two steps (perhaps in a more complex expression), the memory allocated by `new` would be leaked. The very purpose of `std::make_unique` is to bundle these two operations into a single, atomic, exception-safe function call. This workaround completely defeated that purpose.
C++20 to the Rescue: The Fix We've All Been Waiting For
The C++ standards committee recognized this inconsistency. Proposal P1973R1, adopted into C++20, finally addressed the issue by adding support for creating `unique_ptr`s to arrays of known bounds.
With C++20, the code that previously failed to compile now works exactly as you'd expect:
// C++20 and beyond: It just works!
// Creates a unique_ptr to an array of 10 integers, value-initialized to zero.
auto ptr = std::make_unique<int[10]>();
// Creates a unique_ptr to an array of 20 MyClass objects, default-constructed.
auto obj_ptr = std::make_unique<MyClass[20]>();
The new overload is std::make_unique<T[N]>()
. It takes no arguments because the size, N
, is part of the type. The array elements are value-initialized. This means for fundamental types like `int`, they are zero-initialized. For class types, their default constructor is called.
This change restores consistency to the language. The rule is now simple: use `std::make_unique` for all single-object and array allocations managed by `std::unique_ptr`. No more exceptions, no more unsafe workarounds.
Comparison: Array Allocation Before and After C++20
Let's visualize the difference. The table below summarizes the state of `std::make_unique` for various allocation types in C++14/17 versus C++20.
Allocation Type | Pre-C++20 (C++14/17) Syntax | C++20 Syntax | Notes |
---|---|---|---|
Single Object | std::make_unique<T>() | std::make_unique<T>() | No change. Always the preferred method. |
Array of Unknown Bound | std::make_unique<T[]>(size) | std::make_unique<T[]>(size) | No change. Used when size is a runtime variable. |
Array of Known Bound | Compilation Error Workaround: std::unique_ptr<T[]>(new T[N]) | std::make_unique<T[N]>() | The key fix. C++20 provides a safe, modern syntax. |
Why This Matters for Your Code in 2025
This might seem like a small syntactical change, but its impact on code quality, safety, and maintainability is significant.
Promoting Consistency and Safety
Modern C++ is built on a foundation of consistent, safe patterns. The `std::make_unique` hole was a jarring exception. By fixing it, C++20 allows development teams to enforce a simple, universal rule: never use a naked `new`. This reduces the cognitive load on developers and eliminates an entire class of potential memory leak bugs.
Improving Teachability
When teaching C++ to newcomers, explaining the pre-C++20 `std::make_unique` behavior was awkward. You had to present a powerful tool and immediately follow up with a list of caveats and unsafe workarounds. Now, the story is clean. `std::make_unique` is the one-stop shop for `std::unique_ptr` creation, period.
Universal Compiler Support
As of 2025, there's no reason not to use C++20 features. All major compilers—GCC (10+), Clang (10+), and MSVC (v19.29+)—have had full support for this feature for years. Migrating your build system to a C++20-compliant standard is a prerequisite for writing truly modern, safe C++.
Practical Guidance: `std::unique_ptr<T[]>` vs. `std::vector`
With this new power comes the responsibility to use it wisely. When should you reach for std::make_unique<T[N]>()
instead of the ubiquitous std::vector<T>
?
For 95% of cases, std::vector
is still the correct choice. It manages its own memory, knows its own size, and can grow dynamically. It's a robust, feature-rich container that should be your default for dynamic arrays.
However, a fixed-size heap array managed by std::unique_ptr
is superior in specific scenarios:
- Interfacing with C APIs: When a C library requires a pointer to a contiguous, fixed-size block of memory,
std::unique_ptr<T[N]>
is a perfect fit. You get RAII-based lifetime management for a resource that the C API will treat as a simple C-style array. - Performance-Critical Code: If you have a buffer whose size is known at compile time and you absolutely cannot afford the overhead of
std::vector
's size/capacity tracking or potential reallocations,std::unique_ptr<T[N]>
provides raw performance with modern C++ safety. - Embedded Systems: In memory-constrained environments, the minimal overhead of a
std::unique_ptr
compared to astd::vector
can be a meaningful advantage.
The key is that the array size must be a compile-time constant. If the size is determined at runtime, you must use std::make_unique<T[]>(runtime_size)
or, more likely, std::vector<T>(runtime_size)
.
Conclusion: A Small Change, A Big Leap for C++
The C++20 fix for std::make_unique
and bounded arrays is a perfect example of the language's evolution. It's not a flashy new feature, but a careful, considered change that smooths over a rough edge, making the entire ecosystem safer, more consistent, and easier to use correctly.
In 2025, writing new T[N]
should be a major code review red flag. The modern, safe, and now fully consistent solution is here. Embrace std::make_unique<T[N]>()
, clean up your legacy code, and enjoy a more robust C++ development experience.