Chuyển đến nội dung

std::unique_ptr: Quyền sở hữu độc quyền

Tìm hiểu cách std::unique_ptr cung cấp quyền sở hữu độc quyền bằng 0, cách sử dụng make_unique, công cụ xóa tùy chỉnh cho tài nguyên C và các mẫu chức năng gốc.

Unique_ptr là gì?

std::unique_ptrlà một con trỏ thông minh sở hữu một đối tượng độc quyền - có chính xác mộtunique_ptrtrỏ đến bất kỳ đối tượng cụ thể nào. Khiunique_ptrbị hủy (nằm ngoài phạm vi), nó sẽ tự động xóa đối tượng sở hữu.

Thuộc tính chính:
- Không thể sao chép — việc sao chép sẽ tạo ra hai chủ sở hữu, vi phạm quyền sở hữu độc quyền.
- Có thể di chuyển — quyền sở hữu có thể được chuyển quastd::move.
- Không có chi phí —sizeof(unique_ptr) == sizeof(T*)(với trình xóa mặc định). Mã được tạo giống hệt với cách sử dụng con trỏ thô khi xóa thủ công.
- Đây là **con trỏ thông minh mặc định. Hãy tiếp cậnunique_ptrtrước; chỉ sử dụngshared_ptrkhi bạn thực sự cần quyền sở hữu chung.

make_unique: Cách đúng duy nhất

std::make_unique(args...)(C++14) is the only correct way to create aunique_ptr. It is exception-safe, concise, and avoids writingnew.

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"

Custom Deleters for C Resources

Nhiều thư viện C trả về các con trỏ thô phải được giải phóng bằng một hàm cụ thể (không phảidelete).unique_ptrhỗ trợ trình xóa tùy chỉnh để xử lý những việc này. Trình xóa trở thành một phần của loại, do đó, nó không có chi phí hoạt động khi sử dụng con trỏ hàm hoặc lambda không trạng thái.

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);
}

Unique_ptr với mảng

std::unique_ptrquản lý một mảng được phân bổ động và tự động gọidelete[]. Nó cung cấpoperator[]thay vìoperator*vàoperator->.

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

However, std::vector is almost always preferable — it knows its size, supports iteration, and is just as efficient. Use unique_ptr chỉ khi giao tiếp với API C hoặc khi bạn cần bộ đệm không thể thay đổi kích thước, không thể sao chép.

Hàm xuất xưởng Trả về Unique_ptr

Returningunique_ptrfrom factory functions is the idiomatic C++ ownership pattern. The caller receives exclusive ownership. If the caller wants shared ownership, they can convert: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
}
Lỗi thường gặp

Không bao giờ chuyểnunique_ptrbằng tham chiếu const cho một hàm chỉ quan sát đối tượng - thay vào đó hãy chuyển một con trỏ hoặc tham chiếu thô. Thông sốunique_ptrbáo hiệu chuyển quyền sở hữu.

-void use(Widget* w)— quan sát, không có quyền sở hữu
-void sink(std::unique_ptr w)— nắm quyền sở hữu (gọi bằngstd::move)
-void reseat(std::unique_ptr& w)- có thể thay thế đối tượng sở hữu

Không bao giờ tạo haiunique_ptrtừ cùng một con trỏ thô — cả hai sẽ gọidelete, gây ra tình trạng trống kép. Đây là lý do tại saomake_uniqueđược ưu tiên: không có con trỏ thô nào để vô tình sử dụng lại.

Thực hành tốt

1. Mặc định làunique_ptr — đây phải là lựa chọn đầu tiên của bạn để phân bổ vùng nhớ heap.
2. Luôn sử dụngmake_unique — không bao giờ viếtunique_ptr(new T(...)).
3. Trả lạiunique_ptrtừ nhà máy — nó ngầm chuyển đổi thànhshared_ptrnếu người gọi cần.
4. Chuyển theo giá trị để chuyển quyền sở hữu —void sink(unique_ptr p). Người gọi phải sử dụngstd::move.
5. Truyền con trỏ/tham chiếu thô để quan sát —void use(const T& obj)hoặcvoid use(T* obj).
6. Đánh dấu hàm tạo/gán di chuyểnnoexcept — quan trọng đối với các loại được lưu trữ trong vùng chứa.

Điểm chính
  • unique_ptrprovides exclusive ownership with zero overhead vs raw pointers
  • Luôn tạo quastd::make_unique(args...)- không bao giờ sử dụngnewthô
  • Không thể sao chép nhưng có thể di chuyển - chuyển quyền sở hữu quastd::move
  • Custom deleters enable RAII for C resources (FILE*, sockets, etc.)
  • Trả vềunique_ptrtừ các hàm xuất xưởng — nó chuyển đổi thànhshared_ptrnếu cần
  • Truyền con trỏ hoặc tham chiếu thô khi hàm chỉ quan sát chứ không sở hữu

Câu đố — Kiểm tra kiến thức

(15 XP)

1. What is the overhead of`std::unique_ptr<T>`compared to a raw`T*`(with default deleter)?

2. Bạn nên chuyển`unique_ptr`cho một chức năng cần có quyền sở hữu như thế nào?

3. Tại sao`std::make_unique`được ưu tiên hơn`unique_ptr<T>(new T(...))`?