Перейти к содержимому

std::unique ptr: Эксклюзивное право собственности

Узнайте, как std::unique ptr обеспечивает эксклюзивную собственность с нулевыми накладными расходами, как использовать make unique, пользовательские удаляющие устройства для ресурсов C и шаблоны фабричных функций.

Что такое уникальный ptr?

std::unique_ptr - это умный указатель, который владеет объектом исключительно - есть ровно один unique_ptr, указывающий на любой данный объект. При уничтожении unique_ptr (выходит из сферы действия) он автоматически удаляет принадлежащий ему объект.

Ключевые свойства:
Некопируемое копирование создаст двух владельцев, нарушая исключительное право собственности.
Мобильный — право собственности может быть передано через std::move.
- Нулевой накладные расходы - sizeof(unique_ptr) == sizeof(T*) (с дефолтным удаляющим устройством). Созданный код идентичен использованию необработанных указателей с ручным удалением.
- Это "умный указатель" по умолчанию. Сначала используйте unique_ptr; используйте shared_ptr только тогда, когда вам действительно нужно совместное владение.

Исполнитель: The Only Correct Way

std::make_unique(args...) (C++14) является единственным правильным способом создания unique_ptr. Он безопасен, лаконичен и избегает написания new.

unique_ptr_basics.cpp
#include <iostream>
#include <memory>
#include <string>

struct Connection {
    std::string host;
    int port;
    Connection(std::string h, int p) : host(std::move(h)), port(p) {
        std::cout << "Connected to " << host << ":" << port << '\n';
    }
    ~Connection() {
        std::cout << "Disconnected from " << host << '\n';
    }
    void query(const std::string& sql) {
        std::cout << "Executing: " << sql << '\n';
    }
};

int main() {
    // CORRECT: use make_unique
    auto conn = std::make_unique<Connection>("localhost", 5432);
    conn->query("SELECT * FROM users");

    // Access the raw pointer (non-owning)
    Connection* raw = conn.get();  // does NOT transfer ownership

    // Transfer ownership
    auto conn2 = std::move(conn);
    // conn is now nullptr
    if (!conn) std::cout << "conn is null after move\n";

    // conn2 destroyed here — destructor called automatically
}  // Output: "Disconnected from localhost"

Пользовательские удаления для C ресурсов

Многие библиотеки C возвращают исходные указатели, которые должны быть освобождены с определенной функцией (не delete). unique_ptr поддерживает пользовательские удаляющие устройства для их обработки. Удалитель становится частью типа, поэтому он имеет нулевые накладные расходы при использовании указателя функции или лямбды без состояния.

custom_deleter.cpp
#include <cstdio>
#include <memory>
#include <iostream>

// Custom deleter for FILE*
struct FileDeleter {
    void operator()(FILE* f) const {
        if (f) {
            std::fclose(f);
            std::cout << "File closed\n";
        }
    }
};

// Type alias for clarity
using UniqueFile = std::unique_ptr<FILE, FileDeleter>;

// Factory function
UniqueFile open_file(const char* path, const char* mode) {
    FILE* f = std::fopen(path, mode);
    if (!f) throw std::runtime_error("Cannot open file");
    return UniqueFile(f);
}

int main() {
    try {
        auto file = open_file("/tmp/test.txt", "w");
        std::fputs("Hello, RAII!\n", file.get());
        // file automatically closed when scope exits
    } catch (const std::exception& e) {
        std::cerr << e.what() << '\n';
    }

    // Lambda deleter (less common, but useful for one-offs)
    auto deleter = [](int* p) { std::cout << "custom free\n"; delete p; };
    std::unique_ptr<int, decltype(deleter)> p(new int(42), deleter);
}

Уникальный ptr с Arrays

std::unique_ptr управляет динамически распределенным массивом и автоматически вызывает delete[]. operator[] вместо operator* и operator->.

auto arr = std::make_unique<int[]>(100);  // 100 zero-initialized ints
arr[0] = 42;

