Загрузка
Все статьи

C++: умные указатели и ownership — практическое руководство без академизма

Разбираю unique_ptr, shared_ptr и weak_ptr на реальных примерах из продакшена. Какие баги мы ловили из-за неправильного использования и как перестать путаться с ownership.

За четыре года бэкенд-разработки на 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(new T(...)). Это безопаснее в выражениях с неопределённым порядком вычисления аргументов, и компилятору проще оптимизировать.

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 — для наблюдения без владения, сырые указатели и ссылки — для невладеющих аргументов. Просто, как и должно быть.

Поделиться статьёй

Комментарии

0
0 / 2000

Пока нет комментариев

Будьте первым, кто оставит комментарий к этой статье

Читайте также

CMake: модульная архитектура для большого C++ проекта — без ad-hoc include путей

Как мы реорганизовали CMake-структуру проекта на 15+ модулей, избавились от хаотичных include_directories и перешли на современный CMake с target-based зависимостями.

20 июня 2026 10 мин

Kotlin + C++: реальный кейс — видеопроцессинг на сервере через JNI

Как мы построили сервер видеотранскодинга на Kotlin REST API + C++ FFmpeg через JNI. Прокачка JNI_OnLoad, передача фреймов через DirectByteBuffer, обработка ошибок и бенчмарки.

19 июня 2026 13 мин

C++: потоки и потокобезопасность — мьютексы, atomics и deadlock-ы на практике

Разбираем многопоточность в C++ на реальных примерах из продакшена: мьютексы, lock_guard, unique_lock, std::atomic, deadlock-ы и как их избежать. Сравнение с корутинами Kotlin.

18 июня 2026 11 мин