c++ - نصوص - ما هي لغة النسخة والمبادلة؟




مترجم نصوص دقيق (4)

نظرة عامة

لماذا نحتاج إلى لغة النسخة والمبادلة؟

أي فئة تدير موردًا ( غلافًا ، مثل مؤشر ذكي) تحتاج إلى تنفيذ The Big Three . في حين أن أهداف وتطبيق منشئ النسخ و destructor هي واضحة ، يمكن القول إن مشغل نسخ الاحالة الأكثر دقة وصعوبة. كيف ينبغي أن يتم ذلك؟ ما هي المزالق التي يجب تجنبها؟

إن لغة النسخة والتقليد هي الحل ، وتساعد مشغل المشغل بشكل رائع في تحقيق أمرين: تجنب تكرار الكود ، وتوفير ضمان استثناء قوي .

كيف يعمل؟

Conceptually ، فإنه يعمل عن طريق استخدام وظيفة copy-constructor لإنشاء نسخة محلية من البيانات ، ثم يأخذ البيانات المنسوخة بوظيفة swap ، ويقارن البيانات القديمة بالبيانات الجديدة. النسخة المؤقتة ثم تدمير ، مع البيانات القديمة معها. لقد تركنا مع نسخة من البيانات الجديدة.

من أجل استخدام لغة النسخ والتقليب ، نحتاج إلى ثلاثة أشياء: منشئ نسخ عمل ، ودمار عامل (كلاهما أساس أي غلاف ، لذا يجب أن يكون كاملًا) ، ووظيفة swap .

دالة التبادل هي وظيفة غير رمي والتي تقوم بتبديل كائنين من فئة ، عضو للعضو. قد نميل إلى استخدام std::swap بدلاً من توفير الخاصة بنا ، ولكن هذا من المستحيل ؛ يستخدم std::swap مُنشئ النسخ ومشغل عامل نسخ في تطبيقه ، وسنحاول في النهاية تعريف مشغّل التخصيص بمفرده!

(ليس هذا فقط ، ولكن المكالمات غير المؤهلة swap سوف تستخدم مشغل المبادلة المخصص لدينا ، وتخطي البناء غير الضروري وتدمير صفنا الذي يستتبعه std::swap .)

شرح متعمق

الهدف

دعونا النظر في قضية ملموسة. نريد إدارة ، في فئة غير مجدية خلاف ذلك ، مصفوفة ديناميكية. نبدأ بمنشئ العمل ، ومنشئ النسخ ، و destructor:

#include <algorithm> // std::copy
#include <cstddef> // std::size_t

class dumb_array
{
public:
    // (default) constructor
    dumb_array(std::size_t size = 0)
        : mSize(size),
          mArray(mSize ? new int[mSize]() : nullptr)
    {
    }

    // copy-constructor
    dumb_array(const dumb_array& other)
        : mSize(other.mSize),
          mArray(mSize ? new int[mSize] : nullptr),
    {
        // note that this is non-throwing, because of the data
        // types being used; more attention to detail with regards
        // to exceptions must be given in a more general case, however
        std::copy(other.mArray, other.mArray + mSize, mArray);
    }

    // destructor
    ~dumb_array()
    {
        delete [] mArray;
    }

private:
    std::size_t mSize;
    int* mArray;
};

هذه الفئة تقريبًا تدير الصفيف بنجاح ، ولكنها تحتاج إلى operator= للعمل بشكل صحيح.

حل فاشل

في ما يلي كيفية ظهور تنفيذ ساذج:

// the hard part
dumb_array& operator=(const dumb_array& other)
{
    if (this != &other) // (1)
    {
        // get rid of the old data...
        delete [] mArray; // (2)
        mArray = nullptr; // (2) *(see footnote for rationale)

        // ...and put in the new
        mSize = other.mSize; // (3)
        mArray = mSize ? new int[mSize] : nullptr; // (3)
        std::copy(other.mArray, other.mArray + mSize, mArray); // (3)
    }

    return *this;
}

