DEV Community

Cover image for Why prefer std::make_unique over new in std::unique_ptr?
llzzz
llzzz

Posted on

Why prefer std::make_unique over new in std::unique_ptr?

In modern C++, when you need a std::unique_ptr, the idiomatic way to create it is:

auto ptr = std::make_unique<MyClass>();
Enter fullscreen mode Exit fullscreen mode

rather than manually new-ing the object inside the unique_ptr constructor:

std::unique_ptr<MyClass> ptr{new MyClass()};
Enter fullscreen mode Exit fullscreen mode

std::make_unique was introduced in C++14 to fill a gap that std::make_shared had already covered for shared ownership. It is a small function template, but it carries several important advantages. This article walks through every reason to prefer it, notes what changed in C++17 and C++20, and covers the cases where you still need the raw new form.

the implementation of std::make_unique is roughly the following:

namespace std
{
    template<typename T, typename... Args>
    std::unique_ptr<T> make_unique(Args... args) noexcept
    {
        return std::unique_ptr<T>(new T{std::forward<Args>(args)...});
    }
}
Enter fullscreen mode Exit fullscreen mode

1. Exception Safety

This is traditionally cited as the main reason for make_unique. Before C++17, the order in which function arguments are evaluated was unspecified. If you constructed a unique_ptr with new as one argument among several, a leak could occur if another argument threw after the new expression completed but before the unique_ptr constructor took ownership of the raw pointer.

Here is an example:

class T {};

struct U {
    U()
    {
        throw std::runtime_error("boom");
    }
};

int main()
{
    foo(std::unique_ptr<T>{new T{}}, std::unique_ptr<U>{new U{}});
}
Enter fullscreen mode Exit fullscreen mode

Before C++17, a compiler was permitted to evaluate new T{} first, then evaluate new U{}, and only then call the unique_ptr constructors. If new U{} threw, the memory allocated for T had not yet been handed to a unique_ptr, so it leaked.

std::make_unique closes this hole because the allocation and the ownership transfer happen inside a single function call. The temporary unique_ptr is fully constructed before the surrounding expression continues, so if a later argument throws, stack unwinding destroys the already-constructed unique_ptr and frees the memory.

What changed in C++17

C++17 tightened the evaluation-order rules. In a function call, each argument expression is now fully evaluated before the next argument is evaluated, although the order in which the arguments are evaluated relative to each other is still unspecified. Because of this, the manual new form is no longer leak-prone in the exact multi-argument scenario above: once new T{} completes and its unique_ptr temporary is constructed, that temporary is a fully formed object, and an exception from another argument will destroy it correctly.

So if you are compiling as C++17 or later, the exception-safety motivation for make_unique is largely historical. The remaining reasons below are still valid.

2. Reduced Redundancy and auto Type Deduction

Using make_unique lets you write the type name once and rely on auto:

std::unique_ptr<MyClass>(new MyClass(args)); // MyClass written twice
auto ptr = std::make_unique<MyClass>(args);  // written once
Enter fullscreen mode Exit fullscreen mode

Beyond brevity, this eliminates a class of bugs where the two type names drift apart during refactoring: one is changed but the other is not, producing a mismatched or ill-formed expression. The C++ Core Guidelines encode this as R.23: Use make_unique() to make unique_ptrs, citing both conciseness and exception safety.

Polymorphism caveat

make_unique does not perform polymorphic type erasure automatically. With auto, you get exactly the unique_ptr<T> corresponding to the template argument:

struct Base {
    virtual ~Base() = default;
};
struct Derived : Base {};

auto p1 = std::make_unique<Base>();    // std::unique_ptr<Base>
auto p2 = std::make_unique<Derived>(); // std::unique_ptr<Derived>, not unique_ptr<Base>
Enter fullscreen mode Exit fullscreen mode

If you want a base-class handle, you must state the target type explicitly:

std::unique_ptr<Base> p = std::make_unique<Derived>();
Enter fullscreen mode Exit fullscreen mode

This works because unique_ptr<Derived> converts to unique_ptr<Base> when Derived* converts to Base*. Also note the virtual destructor on Base: without it, deleting a Derived through a Base* is undefined behavior.

3. Enforcing a "No Raw new" Discipline

Every occurrence of new in application code must be audited for lifetime correctness: is it deleted exactly once, through the correct type, with the correct delete form? make_unique removes that audit burden from the call site. If the owning object has a correct lifetime, the unique_ptr it produces has a correct lifetime.

This lets you adopt a simple, mechanically checkable rule: do not write new in your codebase except inside the implementation of a factory or a low-level container. As Scott Meyers and others have put it, make_unique teaches “never say new/delete” without disclaimers.

