سلسلة ++c للمحترفين الفصل 20: المؤشرات الذكية (Smart Pointers) في Cpp


محمد الميداوي

الملكية الفريدة

الإصدار ≥ 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





تفاعل الأعضاء


لا توجد أيّة تعليقات بعد



يجب أن تكون عضوًا لدينا لتتمكّن من التعليق

انشاء حساب جديد

يستغرق التسجيل بضع ثوان فقط


سجّل حسابًا جديدًا

تسجيل الدخول

تملك حسابا مسجّلا بالفعل؟


سجّل دخولك الآن