رفتن به محتوا

std::unique_ptr: مالکیت انحصاری

بیاموزید که چگونه std::unique_ptr مالکیت انحصاری با سربار صفر فراهم می‌کند، نحوه استفاده از make_unique، حذف‌کننده‌های سفارشی برای منابع C و الگوهای تابع کارخانه‌ای.

unique_ptr چیست؟

std::unique_ptr یک اشاره‌گر هوشمند است که یک شیء را به‌صورت انحصاری مالک است — دقیقاً یک unique_ptr به هر شیء معین اشاره می‌کند. وقتی unique_ptr نابود می‌شود (از دامنه خارج می‌شود)، به‌صورت خودکار شیء تحت مالکیت را حذف می‌کند.

ویژگی‌های کلیدی:
- غیرقابل کپی — کپی دو مالک ایجاد می‌کند که مالکیت انحصاری را نقض می‌کند.
- قابل انتقال — مالکیت می‌تواند از طریق std::move منتقل شود.
- سربار صفر — sizeof(unique_ptr) == sizeof(T*) (با حذف‌کننده پیش‌فرض). کد تولیدشده با استفاده از اشاره‌گر خام با delete دستی یکسان است.
- این پیش‌فرض اشاره‌گر هوشمند است. ابتدا به سراغ unique_ptr بروید؛ فقط وقتی واقعاً به مالکیت اشتراکی نیاز دارید از shared_ptr استفاده کنید.

make_unique: تنها روش صحیح

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

unique_ptr با آرایه‌ها

std::unique_ptr یک آرایه تخصیص‌یافته پویا را مدیریت می‌کند و به‌صورت خودکار delete[] فراخوانی می‌کند. به جای operator* و operator->، operator[] فراهم می‌کند.

auto arr = std::make_unique<int[]>(100);  // 100 int مقداردهی‌شده با صفر
arr[0] = 42;

با این حال، std::vector تقریباً همیشه ترجیح دارد — اندازه‌اش را می‌داند، از تکرار پشتیبانی می‌کند و به همان اندازه کارآمد است. از unique_ptr فقط هنگام ارتباط با APIهای C یا زمانی که به بافر غیرقابل تغییر اندازه و غیرقابل کپی نیاز دارید استفاده کنید.

توابع کارخانه‌ای با بازگشت unique_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 را با ارجاع const به تابعی که فقط شیء را مشاهده می‌کند ارسال نکنید — به جای آن اشاره‌گر خام یا ارجاع ارسال کنید. پارامترهای 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 امتیاز)

1. سربار `std::unique_ptr<T>` نسبت به `T*` خام (با حذف‌کننده پیش‌فرض) چقدر است؟

2. چگونه باید `unique_ptr` را به تابعی که نیاز به گرفتن مالکیت دارد ارسال کنید؟

3. چرا `std::make_unique` بر `unique_ptr<T>(new T(...))` ترجیح دارد؟