ونقول أننا انتهينا ؛ هذا الآن يدير صفيف ، دون تسرب. ومع ذلك ، فإنه يعاني من ثلاث مشكلات ، تم وضع علامة عليه بالتسلسل في التعليمات البرمجية كـ (n) .

  1. الأول هو اختبار التخصيص الذاتي. هذا الإختبار يخدم غرضين: إنه طريقة سهلة لمنعنا من تشغيل كود لا داعي له في مهمة ذاتية ، كما أنه يحمينا من البق الخفي (مثل حذف المصفوفة فقط لمحاولة نسخها). ولكن في جميع الحالات الأخرى ، تعمل ببساطة على إبطاء البرنامج ، وتعمل كضوضاء في الشفرة ؛ نادرا ما يحدث التنازل الذاتي ، لذلك في معظم الأحيان يكون هذا الفحص بمثابة نفايات. سيكون من الأفضل إذا كان المشغل يمكن أن تعمل بشكل صحيح دون ذلك.

  2. والثاني هو أنه يوفر فقط ضمان استثناء أساسي. إذا فشل new int[mSize] ، *this فسيتم تعديل هذا. (بمعنى ، حجم الخطأ والبيانات قد اختفت!) للحصول على ضمان استثناء قوي ، فإنه يجب أن يكون شيئًا أقرب إلى:

    dumb_array& operator=(const dumb_array& other)
    {
        if (this != &other) // (1)
        {
            // get the new data ready before we replace the old
            std::size_t newSize = other.mSize;
            int* newArray = newSize ? new int[newSize]() : nullptr; // (3)
            std::copy(other.mArray, other.mArray + newSize, newArray); // (3)
    
            // replace the old data (all are non-throwing)
            delete [] mArray;
            mSize = newSize;
            mArray = newArray;
        }
    
        return *this;
    }
    
  3. لقد توسعت الشفرة! الأمر الذي يقودنا إلى المشكلة الثالثة: تكرار الكود. عامل التخصيص لدينا بشكل فعال يكرر كل الشفرة التي كتبناها في مكان آخر ، وهذا أمر فظيع.

في حالتنا ، فإن جوهره عبارة عن سطرين فقط (التخصيص والنسخة) ، ولكن مع موارد أكثر تعقيدًا ، قد يكون هذا الشفرة بالغة التعقيد. يجب أن نسعى إلى عدم تكرار أنفسنا أبدًا.

(قد يتساءل المرء: إذا كان هناك حاجة إلى هذا الكود الكبير لإدارة مورد واحد بشكل صحيح ، فماذا لو تمكن فصلى من إدارة أكثر من واحد؟ في حين أن هذا قد يبدو مصدر قلق صحيح ، ويتطلب في الواقع عبارات catch / try غير عادية ، هذا لا يعني أن الفئة يجب أن تدير مورد واحد فقط !)

حل ناجح

كما ذكرنا ، فإن نسخة النسخة والمبادلة ستحل كل هذه القضايا. ولكن في الوقت الحالي ، لدينا جميع المتطلبات باستثناء واحد: وظيفة swap . في حين تستلزم القاعدة الثلاث بنجاح وجود مُنشئ النسخ ، ومشغل التعيين ، والمدمِّر ، يجب أن يُطلق عليه بالفعل "الثلاثة الكبار والنصف": في أي وقت يدير فصلك موردًا ، يكون من المنطقي أيضًا توفير swap وظيفة.

نحتاج إلى إضافة وظيفة مبادلة إلى فئتنا ، ونقوم بذلك على النحو التالي †:

class dumb_array
{
public:
    // ...

    friend void swap(dumb_array& first, dumb_array& second) // nothrow
    {
        // enable ADL (not necessary in our case, but good practice)
        using std::swap;

        // by swapping the members of two objects,
        // the two objects are effectively swapped
        swap(first.mSize, second.mSize);
        swap(first.mArray, second.mArray);
    }

    // ...
};

( Here هو التفسير لماذا dumb_array public friend swap .) الآن لا يمكننا فقط تبديل dumb_array لدينا ، ولكن المقايضات بشكل عام يمكن أن تكون أكثر كفاءة. فإنه مجرد تبديل المؤشرات وأحجام بدلاً من تخصيص ونسخ صفائف بالكامل. وبصرف النظر عن هذه المكافأة في الكفاءة والفعالية ، نحن الآن على استعداد لتنفيذ لغة النسخ والاستبدال.

بدون مزيد من اللغط ، فإن مشغل المهام لدينا هو:

dumb_array& operator=(dumb_array other) // (1)
{
    swap(*this, other); // (2)

    return *this;
}

