C++ Programming

Why make_unique Hates Arrays: 3 Core Reasons Explained

Discover the 3 core reasons why modern C++ developers often avoid `std::make_unique<T[]>` for dynamic arrays. Learn about its hidden pitfalls and better alternatives.

A

Alexei Volkov

C++ performance engineer and advocate for modern memory management best practices.

6 min read4 views

An Introduction to Smart Creation

In the world of modern C++, the mantra is clear: "Prefer smart pointers over raw pointers." Following this, a key guideline from the C++ Core Guidelines (R.11) is to "Avoid calling new and delete explicitly." The primary tool for this is std::make_unique, a factory function introduced in C++14 that provides exception-safe, clean, and efficient dynamic object creation.

When creating a single object, its superiority is unquestionable:

// Clean, safe, and efficient
auto p_widget = std::make_unique<Widget>(10, "hello");

But what about dynamic arrays? The standard library provides an overload for this exact purpose: std::make_unique<T[]>(size_t size). On the surface, it seems like the perfect counterpart for managing dynamic arrays. However, experienced C++ developers often treat this overload with caution, if not outright disdain. Why does std::make_unique seem to "hate" its own array version? The reasons are not about performance but about design, safety, and usability. Let's dive into the three core reasons why this tool is often the wrong choice.

Reason 1: The Crippling Lack of Constructor Argument Forwarding

The first and most significant drawback lies in how objects within the array are initialized. The elegance of std::make_unique for single objects is its ability to perfectly forward constructor arguments.

The Power of Single-Object Creation

Consider a class DatabaseConnection that requires a connection string and a timeout value to be constructed. With the single-object overload, this is trivial and expressive:

class DatabaseConnection {
public:
    DatabaseConnection(const std::string& conn_str, int timeout_ms) { /* ... */ }
};

// Perfectly forwards arguments to the constructor
auto conn_ptr = std::make_unique<DatabaseConnection>("user=admin;db=prod", 5000);

This code is not only clean but also robust. It ensures the DatabaseConnection object is always created in a valid state.

The Array Overload's Frustrating Limitation

Now, let's try to create an array of these connections. You might intuitively try something like this:

// This does NOT compile!
auto conn_pool = std::make_unique<DatabaseConnection[]>(5, "user=admin;db=prod", 5000);

The compiler will greet you with a wall of errors. The reason is simple: the array overload std::make_unique<T[]>(size) does not accept arguments to forward to the elements' constructors. It can only value-initialize the elements in the array. For class types, this means it calls the default constructor (the one with no arguments). If your class, like our DatabaseConnection, does not have a default constructor, you cannot create an array of it using std::make_unique<T[]> at all.

This limitation relegates std::make_unique<T[]> to arrays of primitive types (like int or char) or to arrays of class types that are default-constructible. This severely curtails its usefulness for managing arrays of complex, stateful objects, which is a common need in software development.

Reason 2: The Raw Pointer Deception and Type System Weakness

Even when you can use make_unique<T[]>, you quickly run into another fundamental problem: the resulting smart pointer behaves more like a C-style raw pointer than a modern C++ object.

The Missing Size Problem

When you create an array, the most crucial piece of metadata, besides the data itself, is its size. Let's see how std::unique_ptr<T[]> handles this:

size_t array_size = 100;
auto numbers = std::make_unique<int[]>(array_size);

// How do we get the size back from 'numbers'?
// You can't. There's no .size() or .length() method.
size_t what_is_my_size = ???; // Impossible

The std::unique_ptr<T[]> object itself does not store the size of the allocation. The type specialization correctly calls delete[] upon destruction, preventing memory leaks, but that's where its array-awareness ends. The size you passed to make_unique is immediately lost.

A Return to C-Style Programming

This missing information forces you back into the old, error-prone patterns of C-style programming. To do anything useful with the array, you must pass its pointer and its size around as separate entities.

void process_data(const int* data, size_t size) {
    for (size_t i = 0; i < size; ++i) {
        // ... process data[i]
    }
}

