Программа курса← Программирование на языке C++
1Введение в язык C++
2Как выполняются программы на C++
3Структуры и классы
4Объектно-ориентированное программирование
5Дополнительные возможности C++
6Шаблоны
7Методические материалы

Умные указатели

Ручное управление памятью – это дисциплина, а дисциплину нарушают. Ранний return, исключение, две ветви, из которых в одной забыли delete. Ответ C++ – переложить работу на деструктор, а готовые обёртки для этого лежат в стандартной библиотеке.

Идея

Указатель кладут в объект на стеке, а в деструкторе этого объекта вызывают delete. Деструктор отработает на любом выходе из блока – значит, освобождение забыть невозможно.

class ScopedPtr {
public:
    explicit ScopedPtr(Expression* ptr = nullptr) : ptr_(ptr) {}

    ~ScopedPtr() { delete ptr_; }

    ScopedPtr(const ScopedPtr&) = delete;             // владелец только один
    ScopedPtr& operator=(const ScopedPtr&) = delete;

    Expression* Get() const { return ptr_; }

    Expression* Release() {          // отдать владение наружу
        Expression* old = ptr_;
        ptr_ = nullptr;
        return old;
    }

    void Reset(Expression* ptr = nullptr) {   // сменить объект
        delete ptr_;
        ptr_ = ptr;
    }

    Expression& operator*() const  { return *ptr_; }
    Expression* operator->() const { return ptr_; }

private:
    Expression* ptr_;
};

Использование неотличимо от обычного указателя, а delete в коде нет:

void Process() {
    ScopedPtr expr(new BinaryOperation(new Number(1), '+', new Number(2)));

    if (Failed()) {
        return;                  // объект освобождён
    }

    std::cout << expr->Evaluate();
}                                // и здесь тоже

Запрет копирования – не ограничение, а сам смысл класса. Разрешив его, вы получите двух владельцев одного объекта и двойное освобождение. До C++11 запрет записывали объявлением этих методов в private без реализации – работало, но ошибка вылезала на этапе компоновки; = delete даёт внятное сообщение сразу.

Счётчик ссылок

Единоличное владение подходит не всегда: бывает, что на объект ссылаются из нескольких мест и заранее неизвестно, кто освободит его последним. Тогда владельцев считают.

class SharedPtr {
public:
    explicit SharedPtr(Expression* ptr = nullptr)
        : ptr_(ptr)
        , count_(ptr ? new size_t(1) : nullptr)
    {}

    SharedPtr(const SharedPtr& other)
        : ptr_(other.ptr_)
        , count_(other.count_)
    {
        if (count_) {
            ++*count_;
        }
    }

    ~SharedPtr() { Decrement(); }

    SharedPtr& operator=(const SharedPtr& other) {
        if (this != &other) {
            Decrement();
            ptr_ = other.ptr_;
            count_ = other.count_;
            if (count_) {
                ++*count_;
            }
        }
        return *this;
    }

private:
    void Decrement() {
        if (count_ && --*count_ == 0) {
            delete ptr_;
            delete count_;
        }
    }

    Expression* ptr_;
    size_t* count_;
};

Счётчик обязательно живёт в куче, а не в самом объекте-обёртке: он общий для всех копий.

Граничные случаи, на которых такой класс обычно и ломается: нулевой указатель (счётчика нет), самоприсваивание (уменьшить, а потом увеличить тот же счётчик – объект успеет умереть), Reset на пустом указателе.

Готовые из стандартной библиотеки

Писать это руками не нужно – всё есть в <memory>.

#include <memory>

std::unique_ptr<Expression> a = std::make_unique<Number>(42);   // C++14
std::shared_ptr<Expression> b = std::make_shared<Number>(42);
unique_ptr shared_ptr
Владельцев один сколько угодно
Размер как у обычного указателя два указателя
Копирование запрещено, только std::move разрешено, увеличивает счётчик
Стоимость нулевая атомарные операции со счётчиком
Когда брать по умолчанию когда владельцев действительно несколько
std::unique_ptr<Expression> Make() {
    return std::make_unique<Number>(1);      // возврат владения
}

void Take(std::unique_ptr<Expression> e);    // забирает себе

auto e = Make();
Take(std::move(e));      // e после этого пуст

std::move не перемещает данные – он лишь позволяет забрать внутренности объекта. Для unique_ptr это перенос сырого указателя и обнуление источника.

Пара make_unique / make_shared предпочтительнее прямого new по двум причинам. Первая – make_shared выделяет объект и счётчик одним блоком, а не двумя. Вторая историческая: в выражении Foo(std::unique_ptr<A>(new A), Bar()) порядок вычисления аргументов до C++17 не был определён, и исключение из Bar() между new и конструктором обёртки давало утечку.

Циклические ссылки

У счётчика ссылок есть принципиальный изъян: два объекта, ссылающиеся друг на друга через shared_ptr, никогда не обнулят свои счётчики.

struct Node {
    std::shared_ptr<Node> next;
    std::shared_ptr<Node> prev;    // цикл: ни один узел не будет удалён
};

Лечится std::weak_ptr – ссылкой, которая не владеет и счётчик не увеличивает:

struct Node {
    std::shared_ptr<Node> next;
    std::weak_ptr<Node> prev;      // обратная ссылка не удерживает объект

    void UseParent() {
        if (auto p = prev.lock()) {   // проверяем, жив ли объект, и получаем shared_ptr
            // ...
        }
    }
};

Правило: владеющая связь – shared_ptr, обратная или наблюдающая – weak_ptr.

Не только память

Идиома шире умных указателей. Всё, что нужно вернуть, оборачивают в объект со стека:

{
    std::lock_guard<std::mutex> lock(mutex_);   // отпустит на выходе
    std::ifstream file("data.txt");             // закроет на выходе
    // ...
}

std::unique_ptr умеет и произвольное освобождение – через свой удалитель:

std::unique_ptr<FILE, decltype(&std::fclose)> file(std::fopen("data.txt", "r"), &std::fclose);

Умный указатель нужен там, где объект обязан жить в куче: полиморфная иерархия, фабрика, время жизни за пределами функции. Локальный объект по-прежнему держите на стеке, массив – в std::vector. std::shared_ptr, розданный «на всякий случай», делает время жизни объекта непредсказуемым: понять, кто освободит его последним, становится нельзя.