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

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

1.8 Middle🔥 121 комментариев
#Язык C++#Умные указатели и управление памятью

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

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

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

Когда не стоит использовать 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++.