// We must manually track and pass the size.
size_t array_size = 100;
auto numbers = std::make_unique<int[]>(array_size);

process_data(numbers.get(), array_size); // Feels like 1998 all over again.

This pattern is fragile. It's easy for the pointer and the size to become desynchronized, leading to buffer overflows or other memory-related bugs. This completely undermines one of the primary goals of modern C++: to create self-contained, stateful objects that reduce cognitive load and prevent errors.

Reason 3: Standard Containers Are Almost Always Better

The final and most compelling reason to avoid std::make_unique<T[]> is that the C++ standard library already provides vastly superior tools for the job: std::vector and std::array.

For dynamic arrays, std::vector is the undisputed champion. It solves every problem we've discussed:

  • Constructor Flexibility: You can create a vector of non-default-constructible objects with ease. std::vector<T> vec(size, constructor_args...) works perfectly.
  • Size Management: A vector always knows its size via the .size() method. No more manual tracking.
  • Rich Interface: It provides a wealth of useful features: resizing (push_back, resize), bounds-checked access (.at()), and full compatibility with the Standard Template Library (STL) algorithms via iterators.

Instead of a fragile `unique_ptr` and a separate `size_t`, you have a single, robust `std::vector` object.

// Using std::vector
std::vector<DatabaseConnection> conn_pool(5, "user=admin;db=prod", 5000);

// Pass the whole object. It's self-contained and safe.
void process_connections(const std::vector<DatabaseConnection>& pool) {
    for (const auto& conn : pool) {
        // ...
    }
}

process_connections(conn_pool);

This code is safer, more expressive, and easier to maintain. For fixed-size arrays where the size is known at compile time, std::array offers similar benefits with the performance of a C-style array on the stack.

Comparison: std::unique_ptr<T[]> vs. std::vector<T>

Feature Comparison for Dynamic Arrays
Feature std::unique_ptr<T[]> (from make_unique) std::vector<T>
Memory Management Automatic via RAII (calls delete[]) Automatic via RAII (more sophisticated allocator support)
Size Tracking No. Size must be tracked manually. Yes. Via .size() method.
Resizing No. Fixed size upon creation. Yes. Can grow or shrink (e.g., push_back, pop_back, resize).
Element Construction Only supports value-initialization (default constructor). Yes. Can construct elements with arbitrary arguments.
Bounds Checking No. operator[] provides raw access. Yes. Via .at() method (throws exception on out-of-bounds).
STL Algorithm Compatibility Limited. Requires manual pointer arithmetic or std::span (C++20). Excellent. Provides .begin() and .end() iterators.

So, When Is make_unique<T[]> Actually Useful?

Despite its many shortcomings, std::make_unique<T[]> isn't entirely useless. It serves a few niche, low-level purposes.

Interfacing with C-style APIs

The most common valid use case is when you need to interface with a C library or an older C-style API that expects to take ownership of a raw, heap-allocated array. You can create the array with make_unique, pass the raw pointer using .release(), and transfer ownership to the C API. This is an advanced use case that requires careful handling of ownership semantics.

Fixed-Size Buffers in Performance-Critical Code

In some extreme performance-critical scenarios, the small overhead of a std::vector (which stores a pointer to data, a size, and a capacity) might be considered too high. A std::unique_ptr<T[]> is just a single pointer, making it the same size as a raw pointer. If you need a simple, fixed-size memory buffer on the heap and are willing to manage the size manually for performance reasons, it can be a viable, if primitive, tool.

Conclusion: Choose Your Tools Wisely

The array overload of std::make_unique exists to provide an exception-safe way to allocate a C-style array on the heap. However, its design limitations make it a poor choice for general-purpose C++ programming. It fails to provide constructor flexibility, it discards essential size information, and it encourages error-prone manual bookkeeping.

The modern C++ answer to dynamic arrays is, and should be, std::vector. It is safer, more powerful, and more expressive. While std::make_unique<T[]> has its place as a low-level tool for specific interoperability or performance niches, for the vast majority of cases, you should let it gather dust and reach for the superior container that is std::vector.