وهذا كل شيء! مع ضربة واحدة ، يتم التعامل مع جميع المشاكل الثلاثة في وقت واحد.

لماذا تعمل؟

نلاحظ أولاً اختيارًا مهمًا: يتم استخدام وسيطة المعلمة عن طريق القيمة . في حين يمكن للمرء القيام بنفس السهولة التالية (وفي الواقع ، فإن العديد من التطبيقات الساذجة للكلية تفعل):

dumb_array& operator=(const dumb_array& other)
{
    dumb_array temp(other);
    swap(*this, temp);

    return *this;
}

نفقد فرصة تحسين مهمة . ليس هذا فقط ، ولكن هذا الاختيار مهم في C ++ 11 ، والذي تمت مناقشته لاحقًا. (في ملاحظة عامة ، هناك مبدأ توجيهي مفيد بشكل ملحوظ على النحو التالي: إذا كنت ستقوم بعمل نسخة من شيء ما في وظيفة ما ، دع المترجم يقوم بذلك في قائمة المعلمات. ‡)

وفي كلتا الحالتين ، فإن هذه الطريقة في الحصول على موردنا هي المفتاح لإزالة تكرار الكود: فنحن نستخدم الرمز من مُنشئ النسخ لعمل النسخة ، ولا نحتاج أبدًا إلى تكرار أي جزء منه. الآن بعد أن تم إنشاء النسخة ، نحن على استعداد لمبادلة.

لاحظ أنه عند دخول الوظيفة ، يتم تخصيص جميع البيانات الجديدة ونسخها وجاهزة للاستخدام. هذا ما يعطينا ضمان استثناء قوي مجاني: لن نقوم بإدخال الوظيفة حتى في حالة فشل إنشاء النسخة ، وبالتالي لا يمكن تغيير حالة *this . (ما فعلناه يدويًا من قبل لضمان الاستثناء القوي ، يقوم المترجم بعملنا الآن ، كيف كان نوعًا ما).

في هذه المرحلة ، نحن في المنزل ، لأن swap غير رمي. سنقوم بتبديل بياناتنا الحالية بالبيانات المنسوخة ، ونغير حالنا بأمان ، ويتم وضع البيانات القديمة في الوضع المؤقت. يتم تحرير البيانات القديمة عند إرجاع الدالة. (حيث ينتهي النطاق الخاص بالمعلمة ويتم استدعاء destructor الخاص به.)

ولأن المصطلح لا يكرر الكود ، لا يمكننا تقديم الأخطاء داخل المشغل. لاحظ أن هذا يعني أننا نتخلص من الحاجة إلى فحص تخصيص ذاتي ، مما يسمح بتنفيذ واحد موحد operator= . (بالإضافة إلى ذلك ، لم يعد لدينا عقوبة أداء على التعيينات غير الذاتية.)

وهذا هو نسخة النسخ والمبادلة.

ماذا عن C ++ 11؟

تقدم النسخة القادمة من C ++ ، C ++ 11 ، تغييرًا مهمًا جدًا لكيفية إدارة الموارد: قاعدة القاعدة الثالثة هي الآن قاعدة أربعة (ونصف). لماذا ا؟ نظرًا لأننا لا نحتاج فقط إلى القدرة على نسخ المورد ، فنحن بحاجة إلى الانتقال إليه أيضًا .

لحسن الحظ بالنسبة لنا ، هذا سهل:

class dumb_array
{
public:
    // ...

    // move constructor
    dumb_array(dumb_array&& other)
        : dumb_array() // initialize via default constructor, C++11 only
    {
        swap(*this, other);
    }

    // ...
};

ما الذي يحدث هنا؟ أذكر الهدف من الانتقال والبناء: أخذ الموارد من مثيل آخر من الصف ، وتركها في دولة مضمونة لتكون قابلة للتخصيص وقابلة للتدمير.

ما قمنا به هو بسيط: التهيئة عبر المُنشئ الافتراضي (ميزة C ++ 11) ، ثم التبديل مع other ؛ نحن نعلم أنه يمكن أن يتم تعيين وتدمير جزء من البنية الافتراضية بشكل افتراضي ، لذلك نعلم أن other سيكونون قادرين على فعل الشيء نفسه ، بعد التبادل.

(لاحظ أن بعض المترجمين لا يدعمون تفويض المُنشئ ؛ في هذه الحالة ، يجب علينا أن نعيد إنشاء الفئة يدويًا. هذه مهمة تافهة ولكنها مهمة تافهة).

