Ручное управление памятью – это дисциплина, а дисциплину нарушают. Ранний 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, розданный «на всякий случай», делает время жизни объекта непредсказуемым: понять, кто освободит его последним, становится нельзя.