4. Array Safety

make_unique distinguishes scalar and array types and returns the appropriate unique_ptr:

auto a = std::make_unique<int[]>(10); // std::unique_ptr<int[]>
auto b = std::make_unique<int>(42);   // std::unique_ptr<int>
Enter fullscreen mode Exit fullscreen mode

With the manual constructor, nothing prevents you from writing a mismatched pair such as std::unique_ptr<int>(new int[10]) or std::unique_ptr<int[]>(new int(42)), both of which lead to undefined behavior when the default deleter uses the wrong delete form.

(make_unique<T[N]> for a fixed bound N is also permitted, but it is almost always a mistake because unique_ptr<T[N]> does not have array-covariant behavior in the way you might expect.)

5. Consistency with make_shared and Easy Switching

You should already be using std::make_shared for shared ownership (it performs a single combined allocation, unlike shared_ptr<T>(new T)). Using make_unique gives your codebase a uniform creation syntax:

auto u = std::make_unique<T>(args);
auto s = std::make_shared<T>(args);
Enter fullscreen mode Exit fullscreen mode

This consistency pays off when requirements change. Switching between exclusive and shared ownership becomes a one-token edit:

auto p = std::make_unique<T>(args); // change to std::make_shared<T>(args)
Enter fullscreen mode Exit fullscreen mode

With the manual form, that switch requires rewriting the declaration, removing new, and reconciling the deleter.

6. std::make_unique_for_overwrite (C++20)

C++20 added std::make_unique_for_overwrite for the case where you want to allocate an object without value-initializing it:

auto buffer = std::make_unique_for_overwrite<char[]>(size);
auto obj    = std::make_unique_for_overwrite<MyPod>();
Enter fullscreen mode Exit fullscreen mode

For scalar types, make_unique<T>() value-initializes (T()), which zero-initializes fundamental types. For large buffers this can be an unnecessary cost. make_unique_for_overwrite performs default-initialization instead, matching what new T does for fundamental types, while preserving all the ownership and exception-safety benefits of the factory function.

When Not to Use std::make_unique

There are two well-defined cases where the manual constructor is still required.

Custom deleters

make_unique always uses std::default_delete<T>, which calls delete (or delete[]). It cannot accept a custom deleter, because its variadic template parameters are forwarded to the object constructor, not to the unique_ptr constructor. If you are wrapping a C API handle or a resource that is not released with delete, construct the unique_ptr directly:

std::unique_ptr<FILE, decltype(&fclose)> file{std::fopen("data.bin", "rb"), &fclose};
Enter fullscreen mode Exit fullscreen mode

A common workaround is to define your own make_unique overload for the resource type in your own namespace.

Private or protected constructors

std::make_unique is not a friend of your class, so it cannot invoke a private or protected constructor. If a class restricts construction to factory member functions, the factory must construct the raw pointer internally and pass it to a unique_ptr:

class Widget {
private:
    Widget();
public:
    static std::unique_ptr<Widget> create() {
        return std::unique_ptr<Widget>(new Widget());
    }
};
Enter fullscreen mode Exit fullscreen mode

or you can use the PassKey idiom if you really want to use std::make_unique

class Widget {
public:
    class Key {
    private:
        // this constructor is private
        Key() = default;
        friend class Widget;   // only this class can make a Key
    };

    // "Public" constructor, but requires a Key you can't forge.
    Widget(Key, int size) : size_(size) {}

    static std::unique_ptr<Widget> create(int size) {
        return std::unique_ptr<Widget>(new Widget(Widget::Key{}, size));
    }
private:
    int size_;
};

int main()
{
    Widget w(Widget::Key{}, 5);   // error: Key's constructor is private
}
Enter fullscreen mode Exit fullscreen mode

but the passkey idiom is a topic for a separate post, so we won't expand on it here.

Here's a more playful version of the summary, keeping the same substance:

Summary

Default to std::make_unique. Its headline selling point — exception safety — has lost some of its bite since C++17 tightened the evaluation-order rules, so consider the famous leak example a historical artifact rather than a live threat. The other arguments, though, are still pulling their weight: it writes the type once instead of twice, it plays nicely with auto, it keeps your arrays and scalars from getting their deletes crossed, and it lets you adopt a clean "no raw new" policy without asterisks.

Reach for the manual unique_ptr constructor only when make_unique genuinely can't help you: custom deleters (it always uses default_delete) and constructors that are private or protected. Everywhere else, let make_unique do the new-ing so you don't have to.

Top comments (0)