Владение — это концепция; умные указатели — инструмент

До unique_ptr и shared_ptr владение было неявным — соглашение, записанное в комментариях и поддерживаемое только дисциплиной. «Кто вызывает create(), должен вызвать destroy()». Это работало до тех пор, пока не переставало: исключения, ранние возвраты и множество путей выхода приводили к утечкам.

Умные указатели делают владение явным и проверяемым системой типов. unique_ptr<T> не просто хранит указатель — он говорит: «Я единственный владелец этого объекта. Когда я выхожу из области видимости, объект уничтожается».

Понимание умных указателей означает понимание трёх моделей владения, которые они кодируют:

Тип Модель владения
unique_ptr<T> Единственный владелец — один указатель, одно время жизни
shared_ptr<T> Разделённое владение — подсчёт ссылок, уничтожается при исчезновении последнего владельца
weak_ptr<T> Невладеющий наблюдатель — не удерживает объект живым

unique_ptr — выбор по умолчанию

unique_ptr — RAII-обёртка с нулевыми накладными расходами. На большинстве компиляторов компилируется в тот же машинный код, что и сырой указатель с ручным delete.

1// Создание
2auto sensor = std::make_unique<Bmp280Sensor>(hi2c1);
3
4// Автоматическое уничтожение при выходе из области видимости
5// delete не нужен — даже при исключении

make_unique предпочтительнее new — он безопасен относительно исключений и предотвращает ситуацию, когда указатель выделен, но ещё не принадлежит никому.

Передача владения

unique_ptr не копируется. Владение передаётся через std::move:

 1std::unique_ptr<ISensor> createSensor(SensorType type) {
 2    switch (type) {
 3        case SensorType::BMP280: return std::make_unique<Bmp280Sensor>(hi2c1);
 4        case SensorType::NTC:    return std::make_unique<NtcSensor>(ADC_CHANNEL_3);
 5    }
 6}
 7
 8// Передача вызывающему — фабрика больше не владеет объектом
 9auto sensor = createSensor(SensorType::BMP280);
10
11// Передача другому unique_ptr
12auto other = std::move(sensor);
13// sensor теперь nullptr; other владеет объектом

Фабричные функции, возвращающие unique_ptr — идиоматический способ передачи динамически созданных объектов без раскрытия new.

Передача в функции

1// Передаёт владение — вызываемый становится владельцем
2void take(std::unique_ptr<ISensor> sensor);
3
4// Заимствует — вызываемый использует объект, но не владеет им
5void use(ISensor& sensor);        // предпочтительно
6void use(ISensor* sensor);        // только если nullptr — допустимый аргумент
7void use(const ISensor& sensor);  // заимствование только для чтения

Передавайте по сырой ссылке или указателю, когда просто используете объект, не владея им. Передача unique_ptr по значению передаёт владение функции — вызывающий теряет объект. Это редко нужно в конвейере, который хранит сенсор.


shared_ptr — разделённое владение со стоимостью

shared_ptr поддерживает счётчик ссылок. Каждая копия увеличивает счётчик; каждое уничтожение уменьшает. Когда счётчик достигает нуля, объект уничтожается.

1auto sensor = std::make_shared<Bmp280Sensor>(hi2c1);
2auto copy   = sensor;  // счётчик = 2
3
4// Оба — sensor и copy — могут использовать объект
5// Уничтожается, когда оба выходят из области видимости

Стоимость

shared_ptr не является нулевой стоимостью:

  • Два выделения на куче по умолчанию (объект + блок управления), сокращается до одного с make_shared
  • Две атомарные операции (инкремент + декремент) при копировании и уничтожении — дорого на многоядерных
  • Больше, чем unique_ptr — обычно 16 байт (указатель + указатель блока управления)

На встраиваемом Cortex-M без ОС атомарные операции всё равно добавляют накладные расходы и требуют отключения прерываний или LDREX/STREX.

Когда shared_ptr — правильный выбор

Используйте shared_ptr, когда несколько несвязанных частей системы должны удерживать объект живым и нельзя определить единственного владельца:

1// Конфигурация загружается один раз, совместно используется между этапами конвейера
2auto config = std::make_shared<SensorConfig>(loadConfig());
3
4SensorPipeline  pipeline(config);
5DiagnosticsUnit diag(config);
6Logger          logger(config);
7
8// Config живёт до уничтожения всех трёх — порядок не важен

Если можно определить единственного владельца — используйте unique_ptr. shared_ptr — не «безопасный вариант по умолчанию», а явный выбор разделённого владения с накладными расходами времени выполнения.


