За четыре года бэкенд-разработки на C++ я видел больше багов от неправильного использования умных указателей, чем от любой другой особенности языка. И дело не в том, что указатели сложные — дело в том, что большинство туториалов объясняют как они работают, а не когда и зачем их использовать. Попробую исправить это.
unique_ptr — ваш дефолт по умолчанию
Правило простое: если объекту нужен динамический lifetime и у него ровно один владелец — используйте std::unique_ptr. Это самый лёгкий умный указатель, его размер равен размеру сырого указателя, и он имеет нулевые накладные расходы по сравнению с new/delete.
#include
#include
struct Session {
int id;
std::string token;
};
// Фабрика — возвращает unique_ptr, ownership передаётся вызывающему
std::unique_ptr create_session(int user_id) {
auto session = std::make_unique();
session->id = user_id;
session->token = generate_token();
return session; // NRVO + move semantics
}
// Использование
auto sess = create_session(42);
// sess владеет объектом, при выходе из scope — деструктор вызовется автоматически
Обратите внимание на std::make_unique — всегда используйте его вместо std::unique_ptr. Это безопаснее в выражениях с неопределённым порядком вычисления аргументов, и компилятору проще оптимизировать.
shared_ptr — удобный, но опасный
std::shared_ptr использует reference counting, и это добавляет накладные расходы: каждый shared_ptr хранит указатель на control block с двумя счётчиками (strong и weak). Копирование стоит дороже, а атомарные операции на счётчике могут создавать contention в многопоточной среде.
В нашем проекте был реальный баг: кэш пользователей хранил shared_ptr, и каждый обработчик запроса тоже держал shared_ptr. Через три месяца работы под нагрузкой мы заметили, что объекты User никогда не освобождались, потому что один из обработчиков утечка ссылку в замыкании callback'а. Профайлер показывал, что 40% памяти съедают «живые» User'ы, которые уже не нужны.
// ПЛОХО: shared_ptr всюду, где можно обойтись ссылкой
void process_request(std::shared_ptr user) {
// Каждый вызов увеличивает refcount
cache.store(user); // +1
logger.log(user); // +1
}
// ХОРОШО: принимаем ссылку, shared_ptr только в точке ownership
class UserCache {
std::unordered_map> users_;
public:
std::shared_ptr get(int id) const {
auto it = users_.find(id);
return it != users_.end() ? it->second : nullptr;
}
};
void process_request(const User& user) {
// Нет дополнительного refcount
logger.log(user);
}
weak_ptr — ломаем циклические ссылки
Классическая проблема: объект A держит shared_ptr на B, а B держит shared_ptr на A. Цикл never breaks, и оба объекта утечают. std::weak_ptr решает это, потому что он не увеличивает strong reference count.
struct Node {
std::string data;
std::shared_ptr next;
std::weak_ptr prev; // weak_ptr разрывает цикл
};
auto a = std::make_shared();
a->data = "first";
auto b = std::make_shared();
b->data = "second";
a->next = b;
b->prev = a; // weak_ptr, не создаёт цикл
// Когда a и b выходят из scope, оба корректно освобождаются
Кастомные делетеры
Одна из недооценённых возможностей unique_ptr — кастомный deleter. Мы используем это для ресурсов, которые нельзя просто deleteить: FILE*, sqlite3*, сокеты.
struct FileCloser {
void operator()(FILE* f) const {
if (f) fclose(f);
}
};
using UniqueFile = std::unique_ptr;
UniqueFile open_file(const std::string& path) {
FILE* f = fopen(path.c_str(), "r");
if (!f) throw std::runtime_error("Cannot open: " + path);
return UniqueFile(f);
}
// Использование — файл закроется автоматически
auto f = open_file("/etc/config.yaml");
char buf[256];
while (fgets(buf, sizeof(buf), f.get())) {
process_line(buf);
}
Сырые указатели для невладеющих ссылок
Есть правило, которому я следую: если функция не владеет объектом, она принимает сырой указатель или ссылку. shared_ptr в аргументах функции — это почти всегда красный флаг, означающий «я не понял, кто тут владелец».
В Kotlin всё проще: есть T (не null) и T? (nullable). Система типов явно показывает, может ли ссылка быть null. В C++ сырой указатель может быть null, и компилятор вас не спасёт. Поэтому в нашем проекте мы ввели конвенцию: если функция принимает невладеющий указатель, она обязана проверять его на null, и мы используем статический анализатор для проверки этого правила.
Move-семантика и умные указатели
unique_ptr некопируем, но перемещаем. Это означает, что ownership можно передать, но нельзя разделить. Когда я вижу std::move(ptr) в код ревью, я всегда проверяю: после move оригинальный указатель становится null, и любой доступ к нему — UB. Мы однажды потратили два дня на отладку краша, который произошёл именно потому, что после std::move кто-то обратился к оригинальному указателю.
Итог: unique_ptr — ваш друг по умолчанию, shared_ptr — когда реально нужно разделённое владение (и вы уверены, что нет циклов), weak_ptr — для наблюдения без владения, сырые указатели и ссылки — для невладеющих аргументов. Просто, как и должно быть.
Пока нет комментариев
Будьте первым, кто оставит комментарий к этой статье