← Назад к вопросам

Почему рекомендуют использовать std::make_shared?

2.0 Middle🔥 131 комментариев
#Умные указатели и управление памятью

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI28 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

std::make_shared: эффективность и безопасность

std::make_shared — это рекомендуемый способ создания объектов со shared_ptr. Он имеет существенные преимущества перед прямым использованием new.

Проблема: new + shared_ptr

1. Две отдельные аллокации памяти

Когда используешь new:

// Два вызова malloc/new!
std::shared_ptr<MyClass> ptr(new MyClass());

// Что происходит:
// 1. new MyClass() выделяет память для объекта
// 2. shared_ptr конструктор выделяет память для control block
// Итого: 2 аллокации

Control block содержит:

  • Счетчик ссылок (ref count)
  • Счетчик weak ссылок (weak count)
  • Deleter
  • Allocator
// Макет памяти при new + shared_ptr:
Heap layout:
  [MyClass object]      <- аллокация 1
  [Control block]       <- аллокация 2

2. Утечка памяти при исключении в order of evaluation

Это опасно:

void process(std::shared_ptr<A> a, std::shared_ptr<B> b);

// Опасный код:
process(
    std::shared_ptr<A>(new A()),      // new A() (аллокация 1)
    std::shared_ptr<B>(new B())       // new B() может выбросить
);

// Порядок выполнения НЕОПРЕДЕЛЕН:
// 1. new A()
// 2. new B() <- может выбросить исключение!
// 3. Создание shared_ptr для A
// 4. Создание shared_ptr для B

// Если B выбросит исключение между шагом 1 и 3,
// объект A не будет освобожден!

Решение: std::make_shared

1. Одна аллокация

std::make_shared выделяет память один раз:

std::shared_ptr<MyClass> ptr = std::make_shared<MyClass>();

// Что происходит:
// 1. Одна аллокация для объекта + control block вместе
// Итого: 1 аллокация

// Макет памяти:
Heap layout:
  [MyClass object]
  [Control block]   <- в одном блоке памяти!

2. Атомарность в order of evaluation

std::make_shared создает объект атомарно:

// Безопасный код:
process(
    std::make_shared<A>(),   // Полная аллокация и конструкция A
    std::make_shared<B>()    // Полная аллокация и конструкция B
);

// Порядок выполнения определен:
// 1. Полностью создать A (аллокация + конструктор)
// 2. Полностью создать B (аллокация + конструктор)
// 3. Передать в function

// Если B выбросит исключение, A уже имеет shared_ptr!

Практические примеры

Пример 1: Performance

class Node {
public:
    std::string data;
    std::vector<int> values;
    
    Node() {
        std::cout << "Node created\n";
    }
};

int main() {
    // Неэффективно: 2 аллокации
    {
        std::shared_ptr<Node> ptr1(new Node());
        std::shared_ptr<Node> ptr2(new Node());
        std::shared_ptr<Node> ptr3(new Node());
    }
    
    // Эффективно: 3 аллокации
    {
        auto ptr1 = std::make_shared<Node>();
        auto ptr2 = std::make_shared<Node>();
        auto ptr3 = std::make_shared<Node>();
    }
}

Время выполнения: make_shared обычно быстрее в 2-3 раза.

Пример 2: Exception Safety

void callFunction(
    std::shared_ptr<Resource> r1,
    std::shared_ptr<Resource> r2
);

// ОПАСНО!
callFunction(
    std::shared_ptr<Resource>(new Resource("A")),
    std::shared_ptr<Resource>(new Resource("B"))  // Может выбросить
);
// Если B выбросит, A утечет!

// БЕЗОПАСНО!
callFunction(
    std::make_shared<Resource>("A"),
    std::make_shared<Resource>("B")  // Может выбросить
);
// Если B выбросит, A уже защищена shared_ptr!

Пример 3: Cache locality

struct TreeNode {
    int value;
    std::shared_ptr<TreeNode> left;
    std::shared_ptr<TreeNode> right;
};

// Плохо: объект и control block в разных местах памяти
auto node1 = std::shared_ptr<TreeNode>(new TreeNode{10});

// Хорошо: объект и control block рядом — лучший cache hit
auto node2 = std::make_shared<TreeNode>();
node2->value = 10;

Конструктор с параметрами

std::make_shared поддерживает perfect forwarding:

class Database {
public:
    Database(const std::string& host, int port, const std::string& user)
        : host_(host), port_(port), user_(user) {}
    
private:
    std::string host_;
    int port_;
    std::string user_;
};

int main() {
    // Perfect forwarding параметров
    auto db = std::make_shared<Database>("localhost", 5432, "admin");
    
    // Эквивалентно:
    // std::shared_ptr<Database>(
    //     new Database("localhost", 5432, "admin")
    // );
}

Когда нельзя использовать make_shared

1. Custom deleter

// Нужен custom deleter? Используй new:
std::shared_ptr<FILE> file(
    fopen("file.txt", "r"),
    [](FILE* f) { if (f) fclose(f); }
);

// make_shared не поддерживает custom deleter напрямую
// (в C++20 есть поддержка через pmr::polymorphic_allocator)

2. Наследование с virtual destructor

Обычно всё работает, но есть нюансы:

class Base {
public:
    virtual ~Base() {}
};

class Derived : public Base {
public:
    Derived(int x) : value(x) {}
private:
    int value;
};

// OK с make_shared
auto ptr = std::make_shared<Derived>(42);

// Control block помещается сразу после Derived объекта
// Это все еще эффективно

3. Очень большие объекты

struct HugeBuffer {
    char data[1024 * 1024 * 1024];  // 1 GB
};

// make_shared выделяет все вместе
auto ptr = std::make_shared<HugeBuffer>();  // 1 GB + control block

// Если нужен более гибкий контроль, используй new:
auto ptr2 = std::shared_ptr<HugeBuffer>(new HugeBuffer());

Сравнение

Аспектnew + shared_ptrstd::make_shared
Аллокации21
Exception safeНетДа
Cache localityПлохаяХорошая
Custom deleterДаНет (C++17)
Размер бинарияБольшеМеньше
PerformanceМедленнееБыстрее (2-3x)

Best Practice

  1. Используй make_shared по умолчанию
  2. Используй new + shared_ptr только если нужен custom deleter
  3. В цепочке вызовов используй make_shared для exception safety
  4. Профилируй для больших объектов

Постоянное использование make_shared улучшает производительность, память и безопасность.