умные указатели — устройство и практикаsmart pointers — internals and practice

std::unique_ptr изнутриinternals

API уже разобран в stl/cpp11/unique-ptr.html — здесь про то, что делает unique_ptr особенным среди умных указателей: он не хранит вообще ничего, кроме самого указателя, и не добавляет ни единого байта или инструкции сверх ручного управления памятью. Это ровно то, что почти всегда просят реализовать на собеседовании.The API is already covered in stl/cpp11/unique-ptr.html — this page covers what makes unique_ptr special among smart pointers: it stores nothing but the pointer itself, adding not a single extra byte or instruction over manual memory management. This is exactly what interviews almost always ask you to implement.

нулевые накладные расходыzero overhead частый вопрос: реализуй самcommon interview question: implement it yourself

Внутреннее устройство: sizeof(unique_ptr) == sizeof(T*)Internal layout: sizeof(unique_ptr) == sizeof(T*)

В типичном случае (без пользовательского deleter, или с deleter без состояния, например обычной функцией) std::unique_ptr<T> внутри — это буквально один сырой указатель T*, и ничего больше. Никакого счётчика ссылок, никакого control block — вся модель владения существует только на уровне типовой системы компилятора, а не в рантайме.In the typical case (no custom deleter, or a stateless one — an ordinary function, say), std::unique_ptr<T> internally is literally one raw T* pointer and nothing else. No reference count, no control block — the entire ownership model exists only at the compiler's type-system level, not at runtime.

static_assert(sizeof(std::unique_ptr<int>) == sizeof(int*));  // проходит на всех мейнстрим-реализациях// holds on every mainstream implementation

Если задан deleter с состоянием (например, лямбда, захватывающая переменные), unique_ptr использует compressed_pair-подобную оптимизацию: если deleter — пустой класс без состояния (stateless functor), Empty Base Optimization позволяет не выделять под него отдельную память вовсе — итоговый размер остаётся равен размеру одного указателя даже с кастомным (но пустым) deleter-типом.If a stateful deleter is given (say, a lambda capturing variables), unique_ptr uses a compressed_pair-like optimization: if the deleter is an empty, stateless class (a stateless functor), the Empty Base Optimization avoids allocating any storage for it at all — the final size stays equal to one pointer's size even with a custom (but empty) deleter type.

Как реализовано единоличное владениеHow sole ownership is implemented

Copy-конструктор и copy-присваивание объявлены как = delete — попытка скопировать unique_ptr не компилируется, а не молча создаёт второго владельца того же ресурса. Move-конструктор просто копирует значение сырого указателя из источника в приёмник и обнуляет указатель в источнике — сам ресурс никогда физически не перемещается, перемещается только владение им.The copy constructor and copy assignment operator are declared = delete — attempting to copy a unique_ptr fails to compile, rather than silently creating a second owner of the same resource. The move constructor simply copies the raw pointer's value from the source into the destination and nulls out the source's pointer — the resource itself never physically moves, only ownership of it does.

template<typename T>
class MyUniquePtr {
    T* ptr = nullptr;
public:
    explicit MyUniquePtr(T* p = nullptr) : ptr(p) {}

    MyUniquePtr(const MyUniquePtr&) = delete;               // нельзя копировать// no copying
    MyUniquePtr& operator=(const MyUniquePtr&) = delete;

    MyUniquePtr(MyUniquePtr&& other) noexcept : ptr(other.ptr) {
        other.ptr = nullptr;                          // источник больше не владеет ничем// the source no longer owns anything
    }
    MyUniquePtr& operator=(MyUniquePtr&& other) noexcept {
        if (this != &other) {
            delete ptr;                               // освободить то, чем владели раньше// release whatever was owned before
            ptr = other.ptr;
            other.ptr = nullptr;
        }
        return *this;
    }

    ~MyUniquePtr() { delete ptr; }               // delete nullptr — безопасен, ничего не делает// delete nullptr is safe, a no-op

    T& operator*()  const { return *ptr; }
    T*  operator->() const { return ptr; }
    T*  get()       const { return ptr; }

    T* release() { T* tmp = ptr; ptr = nullptr; return tmp; }  // отдать владение, не удаляя// give up ownership without deleting
    void reset(T* p = nullptr) { delete ptr; ptr = p; }         // удалить старое, взять новое// delete the old, take the new
};

