الملكية الفريدة
الإصدار ≥ C++ 11
std::unique_ptr
هو قالب صنف (class template) يُدير دورة حياة الكائنات المخزّنة ديناميكيًا، وعلى خلاف std::shared_ptr
، فإنّ كل كائن ديناميكي يكون مملوكًا لمؤشّر حصري (std::unique_ptr
) واحد في أيّ لحظة.
// أنشئ عددًا صحيحًا ديناميكيًا تساوي قيمته 20 ويكون مملوكًا من مؤشّر وحيد std::unique_ptr<int> ptr = std::make_unique<int>(20);
ملاحظة: std::unique_ptr
متاحٌ منذ الإصدار C++ 11، أما std::make_unique
فمنذ الإصدار C++ 14.
المتغيّر ptr
يحتوي على مؤشّر إلى عدد صحيح (int
) مخزّن ديناميكيًّا، ويُحذف الكائن المملوك عندما يخرج مؤشّر حصريّ (unique pointer) يمتلك كائنًا عن النطاق (scope)، أي أنّ المُدمِّر (destructor) الخاصّ به سيُستدعى إذا كان الكائن من نوع صنف (class type)، كما ستُحرّر ذاكرة ذلك الكائن.
ولاستخدام std::unique_ptr
و std::make_unique
مع المصفوفات، استخدم الصياغة التالية:
// إنشاء مؤشّر حصري يشير إلى عدد صحيح يساوي 59 std::unique_ptr<int> ptr = std::make_unique<int>(59); // إنشاء مؤشّر حصري يشير إلى مصفوفة من 15 عددًا صحيحا std::unique_ptr<int[]> ptr = std::make_unique<int[]>(15);
يمكنك الوصول إلى مؤشّر حصريّ (std::unique_ptr
) كما تصل إلى أيّ مؤشّر خام، لأنه يزيد تحميل (overloads) تلك العوامل، كما يمكنك نقل ملكية محتويات مؤشّر ذكي إلى مؤشّر آخر باستخدام std::move
التي ستجعل المؤشّر الذكي الأصلي يشير إلى nullptr
.
// 1. std::unique_ptr std::unique_ptr <int> ptr = std::make_unique <int> (); // 1 تغيير القيمة إلى *ptr = 1; // 2. std::unique_ptr // سيفقد ملكية الكائن 'ptr' فإن 'ptr2' إلى 'ptr' بنقل std::unique_ptr <int> ptr2 = std::move(ptr); int a = *ptr2; // 'a' is 1 int b = *ptr; // 'nullptr' يساوي ptr // بسبب أمر النقل أعلاه
تمرير unique_ptr
إلى دالّة كمعامل:
void foo(std::unique_ptr <int> ptr) { // ضع شيفرتك هنا } std::unique_ptr <int> ptr = std::make_unique <int> (59); foo(std::move(ptr))
إعادة مؤشّر unique_ptr
من دالّة هي الطريقة المفضلة في C++ 11 لكتابة الدوال المُنتجة (factory functions)، إذ أنّها تعبّر بوضوح عن ملكية القيمَة المعادة، فالمُستدعي يمتلك المؤشّر unique_ptr
الناتج وهو المسؤول عنه.
std::unique_ptr <int> foo() { std::unique_ptr <int> ptr = std::make_unique <int> (59); return ptr; } std::unique_ptr <int> ptr = foo();
قارن هذا بالشيفرة التالية:
int* foo_cpp03(); int* p = foo_cpp03(); // أم يجب علي حذفه في مرحلة ما p هل أملك // جواب هذا السؤال غير واضح
الإصدار <C++ 14
قُدِّم قالب الصنف make_unique
منذ الإصدار C++ 14. لكن من السهل إضافته يدويًا إلى C++ 11:
template < typename T, typename...Args > typename std::enable_if < !std::is_array <T> ::value, std::unique_ptr <T>> ::type make_unique(Args && ...args) { return std::unique_ptr <T> (new T(std::forward < Args > (args)...)); } // لأجل المصفوفات make_unique استخدام template < typename T > typename std::enable_if < std::is_array <T> ::value, std::unique_ptr <T>> ::type make_unique(size_t n) { return std::unique_ptr <T> (new typename std::remove_extent <T> ::type[n]()); }
الإصدار ≥ C++ 11
على عكس المؤشّر "الذكي" std::auto_ptr
الغبي، فإن المؤشّر unique_ptr
يستطيع أن يُستنسخ (instantiated) عبر تخصيص المتجه -وليس std::vector
- إذ كانت الأمثلة السابقة للتخصيصات العددية، أما إن أردنا الحصول على مصفوفة من الأعداد الصحيحة المخزّنة ديناميكيًا، فإنك تحدد int[]
كنوع للقالب -وليس فقط int
-، انظر:
std::unique_ptr<int[]> arr_ptr = std::make_unique<int[]>(10);
يمكن تبسيط الشّيفرة باستخدام:
auto arr_ptr = std::make_unique<int[]>(10);
الآن، يمكنك استخدام arr_ptr
كما لو كان مصفوفة:
arr_ptr[2] = 10;
ولا داعي للقلق بشأن إلغاء تخصيص الذاكرة (de-allocation)، فهذا الإصدار المُتخصّص في القوالب يستدعي المُنشِئاتِ (constructors) والمدمِّرات (destructors) بالشكل المناسب، وعليه فإن استخدام الإصدار المتجهي من unique_ptr
أو المتجه vector
نفسه تعود للتفضيل الشخصي.
كان std::auto_ptr
متاحًا في الإصدارات السابقة لـ C++ 11. وعلى عكس unique_ptr
فإن مؤشّرات auto_ptr
مسموح بنسخها، وسيؤدّي هذا إلى أن يفقِد المصدر ptr
ملكية المؤشِّر لتتحوّل الملكيّة إلى الهدف.
الملكية المشتركة std::shared_ptr
يُعرِّف قالب الصنف std::shared_ptr
مؤشّرًا مُشتركًا قادرًا على مشاركة ملكيّة كائن ما مع مؤشّرات مشتركة أخرى، وهذا يتعارض مع طبيعة المؤشّرات الحصرية (std::unique_ptr
) التي تمثّل ملكية حصرية.
ويُنفَّذ سلوك المشاركة عبر تقنية تُعرف بعدِّ المراجع (reference counting)، حيث يُخزّن عدد المؤشّرات المشتركة التي تشير إلى الكائن مع هذا الكائن نفسه، ويُدمَّر هذا الكائن تلقائيًا عندما يصل هذا العدد إلى الصفر إما بسبب تدمير النسخة الأخيرة من std::shared_ptr
أو إعادة تعيينها. انظر المثال التالي حيث يكون firstShared
مؤشرًا مشتركًا لنسخة جديدة من Foo
:
std::shared_ptr<Foo> firstShared = std::make_shared<Foo>(/*args*/);
لإنشاء عدّة مؤشّرات ذكية تشترك في نفس الكائن، نحتاج إلى إنشاء مؤشّر مشترك آخر يأخذ اسم المؤشّر الأول المشترك، ولدينا الآن طريقتان لفعل ذلك:
std::shared_ptr<Foo> secondShared(firstShared); // الطريقة 1: الإنشاء بالنسخ std::shared_ptr<Foo> secondShared; secondShared = firstShared; // الطريقة 2: الإسناد
كلا الطّريقتان المَذكورتان أعلاه تجعلان secondShared
مؤشّرًا مشتركًا يتشارك ملكية نُسخة Foo
مع firstShared
.
تعمل المؤشّرات الذكية مثل المؤشّرات الخام، هذا يعني أنه يمكنك استخدام *
لتحصيلها، كما أنّ العامل العادي ->
يعمل أيضًا:
secondShared->test(); // Foo::test() يستدعي
أخيرًا، عندما يخرج آخر مؤشّر مشترك مُكنّى (aliased) عن النطاق، فسيُستدعى المدمّر الخاصّ بنسخة Foo
.
تنبيه: قد يؤدي إنشاء مؤشّر مشترك إلى إطلاق اعتراض bad_alloc
عند الحاجة إلى تخصيص بيانات إضافية في الملكيّة المشتركة، وفي حال تمرير مؤشّر عادي إلى المُنشئ فإنه سيَفترض أنه يمتلك الكائن الذي يشير إليه ذلك المؤشّر وسَيستدعي دالّة الحذف (deleter) في حالة إطلاق اعتراض. هذا يعني أنّ shared_ptr<T>(new T(args))
لن تُسرِّب كائن T
في حال فشل تخصيص ذاكرة shared_ptr<T>
. أيضًا يُنصح باستخدام make_shared<T>(args)
أو allocate_shared<T>(alloc, args)
إذ يحسّن كفاءة تخصيص الذاكرة.
تخصيص ذاكرة المصفوفات باستخدام المؤشرات المشتركة
C++ 11 =< الإصدار < C++ 17
لا توجد طريقة مباشرة لتخصيص ذاكرة المصفوفات باستخدام make_shared<>
، لكن من الممكن إنشاء مصفوفات shared_ptr<>
باستخدام new
و std::default_delete
. على سبيل المثال، لتخصِيص ذاكرة مصفوفة عُشارية مكوّنة من أعداد صحيحة، يمكننا كتابة الشيفرة التالية:
shared_ptr<int> sh(new int[10], std::default_delete<int[]>());
يجب تحديد std::default_delete
هنا للتأكّد من تنظيف الذاكرة المُخصّصة بشكل صحيح باستخدام delete[]
. وإذا عرفنا حجم المصفوفة في وقت التصريف، يمكننا القيام بذلك بالطريقة التالية:
template < class Arr > struct shared_array_maker {}; template < class T, std::size_t N > struct shared_array_maker < T[N] > { std::shared_ptr <T> operator() const { auto r = std::make_shared < std::array < T, N >> (); if (!r) return {}; return {r.data(), r}; } }; template < class Arr > auto make_shared_array() -> decltype(shared_array_maker < Arr > {}()) { return shared_array_maker < Arr > {}(); }
سيعيد make_shared_array<int[10]>
عندئذٍ المؤشّرَ المشترك shared_ptr<int>
مشيرًا إلى عناصر المصفوفة الذين أنشئوا افتراضيًا.
الإصدار ≥ C++ 17
تحسَّن دعم المؤشّرات المشتركة للمصفوفات في الإصدار C++ 17، فلم يعد ضروريًا تحديد دالّة حذف (array-deleter) للمصفوفة بشكل صريح، وصار من الممكن تحصيل المؤشّر المشترك باستخدام عامل فهرسة المصفوفات []
:
std::shared_ptr<int[]> sh(new int[10]); sh[0] = 42;
تستطيع المؤشرات المشتركة أن تشير إلى كائن فرعي من الكائن الذي تمتلكه، انظر:
struct Foo { int x; }; std::shared_ptr<Foo> p1 = std::make_shared<Foo>(); std::shared_ptr<int> p2(p1, &p1->x);
يمتلك كل من p2
و p1
الكائن المنتمي إلى النوع Foo
إلا أنّ p2
يشير إلى العضو العددي الصحيح x
، وهذا يعني أنّه في حال خرج p1
عن النطاق أو أُعِيد تعيينه فسيظلّ الكائن Foo
حيًّا، ممّا يضمن أنّ p2
لن يتراجع ((dangle.
ملاحظة مهمة: لا تعرِف المؤشّرات المشتركة إلا نفسَها وبقيّة المؤشّرات المشتركة الأخرى التي أنشئت باستخدام المُنشئ المكنّى (alias constructor). ولا تعرف أيّ مؤشّرات أخرى حتى المؤشّرات المشتركة التي أنشئت بالإشارة إلى نفس نُسخة Foo
. انظر ()shared1.reset
في المثال التالي إذ سيحذف foo
لأن shared1
هو المؤشر المشترك الوحيد الذي يمتلكه:
Foo *foo = new Foo; std::shared_ptr < Foo > (foo); std::shared_ptr < Foo > shared2(foo); // لا تفعل هذا shared1.reset(); shared2 -> test(); // قد حُذفت shared2 الخاصة بـ foo سلوك غير محدد إذ أن.
نقل ملكية المؤشرات المشتركة
افتراضيًّا، تزيد المؤشّرات المشتركة shared_ptr
عدد المراجع (reference count) لكنها لا تنقل الملكية، غير أننا نستطيع جعلها تنقل الملكية باستخدام std::move
:
shared_ptr<int> up = make_shared<int>(); // نقل الملكية shared_ptr<int> up2 = move(up); // 1 ذي العدّاد up2 يساوي 0، وملكية المؤشّر محصورة في up الآن، عدّاد المراجع الخاص بـ
المشاركة بملكية مؤقتة
يمكن أن تشير نُسخ المؤشّرات الضعيفة std::weak_ptr
إلى الكائنات المملوكة لنُسخ المؤشّرات المشتركة std::shared_ptr
وتصبح مالِكة مؤقتة، هذا يعني أنّ المؤشّرات الضعيفة لا تغير عدد مراجع الكائن، وعليه لا تمنع حذف الكائن إذا أُعيد إسناد كافّة مؤشّرات الكائن المشتركة أو حذفها. انظر المثال التالي إذ سنستخدم المؤشّرات الضعيفة للسماح بحذف كائن:
#include <memory> #include <vector> struct TreeNode { std::weak_ptr < TreeNode > parent; std::vector < std::shared_ptr < TreeNode > > children; }; int main() { // TreeNode إنشاء std::shared_ptr < TreeNode > root(new TreeNode); // إعطاء الأب 100 عقدة فرعية for (size_t i = 0; i < 100; ++i) { std::shared_ptr < TreeNode > child(new TreeNode); root -> children.push_back(child); child -> parent = root; } // ومعه العُقَد الفرعية root إعادة تعيين المؤشّر المشترك، وتدمير الكائن root.reset(); }
تُسنّد العقدة الجذر إلى parent
بينما تُضاف عقد فرعية (child nodes) إلى فروع العقدة الجذر، كما يصرَّح العضو parent
كمؤشّر ضعيف بدلاً من مؤشّر مشترك حتى لا يُزاد في عدد مراجع العقدة الجذر، وسيُحذَف الجذر عند إعادة تعيين العقدة الجذر في نهاية main()
. أيضًا، نظرًا لأنّ مراجع المؤشّر المشترك المتبقيّة التي تشير إلى العقد الفرعية قد ضُمِّنت في مجموعة الجذر children
، لذا ستُدمَّر جميع العقد الفرعية لاحقًا.
قد لا تُحرّر الذاكرة المخصّصة التي تخصّ المؤشّر المشترك حتى يصل العدّادان المراجعيّان shared_ptr
و weak_ptr
إلى الصفر.
#include <memory> int main() { { std::weak_ptr <int> wk; { // عبر تخصيص الذاكرة مرة واحدة std::make_shared تُحسَّن // تخصّص الذاكرة مرتين std::shared_ptr<int>(new int(42)) std::shared_ptr <int> sh = std::make_shared <int> (42); wk = sh; // ينبغي أن تكون قد حُرِّرت الآن sh ذاكرة } // ما تزال حية wk ّلكن } // (sh و wk) حُرِّرت الذاكرة الآن }
نظرًا لأنّ المؤشّرات الضعيفة (std::weak_ptr
) لا تُبقي الكائن الذي تشير إليه على قيد الحياة، فلا يمكن الوصول المباشر للبيانات عبر المؤشّرات الضعيفة، بيْد أنّها توفّر تابعًا lock()
، والذي يُرجعَ مؤشّرًا مشتركًا std::shared_ptr
إلى الكائن المشار إليه:
#include <cassert> #include <memory> int main() { { std::weak_ptr <int> wk; std::shared_ptr <int> sp; { std::shared_ptr <int> sh = std::make_shared <int> (42); wk = sh; // wk سيؤدي إلى إنشاء مؤشّر مشترك يشير إلى الكائن الذي يشير إليه lock استدعاء sp = wk.lock(); // sp عند هذه اللحظة، على خلاف sh ستُحذف } // تُبقي البيانات حية sp // إن أردنا lock() ما يزال بإمكاننا استدعاء // wk من أجل لحصول على مؤشّر مشترك يشير إلى نفس البيانات من assert( * sp == 42); assert(!wk.expired()); // سيمحو البيانات sp إعادة إسناد // لأنه آخر مؤشّر مشترك له ملكية sp.reset(); // سيعيد مؤشّرا مشتركا فارغا wk على lock محاولة استدعاء // لأن البيانات قد حُذِفت سلفا sp = wk.lock(); assert(!sp); assert(wk.expired()); } }
استخدام دوال حذف مخصصة لتغليف واجهة C
تتوفّر العديد من واجهات C (مثل SDL2) على دوالّ الحذف (deletion functions) الخاصّة بها. هذا يعني أنّه لا يمكنك استخدام المؤشّرات الذكية مباشرة:
std::unique_ptr<SDL_Surface> a; // لن يعمل، غير آمن
بدلاً من ذلك، سيكون عليك أن تعرّف دالّة الحذف الخاصّة بك، تستخدم الأمثلة هنا بنية SDL_Surface
التي يجب تحريرها باستخدام الدالّة SDL_FreeSurface()
، بيْد أنّه يجب تكييفها مع العديد من واجهات C الأخرى. كذلك يجب أن تكون دالة الحذف قابلة للاستدعاء باستخدام مؤشّر وسيط (pointer argument) على النحو التالي:
std::unique_ptr<SDL_Surface, void(*)(SDL_Surface*)> a(pointer, SDL_FreeSurface);
سيعمل أيّ كائن آخر قابل للاستدعاء أيضًا، على سبيل المثال:
struct SurfaceDeleter { void operator()(SDL_Surface* surf) { SDL_FreeSurface(surf); } }; std::unique_ptr < SDL_Surface, SurfaceDeleter > a(pointer, SurfaceDeleter {}); // آمن std::unique_ptr < SDL_Surface, SurfaceDeleter > b(pointer); // مكافئ للشيفرة أعلاه
سيوفر لك هذا إدارة آمنة واقتصادية للذاكرة بدون استخدام unique_ptr
وكذلك ستحصل على الأمان من ناحية الاعتراضات (Exceptions).
لاحظ أنّ دالة الحذف جزء من النوع بالنسبة للمؤشّر الحصري (unique_ptr
)، ويستطيع التنفيذ (implementation) استخدام تحسين الأساس الفارغ (Empty base optimization) لتجنّب أي تغيير في الحجم بالنسبة لدَوالّ الحذف المخصّصة الفارغة (Empty custom deleters). لذا، رغم أنّ:
std::unique_ptr<SDL_Surface, SurfaceDeleter> std::unique_ptr<SDL_Surface, void(*)(SDL_Surface*)>
يحلّان المشكلة بطريقة متماثلة، فإنّ حجم النوع الأوّل يساوي حجم مؤشّر واحد، بينما يجب أن يحتفظ النوع الأخير بمؤشّرين: أي المؤشّر SDL_Surface
* ومؤشّر الدالة! أيضًا، يُفضّل تغليف دوالّ الحذف المخصّصة الحُرّةفي نوع فارغ، ويمكن استخدام مؤشّر مشترك (shared_ptr
) بدلاً من مؤشّر حصري (unique_ptr
) في الحالات التي يكون فيها عدُّ المراجع مهمًا.
تخزِّن المؤشّرات المشتركة دالّة الحذف دائمًا، مما يؤدّي إلى محوِ نوع دالّة الحذف، هذا قد يكون مفيدًا في الواجهات البرمجية (APIs). يعيب استخدام المؤشّرات المشتركة هو أنّ حجم ذاكرة تخزين دالّة الحذف ستكون أكبر، وتكلفة صيانة عدّاد المراجع ستكون أكبر كذلك.
// دالة الحذف مطلوبة في وقت الإنشاء وهي جزء من النوع std::unique_ptr<SDL_Surface, void(*)(SDL_Surface*)> a(pointer, SDL_FreeSurface); // دالة الحذف مطلوبة في وقت الإنشاء ولكنها ليست جزءًا من النوع std::shared_ptr<SDL_Surface> b(pointer, SDL_FreeSurface);
الإصدار ≥ C++ 17
تسهّل template auto
تغليف دوال الحذف المخصّصة:
template <auto DeleteFn> struct FunctionDeleter { template <class T> void operator()(T* ptr) { DeleteFn(ptr); } }; template <class T, auto DeleteFn> using unique_ptr_deleter = std::unique_ptr<T, FunctionDeleter<DeleteFn>>;
المثال أعلاه سُيصبح:
unique_ptr_deleter<SDL_Surface, SDL_FreeSurface> c(pointer);
الغرض من auto
في المثال السابق هو التعامل مع جميع الدوالّ الحرّة (free functions)، سواء كانت تُعيد void
(مثل SDL_FreeSurface
) أم لا (مثل fclose
).
الملكية الحصرية بدون دلالة النقل
الإصدار
ملاحظة: أُهمِلت std::auto_ptr
في C++ 11 وستُزال تمامًا في C++ 17، لذا لا ينبغي أن تستخدمها إلّا إن كنت مضطرًا لاستخدام C++ 03 أو إصدار سابق وكنت على استعداد لتوخّي الحذر الشديد، أما فيما سوى ذلك فيوصى بالانتقال إلى unique_ptr و std::move
.
قبل المؤشّرات الحصريّة (std::unique_ptr
) والدلالات النقليّة (move semantics)، كان لدينا std::auto_ptr
، والذي يوفّر ملكيّة حصريّة، غير أنّه ينقل الملكية عند النسخ. وكما هو الحال مع جميع المؤشّرات الذكية، فإن المؤشّر std::auto_ptr
ينظف الموارد تلقائيًا:
{ std::auto_ptr<int> p(new int(42)); std::cout << *p; } // هنا، لا يوجد تسرب في الذاكرة p تُحذف
لكن يسمح بمالك واحد فقط:
std::auto_ptr<X> px = ...; std::auto_ptr<X> py = px; // فارغ الآن px
هذا يسمح باستخدام std::auto_ptr
للإبقاء على المِلكِيّة صريحةً وحصريّة، لكن مع خطر خسارة الملكية بشكل غير مقصود:
void f(std::auto_ptr < X > ) { // X افتراض ملكية // تحذفها في نهاية النطاق. }; std::auto_ptr < X > px = ...; f(px); // X ملكية f تتطلب // فارغ الآن px px -> foo(); // NPE! // لا يحذف px.~auto_ptr()
حدث نقل الملكية في مُنشئ النَّسْخ (copy constructor)، ويأخذ مُنشئ النَّسخ وعامل تعيين النَّسخ الخاصّ بالمؤشّر auto_ptr
معامِلاته بواسطة مرجع غير ثابت (non-const) حتى يمكن تعديلها. هذا مثال على ذلك:
template < typename T > class auto_ptr { T * ptr; public: auto_ptr(auto_ptr & rhs): ptr(rhs.release()) {} auto_ptr & operator = (auto_ptr & rhs) { reset(rhs.release()); return *this; } T * release() { T * tmp = ptr; ptr = nullptr; return tmp; } void reset(T * tmp = nullptr) { if (ptr != tmp) { delete ptr; ptr = tmp; } } /* دوال أخرى */ };
هذا يكسر الدلالة النسخيّة (copy semantics) التي تتطلّب أن ينتُج عن عمليّة نسخ كائنٍ ما نسختان متكافئتان. إن كان T
نوعًا قابلًا للنسخ، فيمكن كتابة:
T a = ...; T b(a); assert(b == a);
لكن ليس هذا هو الحال بالنسبة إلى auto_ptr
، لهذا من غير الآمن وضع auto_ptr
في الحاويات.
تحويل المؤشرات المشتركة
لا يمكن استخدام:
-
static_cast
-
const_cast
-
dynamic_cast
-
reinterpret_cast
مباشرةً على المؤشّرات المشتركة std::shared_ptr
للحصول على مؤشّر يتشارك المِلكِيَّة مع المؤشّر المُمرَّر كوسيط، وإنما يجب استخدام الدوالّ:
-
std::static_pointer_cast
-
std::const_pointer_cast
-
std::dynamic_pointer_cast
-
std::reinterpret_pointer_cast
struct Base { virtual~Base() noexcept {}; }; struct Derived: Base {}; auto derivedPtr(std::make_shared < Derived > ()); auto basePtr(std::static_pointer_cast < Base > (derivedPtr)); auto constBasePtr(std::const_pointer_cast < Base const > (basePtr)); auto constDerivedPtr(std::dynamic_pointer_cast < Derived const > (constBasePtr));
لاحظ أنّ std::reinterpret_pointer_cast
غير متاحة في C++ 11 و C++ 14، إذ لم تُقترح إلا في N3920، ولم تُعتمد في مكتبة الأساسيات (Library Fundamentals) TS إلّا في فبراير 2014. لكن يبقى من الممكن تنفيذها على النحو التالي:
template < typename To, typename From > inline std::shared_ptr < To > reinterpret_pointer_cast( std::shared_ptr < From > const & ptr) noexcept { return std::shared_ptr < To > (ptr, reinterpret_cast < To* > (ptr.get())); }
كتابة مؤشر ذكي: value_ptr
value_ptr
هو مؤشّر ذكيّ يتصرّف كقيمة، فعند النسخ ينسخ محتوياته، وعند الإنشاء ينشئ محتوياته أيضًا. انظر:
// std::default_delete: مثل template <class T> struct default_copier { // null فارغا ويعيد القيمة T const* ينبغي أن يعالج الناسخ نوعا T *operator()(T const *tin) const { if (!tin) return nullptr; return new T(*tin); } void operator()(void *dest, T const *tin) const { if (!tin) return; return new (dest) T(*tin); } }; // لمعالجة القيمة الفارغة tag صنف: struct empty_ptr_t { }; constexpr empty_ptr_t empty_ptr{}; // مؤشر القيمة يطبع نفسه: template <class T, class Copier = default_copier<T>, class Deleter = std::default_delete<T>, class Base = std::unique_ptr<T, Deleter>> struct value_ptr : Base, private Copier { using copier_type = Copier; // unique_ptr من typedefs أيضًا using Base::Base; value_ptr(T const &t) : Base(std::make_unique<T>(t)), Copier() { } value_ptr(T &&t) : Base(std::make_unique<T>(std::move(t))), Copier() { } // لا يكون فارغا أبدا: value_ptr() : Base(std::make_unique<T>()), Copier() { } value_ptr(empty_ptr_t) {} value_ptr(Base b, Copier c = {}) : Base(std::move(b)), Copier(std::move(c)) { } Copier const &get_copier() const { return *this; } value_ptr clone() const { return { Base( get_copier()(this->get()), this->get_deleter()), get_copier()}; } value_ptr(value_ptr &&) = default; value_ptr &operator=(value_ptr &&) = default; value_ptr(value_ptr const &o) : value_ptr(o.clone()) {} value_ptr &operator=(value_ptr const &o) { if (o && *this) { // عيّن المحتوى إن كانا فارغيْن: **this = *o; } else { // وإلا فعيِّن قيمة منسوخة: *this = o.clone(); } return *this; } value_ptr &operator=(T const &t) { if (*this) { **this = t; } else { *this = value_ptr(t); }
لا تكون قيمة المؤشّر الذكي (value_ptr) فارغة إلّا إذا أنشأته باستخدام empty_ptr_t
، أو قمت بعملية نقل (move) منه، وهذ يكشف حقيقة أنّه حصريّ (unique_ptr
)، لذلك سيعمل العامل explicit operator bool() const
عليه. يعيد التابع .get()
مرجعًا (إذ أنه لا يكاد يكون فارغًا أبدًا)، فيما يعيد التابع .get_pointer()
مؤشّرًا. ويمكن أن يكون هذا المؤشّر الذكي مفيدًا في حال كنّا نريد دلالة قيميّة (value-semantics)، لكن لا نريد الكشف عن المحتويات خارج نطاق التنفيذ. كذلك يمكن باستخدام ناسخ Copier
غير افتراضي أن نتعامل مع الأصناف الأساسية الوهمية (virtual base classes) التي تعرف كيفيّة إنتاج نُسخ من الصنف المشتق وتحويلها إلى أنواع قيميّة (value-types).
جعل المؤشرات المشتركة تشير إلى this
يتيح لك enable_shared_from_this
الحصول على نُسخة صالحة من مؤشّر مشترك يشير إلى [this](رابط الفصل 32)، وسترث تابع shared_from_this
عبر اشتقاق صنف من قالب الصنف enable_shared_from_this
، ليعيد مؤشّرا مشتركًا يشير إلى this
.
لاحظ أنه لا بدّ من إنشاء الكائن كمؤشّر مشترك (shared_ptr
):
#include <memory> class A : public enable_shared_from_this<A> { }; A *ap1 = new A(); shared_ptr<A> ap2(ap1); // تحضير مؤشّر مشتركا يشير إلى الكائن الذي يحتويه // ثم الحصول على مؤشّر مشترك يشير إلى الكائن من داخل الكائن نفسه shared_ptr<A> ap3 = ap1->shared_from_this(); int c3 = ap3.use_count(); // =2: يشير إلى نفس الكائن.
ملاحظة: لا يمكنك استدعاء enable_shared_from_this
داخل المُنشئ.
#include <memory> // enable_shared_from_this class Widget: public std::enable_shared_from_this < Widget > { public: void DoSomething() { std::shared_ptr < Widget > self = shared_from_this(); someEvent -> Register(self); } private: ... }; int main() { ... auto w = std::make_shared < Widget > (); w->DoSomething(); ... }
إذا استخدمت shared_from_this()
على كائن غير مملوك من مؤشّر مشترك، مثل كائن تلقائي محلي (local automatic object) أو كائن عام (global object)، فإنّ السلوك لن يكون محدّدًا. لكن المصرِّف أصبح يطلق الاعتراض std::bad_alloc
منذ الإصدار C++ 17.
يكافئ استخدام shared_from_this()
من داخل مُنشئ، استخدامه على كائن غير مملوك من مؤشّر مشترك، لأنّ الكائنات ستكون مملوكة للمؤشّر المشترك بعد عودة المُنشئ.
هذا الدرس جزء من سلسلة دروس عن C++.
ترجمة -بتصرّف- للفصل Chapter 33: Smart Pointers من كتاب C++ Notes for Professionals
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.