← Назад к вопросам
Почему рекомендуют использовать 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_ptr | std::make_shared |
|---|---|---|
| Аллокации | 2 | 1 |
| Exception safe | Нет | Да |
| Cache locality | Плохая | Хорошая |
| Custom deleter | Да | Нет (C++17) |
| Размер бинария | Больше | Меньше |
| Performance | Медленнее | Быстрее (2-3x) |
Best Practice
- Используй make_shared по умолчанию
- Используй new + shared_ptr только если нужен custom deleter
- В цепочке вызовов используй make_shared для exception safety
- Профилируй для больших объектов
Постоянное использование make_shared улучшает производительность, память и безопасность.