weak_ptr — наблюдение без владения

weak_ptr хранит невладеющую ссылку на объект, управляемый shared_ptr. Он не предотвращает уничтожение. Чтобы использовать объект, нужно вызвать lock() — он возвращает shared_ptr, если объект ещё существует, или пустой shared_ptr, если уже уничтожен.

1std::shared_ptr<Display> display = std::make_shared<TftDisplay>(hspi1);
2std::weak_ptr<Display>   observer = display;
3
4// Позже, возможно из callback:
5if (auto d = observer.lock()) {
6    d->refresh();  // безопасно — объект ещё жив
7} else {
8    // display был уничтожен — пропустить
9}

Главный случай использования: разрыв циклов

Циклы shared_ptr вызывают утечки памяти — A хранит shared_ptr на B, B хранит на A, ни один не достигает нулевого счётчика.

 1// Цикл — оба утекают
 2struct Node {
 3    std::shared_ptr<Node> next;  // ПЛОХО при циклической ссылке
 4};
 5
 6// Исправление: одно направление — weak
 7struct Node {
 8    std::shared_ptr<Node> next;
 9    std::weak_ptr<Node>   prev;  // наблюдатель, не владелец
10};

Кастомные удалители

unique_ptr и shared_ptr принимают кастомные удалители — полезно для ресурсов, не являющихся объектами на куче:

 1// Дескриптор HAL, который нужно освободить, а не удалить
 2auto timer = std::unique_ptr<TIM_HandleTypeDef, decltype(&HAL_TIM_Base_DeInit)>(
 3    &htim1, HAL_TIM_Base_DeInit
 4);
 5// HAL_TIM_Base_DeInit вызывается автоматически при выходе из области видимости
 6
 7// Файловый дескриптор
 8auto fd = std::unique_ptr<FILE, decltype(&fclose)>(
 9    fopen("log.bin", "wb"), fclose
10);

Для встраиваемых платформ этот паттерн управляет дескрипторами HAL, пинами CS SPI и другими ресурсами с парными функциями инициализации/деинициализации.


Сырые указатели — когда они корректны

Сырые указатели не являются неправильными. Они неправильны как владельцы. Как невладеющие наблюдатели они полностью идиоматичны:

1class SensorPipeline {
2    ISensor* sensor_;  // сырой указатель — конвейер НЕ владеет объектом
3public:
4    // Принадлежит в другом месте (стек, unique_ptr в корне композиции)
5    explicit SensorPipeline(ISensor* sensor) : sensor_(sensor) {
6        assert(sensor != nullptr);
7    }
8};

Владение ясно из точки вызова:

1// main.cpp
2Bmp280Sensor sensor(hi2c1);            // принадлежит стеку
3SensorPipeline pipeline(&sensor);      // конвейер заимствует указатель
4// Оба уничтожаются при выходе из main — утечка невозможна

Это доминирующий паттерн во встраиваемой прошивке, где динамическое выделение сведено к минимуму или запрещено. Все объекты живут на стеке или в статической памяти; указатели передают невладеющие ссылки.


Владение во встраиваемой прошивке

Большинство встраиваемых кодовых баз полностью избегают динамического выделения:

Правило: никакого new/delete за пределами инициализации

В этой модели:

  • Объекты живут на стеке или как static в области функции или файла
  • unique_ptr используется для фабричных функций при необходимости выделения
  • shared_ptr избегается — атомарный подсчёт ссылок требует времени отключения прерываний
  • Сырые указатели передают невладеющие ссылки
  • Ссылки (&) предпочтительнее сырых указателей, когда nullptr не является допустимым значением
1// Типичный корень композиции для встраиваемых
2static Bmp280Sensor   sensor(hi2c1);   // статический — живёт вечно
3static MovingAvgFilter filter(8);
4static TftDisplay      display(hspi1);
5
6static SensorPipeline pipeline(&sensor, &filter, &display);

Статические объекты в области файла инициализируются нулями до main() и уничтожаются после него — гарантия стандарта C++.


Краткий справочник

Нужно единственное владение, динамическое выделение?   unique_ptr
Нужно разделённое владение (несколько владельцев)?    shared_ptr
Нужно наблюдать за shared_ptr без владения?           weak_ptr
Объект на стеке / в статической памяти?               сырая ссылка или сырой указатель
Передать функции, которая использует (не владеет)?    T& или const T&
Передать функции, которая берёт владение?             unique_ptr<T> по значению
Вернуть новый созданный объект?                       unique_ptr<T>

Что дальше