لماذا هذا العمل؟

هذا هو التغيير الوحيد الذي نحتاجه لعمل صفنا ، فلماذا يعمل؟ تذكر القرار الأهم الذي اتخذناه لجعل المعلمة قيمة وليست مرجعية:

dumb_array& operator=(dumb_array other); // (1)

الآن ، إذا تم تهيئة أخرى مع rvalue ، فسيتم بناءه . في احسن الاحوال. وبنفس الطريقة التي تسمح لنا C ++ 03 بإعادة استخدام وظيفة منشئ النسخ الخاصة بنا عن طريق أخذ الوسيطة ذات القيمة ، فإن C ++ 11 سيقوم تلقائيًا باختيار منشئ الحركة عند الضرورة. (وبالطبع ، كما ذكرنا في المقال المرتبط سابقًا ، يمكن ببساطة نسخ / تحريك القيمة تمامًا).

وهكذا يخلص إلى نسخة النسخ والمبادلة.

الحواشي

* لماذا نضع mArray ؟ لأنه إذا كان أي رمز إضافي في المشغل يلقي ، يمكن استدعاء destructor من dumb_array . وإذا حدث ذلك دون تعيينه على قيمة null ، فإننا نحاول حذف الذاكرة التي تم حذفها بالفعل! نحن نتفادى ذلك من خلال تعيينه إلى قيمة خالية ، لأن حذف null هو no-operation.

† هناك ادعاءات أخرى مفادها أننا يجب أن نتخصص std::swap النوع الذي ننتجه ، وأن نقدم swap في الصف جنبا إلى جنب swap الوظائف الحرة ، إلخ. ولكن هذا كله غير ضروري: أي استخدام مناسب swap سيكون من خلال غير مؤهل الاتصال ، وسيتم العثور على ADL خلال ADL . وظيفة واحدة ستفعل.

‡ السبب بسيط: بمجرد حصولك على المورد لنفسك ، يمكنك استبداله و / أو تحريكه (C ++ 11) في أي مكان يحتاج إلى أن يكون. وبجعل النسخة في قائمة المعلمات ، يمكنك تحقيق أقصى قدر من التحسين.

https://code.i-harness.com

ما هو هذا المصطلح ومتى يجب استخدامه؟ ما هي المشاكل التي تحلها؟ هل يتغير التعبير عند استخدام C ++ 11؟

على الرغم من أنه تم ذكره في العديد من الأماكن ، إلا أنه لم يكن لدينا أي سؤال وجواب "ما هو" واحد ، لذلك هنا. فيما يلي قائمة جزئية بالأماكن التي سبق ذكرها:


أرغب في إضافة كلمة تحذير عند التعامل مع حاويات مخصصة لـ C ++ 11-نمط مخصص. التبادل والتنازل لهما دلالات مختلفة بمهارة.

من أجل الدقة ، دعنا نأخذ في الاعتبار حاوية std::vector<T, A> ، حيث A هي نوع من المخصّصين الحميميين ، وسنقارن الوظائف التالية:

void fs(std::vector<T, A> & a, std::vector<T, A> & b)
{ 
    a.swap(b);
    b.clear(); // not important what you do with b
}

void fm(std::vector<T, A> & a, std::vector<T, A> & b)
{
    a = std::move(b);
}

