C++ Programming

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.

A

Adrian Volkov

Senior C++ developer specializing in performance optimization and modern language features.

6 min read3 views

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 calls delete[] 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.

std::make_unique Usage Comparison
Allocation TypePre-C++20 (C++14/17) SyntaxC++20 SyntaxNotes
Single Objectstd::make_unique<T>()std::make_unique<T>()No change. Always the preferred method.
Array of Unknown Boundstd::make_unique<T[]>(size)std::make_unique<T[]>(size)No change. Used when size is a runtime variable.
Array of Known BoundCompilation 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 a std::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.