умные указателиsmart pointers
Единоличное владение динамически выделенным объектом с автоматическим освобождением при выходе из скоупа. Move-only — копирование запрещено на уровне типа, передача владения возможна только через move.Sole ownership of a dynamically allocated object with automatic release on scope exit. Move-only — copying is disallowed at the type level, ownership can only be transferred via move.
std::unique_ptr<T> — RAII-обёртка над указателем, которая владеет объектом единолично: при уничтожении unique_ptr (выход из скоупа, исключение, явный reset) автоматически вызывается delete (или пользовательский deleter) над управляемым объектом. Copy-конструктор и copy-присваивание явно удалены (= delete) — существует только move-конструктор и move-присваивание.std::unique_ptr<T> is a RAII wrapper around a pointer that owns its object exclusively: when the unique_ptr is destroyed (scope exit, an exception, an explicit reset), delete (or a custom deleter) is automatically invoked on the managed object. The copy constructor and copy assignment operator are explicitly deleted (= delete) — only a move constructor and move assignment operator exist.
Widget* w = new Widget();
// ... использование ...// ... usage ...
// нужно не забыть delete w на каждом пути выхода,// you must remember delete w on every exit path,
// включая исключения — источник утечек// including exceptions — a source of leaks
auto w = std::make_unique<Widget>();
// использование ...// usage ...
// delete вызывается автоматически при выходе из скоупа,// delete is called automatically on scope exit,
// в том числе при исключении// including when an exception is thrown
unique_ptr закрывает главный источник утечек и double-free в до-C++11 коде:unique_ptr closes off the main source of leaks and double-frees in pre-C++11 code:
Ручной delete нужно было ставить на каждом пути завершения функции, включая исключения — забытый delete на одном из путей означает утечку. unique_ptr переносит эту ответственность на деструктор, вызываемый компилятором гарантированно.A manual delete had to be placed on every function exit path, including exceptions — a forgotten delete on any one path means a leak. unique_ptr shifts that responsibility onto the destructor, which the compiler guarantees to invoke.
Raw pointer не говорит, кто владеет объектом — вызывающий или функция. Тип unique_ptr<T> в сигнатуре сразу сообщает: владение передаётся, единолично, и получатель отвечает за освобождение.A raw pointer doesn't say who owns the object — the caller or the function. The type unique_ptr<T> in a signature immediately communicates: ownership is being transferred, exclusively, and the recipient is responsible for releasing it.
Предшественник unique_ptr — std::auto_ptr (deprecated в C++11, удалён в C++17) — реализовывал передачу владения через copy-конструктор, что означало «копирование» с side-эффектом обнуления источника — ловушка, из-за которой auto_ptr нельзя было безопасно хранить в контейнерах STL.unique_ptr's predecessor, std::auto_ptr (deprecated in C++11, removed in C++17), implemented ownership transfer through the copy constructor — meaning a "copy" with the side effect of nulling out the source, a trap that made auto_ptr unsafe to store in STL containers.
unique_ptr — тонкая обёртка практически без overhead: хранит указатель на объект (и, если deleter стейтфул, сам deleter — обычно оптимизируется до нулевого дополнительного размера через empty base optimization при stateless deleter). Copy-конструктор и copy-присваивание объявлены как = delete, поэтому компилятор отклоняет любую попытку копирования на этапе компиляции — это статическая, а не рантайм-гарантия.unique_ptr is a thin wrapper with practically no overhead: it stores a pointer to the object (and, if the deleter is stateful, the deleter itself — usually optimized down to zero extra size via empty base optimization for a stateless deleter). The copy constructor and copy assignment operator are declared = delete, so the compiler rejects any attempt to copy at compile time — a static guarantee, not a runtime one.
std::unique_ptr<Widget> a = std::make_unique<Widget>();
// std::unique_ptr<Widget> b = a; // ошибка компиляции — copy-конструктор = delete// std::unique_ptr<Widget> b = a; // compile error — copy constructor = delete
std::unique_ptr<Widget> b = std::move(a); // ок — move-конструктор передаёт владение,// ok — the move constructor transfers ownership,
// a становится nullptr// a becomes nullptr
// кастомный deleter — часто для не-new/delete ресурсов:// a custom deleter — often used for non-new/delete resources:
auto file_closer = [](FILE* f) { if (f) fclose(f); };
std::unique_ptr<FILE, decltype(file_closer)> fp(fopen("x", "r"), file_closer);
// массивы — частичная специализация unique_ptr<T[]>:// arrays — the unique_ptr<T[]> partial specialization:
std::unique_ptr<int[]> arr(new int[10]); // деструктор вызовет delete[], не delete// the destructor calls delete[], not delete
Возврат unique_ptr из функции по значению — стандартный паттерн: компилятор применяет move (или RVO), а не копирование, потому что copy-конструктор недоступен, а move — доступен и дёшев (передача одного указателя).Returning a unique_ptr from a function by value is a standard pattern: the compiler applies move (or RVO) rather than copy, because the copy constructor is unavailable while move is available and cheap (transferring a single pointer).
Минимальный skeleton без deleter-параметра и без специализации под массивы — этого достаточно, чтобы показать понимание RAII + move-only семантики за отведённое время.A minimal skeleton with no deleter parameter and no array specialization — that's enough to demonstrate understanding of RAII + move-only semantics within the allotted time.
my_unique_ptr.hpp
template <class T>
class my_unique_ptr {
T* ptr_ = nullptr;
public:
my_unique_ptr() noexcept = default;
explicit my_unique_ptr(T* p) noexcept : ptr_(p) {}
~my_unique_ptr() { delete ptr_; }
// copy запрещён — единоличное владение// copy is disallowed — sole ownership
my_unique_ptr(const my_unique_ptr&) = delete;
my_unique_ptr& operator=(const my_unique_ptr&) = delete;
// move — передача владения, источник обнуляется// move — ownership transfer, the source is nulled out
my_unique_ptr(my_unique_ptr&& other) noexcept
: ptr_(other.ptr_) {
other.ptr_ = nullptr;
}
my_unique_ptr& operator=(my_unique_ptr&& other) noexcept {
if (this != &other) {
delete ptr_;
ptr_ = other.ptr_;
other.ptr_ = nullptr;
}
return *this;
}
T& operator*() const { return *ptr_; }
T* operator->() const { return ptr_; }
T* get() const { return ptr_; }
T* release() noexcept {
T* tmp = ptr_;
ptr_ = nullptr;
return tmp;
}
void reset(T* p = nullptr) noexcept {
delete ptr_;
ptr_ = p;
}
explicit operator bool() const { return ptr_ != nullptr; }
};
Что обычно проверяют дополнительным вопросом: почему operator=(my_unique_ptr&&) нужна проверка this != &other (self-move-assignment — иначе delete ptr_ удалит объект до того, как ptr_ = other.ptr_ его переприсвоит, хотя тут ptr_ == other.ptr_ и баг не проявится физически, но привычка проверять обязательна), и куда добавить кастомный deleter (второй шаблонный параметр + хранение его как поля, обычно через [[no_unique_address]] в более поздних стандартах или EBO-приём в C++11).What's usually probed with a follow-up question: why operator=(my_unique_ptr&&) needs the this != &other check (self-move-assignment — otherwise delete ptr_ would destroy the object before ptr_ = other.ptr_ reassigns it, though here ptr_ == other.ptr_ so the bug wouldn't physically manifest, but the habit of checking is still mandatory), and where a custom deleter would go (a second template parameter plus storing it as a field, typically via [[no_unique_address]] in later standards, or the EBO trick in C++11).
Попытка передать unique_ptr по значению в функцию без move (f(ptr) вместо f(std::move(ptr))) — ошибка компиляции, а не рантайм-баг, что на практике безопаснее, но требует явного std::move везде, где передаётся владение.Trying to pass a unique_ptr by value into a function without move (f(ptr) instead of f(std::move(ptr))) is a compile error, not a runtime bug — safer in practice, but it requires an explicit std::move everywhere ownership is transferred.
f(std::unique_ptr<T>(new T()), g()) — если порядок вычисления аргументов поставит new T() до конструирования unique_ptr, а g() кинет исключение между ними, память утечёт. std::make_unique<T>() (C++14) устраняет этот риск полностью; в C++11, где make_unique ещё нет в стандартной библиотеке, стоит писать его самостоятельно (пример — в variadic-templates.html) именно по этой причине.f(std::unique_ptr<T>(new T()), g()) — if the argument evaluation order places new T() before the unique_ptr is constructed, and g() throws in between, the memory leaks. std::make_unique<T>() (C++14) eliminates this risk entirely; in C++11, where make_unique isn't yet in the standard library, it's worth writing your own (example in variadic-templates.html) for exactly this reason.
ptr.get() возвращает голый указатель, не влияя на владение — если этот raw pointer случайно обернуть в новый unique_ptr (std::unique_ptr<T>(ptr.get())), возникают два владельца одного объекта и гарантированный double-free.ptr.get() returns a bare pointer without affecting ownership — if that raw pointer is accidentally wrapped into a new unique_ptr (std::unique_ptr<T>(ptr.get())), you end up with two owners of the same object and a guaranteed double-free.