Contact
Evil Genius SocietyHomeLearnForumLeaderboardSocialsMessages
← Back to module

Lesson 5 of 6

std::make_unique<T>(args...) and std::make_shared<T>(args...) construct an object and wrap it in one step.

#include <iostream>
#include <memory>
#include <string>

struct Widget {
    std::string name;
    int size;

    Widget(std::string n, int s) : name(std::move(n)), size(s) {}
};

int main() {
    // The arguments are forwarded to Widget's constructor.
    auto a = std::make_unique<Widget>("alpha", 10);
    auto b = std::make_shared<Widget>("beta", 20);

    std::cout << a->name << " " << a->size << "\n";
    std::cout << b->name << " " << b->size << "\n";
    return 0;
}
alpha 10
beta 20

Both work with the new form too — std::unique_ptr<Widget>(new Widget("alpha", 10)). The factories are preferred for three reasons, and only the first is cosmetic.

Reason 1: the type is written once

auto a = std::unique_ptr<Widget>(new Widget("alpha", 10));   // Widget twice
auto b = std::make_unique<Widget>("alpha", 10);              // Widget once

Change the type and there is one place to edit rather than two, which cannot get out of sync.

Reason 2: make_shared allocates once

std::shared_ptr<T>(new T) performs two allocations: one for the object, one for the control block. make_shared performs one, placing both in a single block.

#include <iostream>
#include <memory>
#include <cstdlib>
#include <new>

int allocations = 0;

void* operator new(std::size_t size) {
    ++allocations;
    void* p = std::malloc(size);
    if (!p) {
        throw std::bad_alloc();
    }
    return p;
}

void operator delete(void* p) noexcept { std::free(p); }
void operator delete(void* p, std::size_t) noexcept { std::free(p); }

int main() {
    allocations = 0;
    {
        std::shared_ptr<int> a(new int(1));
        std::cout << "shared_ptr(new int):  " << allocations << " allocation(s)\n";
    }

    allocations = 0;
    {
        auto b = std::make_shared<int>(1);
        std::cout << "make_shared<int>:     " << allocations << " allocation(s)\n";
    }
    return 0;
}
shared_ptr(new int):  2 allocation(s)
make_shared<int>:     1 allocation(s)

Half the allocations, and the object sits next to its counter in memory, so touching one brings the other into cache.

The trade-off: because both live in one block, the memory cannot be released until the last weak_ptr is also gone. An object with a large body and long-lived weak_ptrs watching it will hold its bytes longer than necessary. That is a genuine but narrow reason to prefer the new form.

Reason 3: exception safety

This is the one that actually matters, and it is not obvious.

void process(std::unique_ptr<Widget> w, int value);
int risky();

// Potentially leaks:
process(std::unique_ptr<Widget>(new Widget("a", 1)), risky());

The compiler may evaluate the arguments in any order (lesson 8.7 called this unspecified). One legal order is:

  1. new Widget("a", 1) — allocates
  2. risky() — throws
  3. std::unique_ptr<Widget>(...) — never runs

The Widget was allocated and never wrapped, so nothing will ever free it. A leak from code that looks careful.

// Cannot leak:
process(std::make_unique<Widget>("a", 1), risky());

make_unique allocates and wraps inside a single function call, so there is no window between the two. Whatever order the arguments are evaluated in, the pointer is owned the moment it exists.

C++17 tightened the evaluation rules so each argument is now fully evaluated before the next begins, which closes this particular hole. The habit is still right: the factory form has no window at all, and does not depend on your readers knowing which standard you compile with.

When you cannot use them

A custom deleter. make_unique and make_shared always use delete:

std::unique_ptr<Handle, decltype(&close_handle)> h(open_handle(7), &close_handle);

A private constructor. The factories call the constructor from inside the standard library, which is not a friend of your class.

new T[n]. std::make_unique<T[]>(n) exists and is fine; make_shared for arrays only arrived in C++20.

You need new T() versus new T. make_unique<T>() value-initialises; there are corner cases where you specifically want the uninitialised form for performance.

Outside those, use the factories.

make_unique was late

A historical footnote worth knowing when reading older code: make_shared arrived in C++11 but make_unique was forgotten and only added in C++14. Code written in between uses std::unique_ptr<T>(new T(...)) — not because the author was careless, but because the factory did not exist.

Try it

Write a class whose constructor takes three arguments and print when it is constructed and destroyed. Create it with make_unique, with make_shared, and with the new forms. Then use the allocation counter to compare shared_ptr(new T) against make_shared<T>().

Checkpoint

  • make_unique<T>(args...) and make_shared<T>(args...) construct and wrap in one step.
  • The type is named once, so it cannot drift out of sync.
  • make_shared performs one allocation instead of two, keeping object and counter together.
  • Its trade-off: the memory is held until the last weak_ptr is gone too.
  • The factories close the exception-safety window between allocating and wrapping.
  • Use new only for a custom deleter, a private constructor, or the array corner cases.
  • make_unique arrived in C++14, which is why older code uses the new form.

Questions about this lesson

Log in to ask a question.