الغرض من كلا الوظيفتين fs و fm هو إعطاء الحالة التي كان b بها في البداية. ومع ذلك ، هناك سؤال مخفي: ماذا يحدث إذا كان a.get_allocator() != b.get_allocator() ؟ الجواب هو، فإنه يعتمد. دعنا نكتب AT = std::allocator_traits<A> .

  • إذا كان AT::propagate_on_container_move_assignment std::true_type ، فحينئذٍ يعيد تعيين fm تخصيص قيمة b.get_allocator() ، وإلا فإنه لا يستمر ويستمر في استخدام مخصصه الأصلي. في هذه الحالة ، يجب تبديل عناصر البيانات بشكل فردي ، نظرًا لأن تخزين a و b غير متوافق.

  • إذا كان AT::propagate_on_container_swap std::true_type ، std::true_type fs std::true_type كل من البيانات std::true_type المتوقعة.

  • إذا كان AT::propagate_on_container_swap std::false_type ، std::false_type إلى فحص ديناميكي.

    • إذا كان a.get_allocator() == b.get_allocator() ، عندها تستخدم a.get_allocator() == b.get_allocator() متوافقًا ، وتبادل العائدات a.get_allocator() == b.get_allocator() المعتادة.
    • ومع ذلك ، إذا كان a.get_allocator() != b.get_allocator() ، فإن البرنامج له سلوك غير معروف (راجع [container.requirements.general / 8].

النتيجة هي أن عملية التبادل أصبحت عملية غير عادية في C ++ 11 حالما تبدأ الحاوية في دعم الممولين الحاصلين على الإحالة. هذه "حالة استخدام متطورة" نوعًا ما ، ولكنها ليست مستبعدًا تمامًا ، نظرًا لأن تحسينات التحويل عادة ما تصبح مثيرة للاهتمام فقط عندما يدير فصلك موردًا ، والذاكرة هي أحد الموارد الأكثر شعبية.


هذه الإجابة أشبه بإضافات وتعديل طفيف للإجابات أعلاه.

في بعض إصدارات Visual Studio (وربما compilers الأخرى) هناك خلل مزعج حقا وغير منطقي. لذا إذا قمت بتعريف / تعريف وظيفة swap الخاص بك مثل هذا:

friend void swap(A& first, A& second) {

    std::swap(first.size, second.size);
    std::swap(first.arr, second.arr);

}

... سوف يصيح المترجم عند استدعاء الدالة swap :

وهذا له علاقة بوظيفة friend التي يتم الاتصال بها ويتم تمرير this الكائن كمعلمة.

طريقة حول هذا هو عدم استخدام الكلمة الأساسية friend وإعادة تعريف الدالة swap :

void swap(A& other) {

    std::swap(size, other.size);
    std::swap(arr, other.arr);

}

في هذه المرة ، يمكنك فقط الاتصال swap والانتقال إلى other ، مما يجعل المترجم سعيدًا:

بعد كل شيء ، لا تحتاج إلى استخدام وظيفة friend لمبادلة كائنات 2. من المنطقي أن يتم swap وظيفة العضو التي تحتوي على كائن other كمعامل.

لديك بالفعل حق الوصول إلى this الكائن ، لذلك فإن تمريره كمعلمة يعتبر تكرارًا تقنيًا.


هناك بعض الإجابات الجيدة بالفعل. سأركز بشكل رئيسي على ما أعتقد أنهم يفتقرون إليه - تفسير "السلبيات" مع لغة النسخ والتقليب ....

ما هي لغة النسخة والمبادلة؟

طريقة لتنفيذ مشغل التعيين من حيث وظيفة المبادلة:

X& operator=(X rhs)
{
    swap(rhs);
    return *this;
}

الفكرة الأساسية هي:

  • الجزء الأكثر عرضة للخطأ من التخصيص إلى كائن هو ضمان أي موارد يتم الحصول عليها احتياجات الدولة الجديدة (مثل الذاكرة ، واصفات)

  • يمكن محاولة هذا الاكتساب قبل تعديل الحالة الحالية للكائن (أي *this ) إذا تم عمل نسخة من القيمة الجديدة ، وهذا هو السبب في قبول rhs بالقيمة (أي منسوخة) بدلاً من الرجوع إليها

  • تبديل حالة نسخ rhs المحلية و *this عادة ما يكون من السهل نسبيا القيام به دون الفشل / الاستثناءات المحتملة ، بالنظر إلى أن النسخة المحلية لا تحتاج إلى أي حالة معينة بعد ذلك (تحتاج فقط إلى حالة مناسبة لتشغيل destructor ، بقدر ما ل كائن يتم نقله من في> = C ++ 11)

متى يجب استخدامها؟ (ما هي المشاكل التي تحلها [/ create] ؟)

  • عندما تريد أن يعترض المعترض غير متأثر بمهمة تطرح استثناءً ، بافتراض أن لديك أو يمكن أن تكتب swap مع ضمان استثناء قوي ، ومن الناحية المثالية لا يمكن للفشل / throw .. †

  • عندما تريد طريقة نظيفة وسهلة الفهم ، قوية لتعريف مشغل المهمة من حيث (أبسط) نسخ المنشئ ، وظائف swap و destructor.

    • إن تخصيص المهام الذاتية كنسخة وتقليب يتجنب حالات الحواف التي لا يتم إغفالها.

  • عندما تكون أي عقوبة أداء أو استخدام أعلى للموارد بشكل مؤقت يتم إنشاؤه من خلال وجود كائن مؤقت إضافي أثناء المهمة غير مهم للتطبيق الخاص بك. ⁂

† رمي swap : من الممكن بصفة عامة تبادل أعضاء البيانات بطريقة موثوقة ، بحيث تتتبع الكائنات بواسطة المؤشر ، ولكن أعضاء البيانات غير المؤشرين الذين لا يملكون مبادلة خالية من الإلقاء ، أو يجب تنفيذ التبادل مثل X tmp = lhs; lhs = rhs; rhs = tmp; X tmp = lhs; lhs = rhs; rhs = tmp; وقد يتم رمي الإنشاء أو الإنشاء أو التنازل ، ولكن لا يزال هناك احتمال للفشل في ترك بعض بيانات الأعضاء المتبادلة وغيرهم. تنطبق هذه الإمكانية حتى على std::string في C ++ 03 كما يعلق James على إجابة أخرى:

wilhelmtell: في C ++ 03 ، لا يوجد أي ذكر من الاستثناءات المحتمل طرحها بواسطة std :: string :: swap (الذي يسمى بواسطة std :: swap). في C ++ 0x، std :: string :: swap هو noexcept ويجب عدم إلقاء الاستثناءات. - جيمس ماكنيلس في 22 ديسمبر 2010 في تمام الساعة 15:24

‡ تنفيذ مشغل التعيين الذي يبدو عاقلًا عند التعيين من كائن مميز يمكن أن يفشل بسهولة للتخصيص الذاتي. في حين أنه قد يبدو من الصعب تخيل أن شفرة العميل ستحاول حتى تعيين الذات ، إلا أنه يمكن أن يحدث بسهولة نسبية أثناء عمليات algo على الحاويات ، مع x = f(x); رمز حيث f (ربما لبعض الفروع #ifdef ) ماكرو ala #define f(x) x أو دالة بإرجاع مرجع إلى x أو حتى (على الأرجح غير فعالة ولكن موجزة) رمز مثل x = c1 ? x * 2 : c2 ? x / 2 : x; x = c1 ? x * 2 : c2 ? x / 2 : x; ). فمثلا:

struct X
{
    T* p_;
    size_t size_;
    X& operator=(const X& rhs)
    {
        delete[] p_;  // OUCH!
        p_ = new T[size_ = rhs.size_];
        std::copy(p_, rhs.p_, rhs.p_ + rhs.size_);
    }
    ...
};

في التعيين الذاتي ، حذف الرمز أعلاه x.p_; نقاط p_ في منطقة الكومة المخصصة حديثًا ، ثم يحاول قراءة البيانات غير p_ في ذلك (السلوك غير المحدد) ، إذا كان ذلك لا يفعل أي شيء غريبًا للغاية ، فإن copy تحاول إجراء تكليف ذاتي لكل T فقط!

⁂ يمكن لنسخة النسخة والمبادلة أن تُظهر أوجه قصور أو قيود بسبب استخدام مؤقت إضافي (عندما تكون معلمة المشغل هي نسخة مبنية):

struct Client
{
    IP_Address ip_address_;
    int socket_;
    X(const X& rhs)
      : ip_address_(rhs.ip_address_), socket_(connect(rhs.ip_address_))
    { }
};

هنا ، قد يتحقق Client::operator= مكتوب يدويًا إذا كان *this مرتبطًا بالفعل بنفس الخادم مثل rhs (ربما يرسل رمز "إعادة التعيين" إذا كان مفيدًا) ، في حين أن منهج النسخ والمبادلة يستدعي النسخ من المرجح أن تكون مكتوبة لفتح اتصال مقبس متميز ثم إغلاق المستند الأصلي. لا يمكن أن يعني ذلك فقط تفاعل شبكة الاتصال عن بعد بدلاً من نسخة متغيرة بسيطة في العملية ، يمكن أن تتعارض مع حدود العميل أو الخادم على موارد أو اتصالات مأخذ التوصيل. (بالطبع هذا الفصل لديه واجهة مرعبة جدا ، ولكن هذه مسألة أخرى ؛- P).





copy-and-swap