Это и есть тот самый «реализуй unique_ptr сам» — примерно 25 строк, покрывающих RAII, запрет копирования, move-семантику и базовый интерфейс. Именно компактность этой реализации — причина, по которой она так популярна как вопрос: она проверяет одновременно понимание Rule of 5, move-семантики и RAII в одном небольшом куске кода.This is exactly the "implement unique_ptr yourself" ask — roughly 25 lines covering RAII, the ban on copying, move semantics, and the basic interface. That compactness is exactly why it's such a popular question: it checks understanding of the Rule of 5, move semantics, and RAII all in one small piece of code.

Пользовательский deleter меняет тип, а не только поведениеA custom deleter changes the type, not just the behavior

В отличие от shared_ptr, где deleter — деталь рантайма (два shared_ptr<T> с разными deleter-ами — один и тот же тип), у unique_ptr тип deleter-а — часть самого типа std::unique_ptr<T, Deleter>. Это прямое следствие «нулевых накладных расходов»: раз в рантайме не хранится указатель на функцию-удалитель, единственный способ узнать, как удалять объект, — закодировать это в типе на этапе компиляции.Unlike shared_ptr, where the deleter is a runtime detail (two shared_ptr<T> instances with different deleters are the same type), unique_ptr's deleter type is part of the type itself: std::unique_ptr<T, Deleter>. This is a direct consequence of "zero overhead": since no function pointer to a deleter is stored at runtime, the only way to know how to delete the object is to encode it in the type at compile time.

auto fileDeleter = [](FILE* f) { if (f) fclose(f); };
std::unique_ptr<FILE, decltype(fileDeleter)> file(fopen("data.txt", "r"), fileDeleter);
// тип включает Deleter — нельзя присвоить unique_ptr сюда// the type includes Deleter — a unique_ptr can't be assigned here

ГраблиGotchas

unique_ptr<Base> с неполным типом и невиртуальным деструктором в заголовке — тихий UBwith an incomplete type and a non-virtual destructor in the header — silent UB

Классический PImpl-паттерн (unique_ptr<Impl> как поле, где Impl объявлен, но не определён в заголовке) требует, чтобы деструктор внешнего класса был объявлен в заголовке, но определён в .cpp-файле, где Impl уже полностью виден — иначе неявно сгенерированный деструктор попытается вызвать delete на неполном типе, что либо не скомпилируется, либо (что хуже) не вызовет деструктор Impl вовсе.The classic PImpl pattern (unique_ptr<Impl> as a field, where Impl is declared but not defined in the header) requires the outer class's destructor to be declared in the header but defined in the .cpp file, where Impl is already fully visible — otherwise the implicitly generated destructor tries to delete an incomplete type, which either fails to compile or, worse, silently skips Impl's destructor entirely.

get() не передаёт владение — источник частых double-freedoesn't transfer ownership — a common source of double-free

raw_ptr = uptr.get(); отдаёт наблюдение за адресом, но uptr по-прежнему владеет объектом и удалит его при выходе из скоупа. Хранение raw_ptr дольше, чем живёт uptr, — висячий указатель; попытка вручную delete raw_ptr — двойное удаление. Только release() реально отдаёт владение, обнуляя внутренний указатель uptr.raw_ptr = uptr.get(); hands out visibility into the address, but uptr still owns the object and will delete it on scope exit. Keeping raw_ptr around longer than uptr lives creates a dangling pointer; manually delete-ing raw_ptr is a double delete. Only release() genuinely transfers ownership, nulling out uptr's internal pointer.

unique_ptr<T[]> — отдельная специализация для массивов, не общий шаблон— a separate array specialization, not the general template

Указание массивного типа переключает unique_ptr на специализацию, использующую delete[] вместо delete, и добавляющую operator[]. Смешение unique_ptr<T> для массива, выделенного через new T[n], — undefined behavior: обычный delete на массиве не гарантированно вызовет деструкторы всех элементов.Specifying an array type switches unique_ptr to a specialization that uses delete[] instead of delete, and adds operator[]. Using a plain unique_ptr<T> for an array allocated via new T[n] is undefined behavior — an ordinary delete on an array isn't guaranteed to run every element's destructor.