Когда не стоит использовать std::make_shared?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Когда не стоит использовать std::make_shared?
std::make_shared — это удобный способ создания std::shared_ptr, но он имеет серьёзные ограничения и потенциальные проблемы. Для опытного backend разработчика критично знать эти случаи.
Проблема 1: Приватный конструктор
std::make_shared требует доступа к конструктору объекта. Если конструктор приватный, использовать make_shared нельзя:
class Database {
private:
Database(const std::string& connection_string) { }
friend class DatabaseFactory;
public:
static std::shared_ptr<Database> create(const std::string& conn_str) {
// ❌ ОШИБКА КОМПИЛЯЦИИ!
// return std::make_shared<Database>(conn_str);
// ✅ Правильно:
return std::shared_ptr<Database>(new Database(conn_str));
}
};
Почему это проблема? Приватные конструкторы используются для контроля создания объектов (паттерны Factory, Singleton).
Проблема 2: Кастомные Deleter'ы
std::make_shared создаёт дефолтный deleter (вызов delete). Если нужен custom deleter, нельзя использовать make_shared:
#include <memory>
#include <cstdio>
class FileHandle {
public:
FileHandle(const char* filename) : file_(fopen(filename, "r")) { }
~FileHandle() { if (file_) fclose(file_); }
private:
FILE* file_;
};
int main() {
// Для FILE* нужен custom deleter
FILE* file = fopen("data.txt", "r");
// ❌ Нельзя использовать make_shared для FILE*
// std::shared_ptr<FILE> file_ptr = std::make_shared<FILE>();
// ✅ Нужен custom deleter:
std::shared_ptr<FILE> file_ptr(
file,
[](FILE* f) { if (f) fclose(f); } // Lambda deleter
);
return 0;
}
Другие примеры с custom deleter:
// Postgres connection
PGconn* conn = PQconnectdb("dbname=mydb");
std::shared_ptr<PGconn> pg_conn(
conn,
[](PGconn* c) { PQfinish(c); } // Используем PQfinish вместо delete
);
// OpenSSL EVP_CIPHER_CTX
EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new();
std::shared_ptr<EVP_CIPHER_CTX> ssl_ctx(
ctx,
[](EVP_CIPHER_CTX* c) { EVP_CIPHER_CTX_free(c); }
);
Проблема 3: Контроль выравнивания памяти
std::make_shared может выделить память неоптимально для объектов требующих специального выравнивания:
// SIMD объект требует 32-byte выравнивания
aligned struct Vector {
alignas(32) float data[8];
};
int main() {
// make_shared может не обеспечить нужное выравнивание
auto vec = std::make_shared<Vector>(); // Потенциально неправильно выравнен
// Лучше:
auto vec_ptr = std::shared_ptr<Vector>(new Vector()); // Гарантирует выравнивание
// Или (C++17):
auto aligned_vec = std::make_shared<Vector>(); // В C++17 это исправлено
return 0;
}
Проблема 4: Контроль выделения памяти
std::make_shared выделяет память из heap, но иногда нужен контроль:
// Высоконагруженный сервер — нужна контролируемая аллокация
class RateLimiter {
private:
// Custom allocator для mempool
boost::pool<> memory_pool{sizeof(RequestContext)};
public:
std::shared_ptr<RequestContext> create_context() {
// ❌ make_shared не поддерживает custom allocator
// auto ctx = std::make_shared<RequestContext>();
// ✅ Нужно использовать shared_ptr с custom allocator (C++20)
// Или явно:
void* mem = memory_pool.malloc();
return std::shared_ptr<RequestContext>(
new (mem) RequestContext(), // Placement new
[this](RequestContext* p) {
p->~RequestContext();
memory_pool.free(p);
}
);
}
};
Проблема 5: Временные объекты и исключения
std::make_shared может привести к утечкам при исключениях в некоторых случаях:
// Вызов функции с аргументами shared_ptr
void process(std::shared_ptr<Data> data, std::shared_ptr<Handler> handler) { }
int main() {
// ✅ БЕЗОПАСНО (C++17+):
process(
std::make_shared<Data>(),
std::make_shared<Handler>() // Если здесь исключение - утечки нет
);
// ❌ ОПАСНО (C++11-14):
// Порядок вычисления аргументов не определён!
// 1. make_shared<Data>()
// 2. make_shared<Handler>()
// 3. Exception!
// → утечка Data если исключение при create Handler
return 0;
}
Проблема 6: Невозможность объединить с make_unique (и обратно)
Вы не можете легко переходить между shared_ptr и unique_ptr:
class Connection {
public:
void connect() { /* ... */ }
};
int main() {
// Если функция требует ownership (unique_ptr)
auto transfer_ownership(std::unique_ptr<Connection>) { }
// Вы не можете использовать make_shared для этого
// auto conn = std::make_shared<Connection>();
// transfer_ownership(std::move(conn)); // ❌ Нельзя! shared_ptr → unique_ptr
// Должно быть:
auto conn = std::make_unique<Connection>();
transfer_ownership(std::move(conn)); // ✅ OK
return 0;
}
Проблема 7: Reference cycles и утечки памяти
std::make_shared может скрывать проблемы с циклическими ссылками:
class Node {
public:
std::shared_ptr<Node> parent;
std::vector<std::shared_ptr<Node>> children;
};
int main() {
auto node1 = std::make_shared<Node>();
auto node2 = std::make_shared<Node>();
// Циклическая ссылка!
node1->children.push_back(node2);
node2->parent = node1; // node1 и node2 никогда не удалятся (утечка)
// Решение: использовать weak_ptr
// class Node {
// public:
// std::weak_ptr<Node> parent; // Слабая ссылка
// std::vector<std::shared_ptr<Node>> children;
// };
return 0;
}
Проблема 8: Отсроченное удаление в многопоточной среде
std::make_shared объединяет управление объектом и счётчик ссылок в одной аллокации. Это может привести к задержкам удаления при многопоточности:
class HeavyObject {
public:
~HeavyObject() {
// Долгие операции очистки (~1 сек)
std::this_thread::sleep_for(std::chrono::seconds(1));
}
};
int main() {
std::vector<std::shared_ptr<HeavyObject>> objects;
for (int i = 0; i < 1000; ++i) {
objects.push_back(std::make_shared<HeavyObject>());
}
// Когда вектор очищается, все деструкторы выполняются последовательно
// ~1000 секунд ждём! (блокирует вызывающий поток)
objects.clear();
// Лучший подход для высоконагруженных систем:
// Удалять асинхронно в отдельном потоке
return 0;
}
Рекомендации для backend разработчика
Используйте std::make_shared когда:
- Простое создание объекта с дефолтным deleter
- Нет требований к custom allocator'у
- Нет циклических ссылок (используйте weak_ptr)
- C++17 или выше (лучше гарантии)
Используйте прямой shared_ptr<T>(new T(...)) когда:
- Нужен custom deleter
- Приватный конструктор
- Специальные требования к выравниванию
- Custom allocator
- Контроль над timing удаления в многопоточной среде
Пример правильного паттерна:
class Server {
private:
explicit Server(int port) : port_(port) { }
friend class ServerBuilder;
public:
static ServerBuilder builder() { return ServerBuilder(); }
};
class ServerBuilder {
public:
std::shared_ptr<Server> build() {
return std::shared_ptr<Server>(new Server(port_));
}
private:
int port_ = 8080;
};
int main() {
auto server = ServerBuilder().build();
return 0;
}
Понимание этих тонкостей критично для написания надёжного backend кода на C++.