std::unique_ptr: مالکیت انحصاری
بیاموزید که چگونه std::unique_ptr مالکیت انحصاری با سربار صفر فراهم میکند، نحوه استفاده از make_unique، حذفکنندههای سفارشی برای منابع C و الگوهای تابع کارخانهای.
unique_ptr چیست؟
std::unique_ptr یک اشارهگر هوشمند است که یک شیء را بهصورت انحصاری مالک است — دقیقاً یک unique_ptr به هر شیء معین اشاره میکند. وقتی unique_ptr نابود میشود (از دامنه خارج میشود)، بهصورت خودکار شیء تحت مالکیت را حذف میکند.
ویژگیهای کلیدی:
- غیرقابل کپی — کپی دو مالک ایجاد میکند که مالکیت انحصاری را نقض میکند.
- قابل انتقال — مالکیت میتواند از طریق std::move منتقل شود.
- سربار صفر — sizeof(unique_ptr (با حذفکننده پیشفرض). کد تولیدشده با استفاده از اشارهگر خام با delete دستی یکسان است.
- این پیشفرض اشارهگر هوشمند است. ابتدا به سراغ unique_ptr بروید؛ فقط وقتی واقعاً به مالکیت اشتراکی نیاز دارید از shared_ptr استفاده کنید.
make_unique: تنها روش صحیح
std::make_unique (C++14) تنها روش صحیح ایجاد unique_ptr است. ایمن در برابر استثنا، مختصر و بدون نیاز به نوشتن new است.
#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 از حذفکنندههای سفارشی برای مدیریت اینها پشتیبانی میکند. حذفکننده بخشی از نوع میشود، بنابراین هنگام استفاده از اشارهگر تابع یا لامبدای بدون حالت سربار صفر دارد.
#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.
#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 — مالکیت را میگیرد (با std::move فراخوانی کنید)
- void reseat(std::unique_ptr — ممکن است شیء تحت مالکیت را جایگزین کند
هرگز دو unique_ptr از یک اشارهگر خام یکسان نسازید — هر دو delete فراخوانی میکنند و باعث آزادسازی مضاعف میشوند. به همین دلیل make_unique ترجیح دارد: اشارهگر خامی وجود ندارد که تصادفاً بازاستفاده شود.
1. بهصورت پیشفرض از unique_ptr استفاده کنید — باید اولین انتخاب شما برای تخصیص هیپ باشد.
2. همیشه از make_unique استفاده کنید — هرگز unique_ptr ننویسید.
3. unique_ptr را از کارخانهها برگردانید — بهصورت ضمنی به shared_ptr تبدیل میشود اگر فراخواننده نیاز داشته باشد.
4. با مقدار ارسال کنید برای انتقال مالکیت — void sink(unique_ptr. فراخواننده باید از 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(...))` ترجیح دارد؟