Тем не менее, std::vector почти всегда предпочтительнее - он знает свой размер, поддерживает итерацию и так же эффективен. Используйте unique_ptr только при взаимодействии с C API или когда вам нужен неразмерный, не копируемый буфер.

Заводские функции Возвращение уникальных ptr

Возвращение unique_ptr из заводских функций является идиоматической моделью владения C++. Абонент получает исключительное право собственности. Если абонент хочет совместное владение, он может конвертировать: std::shared_ptr sp = factory();.

factory_unique_ptr.cpp
#include <memory>
#include <iostream>
#include <string>

class Shape {
public:
    virtual ~Shape() = default;
    virtual double area() const = 0;
    virtual std::string name() const = 0;
};

class Circle : public Shape {
    double radius_;
public:
    explicit Circle(double r) : radius_(r) {}
    double area() const override { return 3.14159 * radius_ * radius_; }
    std::string name() const override { return "Circle"; }
};

class Rectangle : public Shape {
    double w_, h_;
public:
    Rectangle(double w, double h) : w_(w), h_(h) {}
    double area() const override { return w_ * h_; }
    std::string name() const override { return "Rectangle"; }
};

// Factory — caller owns the result
std::unique_ptr<Shape> make_shape(const std::string& type, double a, double b = 0) {
    if (type == "circle")    return std::make_unique<Circle>(a);
    if (type == "rectangle") return std::make_unique<Rectangle>(a, b);
    return nullptr;  // unknown shape
}

int main() {
    auto s = make_shape("circle", 5.0);
    if (s) {
        std::cout << s->name() << " area: " << s->area() << '\n';
    }
    // s automatically deleted here
}
Подводный камень

Никогда не передавайте unique_ptr по ссылке на функцию, которая наблюдает только за объектом, вместо этого передавайте необработанный указатель или ссылку. Параметры unique_ptr сигнализируют о передаче прав собственности.

- void use(Widget* w) - не имеет права собственности
- void sink(std::unique_ptr w) - приобретает право собственности (позвоните в std::move)
void reseat(std::unique_ptr& w) может заменить принадлежащий объект

Никогда не создавайте два unique_ptr из одного и того же исходного указателя ** - оба будут называть delete, что приведет к двойному освобождению. Вот почему make_unique является предпочтительным: нет необработанного указателя для случайного повторного использования.

Лучшая практика

1. По умолчанию unique_ptr — это должен быть ваш первый выбор для распределения кучи.
2. Всегда используйте make_unique — никогда не пишите unique_ptr(new T(...)).
3.Возврат unique_ptr с заводов — он неявно конвертируется в shared_ptr, если абоненту это нужно.
4.Перейти по стоимости к передаче права собственности — void sink(unique_ptr p). Абонент должен использовать std::move.
5. Передайте исходный указатель/ссылку для наблюдения — void use(const T& obj) или void use(T* obj).
6. Маркодвигатель/назначение noexcept — критический для типов, хранящихся в контейнерах.

Ключевые выводы
  • unique_ptr предоставляет эксклюзивное право собственности с нулевыми накладными расходами по сравнению с исходными указателями
  • Всегда создавайте с помощью std::make_unique(args...), никогда не используйте сырой new
  • Некопируемый, но движимый — передача прав собственности через std::move
  • Пользовательские удаляющие устройства включают RAII для ресурсов C (FILE*, сокетов и т.д.)
  • Возврат unique_ptr с заводских функций — при необходимости конвертируется в shared_ptr
  • Передайте необработанный указатель или ссылку, когда функция только наблюдает, а не владеет

Тест — проверьте свои знания

(15 XP)

1. Что такое накладные расходы `std::unique_ptr<T>` по сравнению с необработанным `T*` (с дефолтом)?

2. Как передать `unique_ptr` функции, которая должна стать собственностью?

3. Почему `std::make_unique` предпочтительнее `unique_ptr<T>(new T(...))`?