سلسلة ++c للمحترفين الدرس 17: المراجع (References) والدلالات القيمية والمرجعية في Cpp


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

تتشابه المراجع في سلوكها وتختلف عن المؤشّرات الثابتة (const pointers)، وتُعرَّف عن طريق إتباع الرمز ‎&‎ ‏‏(ampersand) باسم نوع.

int i = 10;
int &refi = i;

يمثّل ‎refi‎ في المثال أعلاه مرجعًا مربوطًا (reference bound) إلى ‎i‎، كذلك فإن المراجع تُجرِّد مفهوم المؤشرات وتتصرف كاسم للكائن المُشار إليه:

refi = 20; // i = 20;

يمكنك أيضًا تعريف عدة مراجع في تعريف واحد:

int i = 10, j = 20;
int &refi = i, &refj = j;
// خطأ شائع
// int& refi = i, k = j;

في المثال السابق ستكون refi من نوع ‎&int رغم أن k ستكون من نوع int وليس ‎&int. أيضًا، يجب تهيئة المراجع بشكل صحيح في التعريف، ولن يمكنك تعديلها بعد ذلك. انظر المثال التالي حيث تؤدي الشيفرة إلى خطأ في التصريف لأن الإعلان عن متغير المرجع i يتطلب مهيِّئًا.

int &i; 

كذلك لا يمكن ربط مرجع إلى المؤشر الفارغ ‎nullptr‎ مباشرة، على عكس المؤشرات، انظر المثال التالي حيث نحصل على خطأ لاستحالة ربط مرجع غير ثابت لقيمة يسارية من نوع int بعنصر من نوع nullptr_t:

int *const ptri = nullptr;
int &refi = nullptr; 

الدلالات القيميّة والمرجعية

يكون لنوع ما دلالة قيميّة (value semantics) إذا كانت حالة الكائن القابلة للمُلاحظة مختلفة وظيفيًا عن جميع الكائنات الأخرى من ذلك النوع. هذا يعني أنه إذا نسخت أحد الكائنات فستحصل على كائن جديد، ولن تؤثر التعديلات على الكائن الجديد بأي شكل من الأشكال على الكائن القديم. معظم أنواع C++‎ الأساسية لها دلالات قيميّة، انظر المثال التالي حيث تطبع std::cout << i الرقم 5 لأن i لم تتأثر بالتغييرات التي أجريت على j.

int i = 5;
int j = i; // منسوخ
j += 20;
std::cout << i; // j لم تتأثر بالتغييرات التي أجريناها على i تطبع 5، لأنّ 

معظم الأنواع المُعرّفة في المكتبة القياسية لها دلالة قيميّة أيضًا:

std::vector<int> v1(5, 12); // مصفوفة خماسية كل قيمها تساوي 12
std::vector<int> v2 = v1; // نسخ المتجهة
v2[3] = 6; v2[4] = 9;
std::cout << v1[3] << " " << v1[4]; //  "12 12" 

يقال إنّ نوعًا ما له دلالة مرجعيّة (reference semantics) إذا تشاركت نُسخ ذلك النوع حالتها القابلة للملاحظة مع كائنات أخرى (خارجية)، بحيث يؤدي تعديل كائن واحد إلى تغيير حالة كائن آخر، وللمؤشّرات دلالة قيميّة في C++‎ فيما يتعلّق بهويّة الكائن الذي تشير إليه، ولها دلالة مرجعيّة فيما يتعلق بحالة الكائن الذي تشير إليه:

int *pi = new int(4);
int *pi2 = pi;
pi = new int(16);
assert(pi2 != pi); // تتحقق دائما
int *pj = pi;
*pj += 5;
std::cout << *pi; // يشيران إلى نفس العنصر pj و pi تطبع 9 لأن 

كذلك فإن مراجع C++‎ لها دلالة مرجعية.

النسخ العميق ودعم النقل

إذا كنت تريد أن تجعل لنوع ما دلالة قيميّة وكان ذلك النوع يحتاج إلى تخزين الكائنات الديناميكية (dynamically allocated)، فسيحتاج ذلك النوع عند عمليات النسخ إلى تخصيص (allocate) نسخ جديدة من تلك الكائنات، كما يجب أن يفعل ذلك أيضًا عند تعيين النسخة (copy assignment). يُسمّى هذا النوع من النسخ "النسخ العميق" (deep copy) إذ أنّه يحوّل الدلالة المرجعية إلى دلالة قيمية:

struct Inner {
    int i;
};
const int NUM_INNER = 5;
class Value {
    private:
        Inner *array_; // في العادة يكون لها دلالة مرجعية
    public:
        Value(): array_(new Inner[NUM_INNER]) {}~Value() {
            delete[] array_;
        }
    Value(const Value &val): array_(new Inner[NUM_INNER]) {
        for (int i = 0; i < NUM_INNER; ++i)
            array_[i] = val.array_[i];
    }
    Value &operator = (const Value &val) {
        for (int i = 0; i < NUM_INNER; ++i)
            array_[i] = val.array_[i];
        return *this;
    }
};

الإصدار ≥ C++‎ 11

تسمح الدلالة النقليّة (Move semantics) لنوع مثل الصنف ‎Value‎ أن يتجنب نسخ بياناته المرجعية (referenced data)، وإذا استعمل المُستخدم القيمة بطريقة تؤدي إلى نقلٍ (move) فيمكن أن يؤدّي ذلك إلى تفريغ الكائن المنسوخ من البيانات التي أشار إليها:

struct Inner {
    int i;
};
constexpr auto NUM_INNER = 5;
class Value {
    private:
        Inner *array_; // في العادة يكون لها دلالة مرجعية
    public:
        Value(): array_(new Inner[NUM_INNER]) {}
        // nullptr يُسمح بالنقل حتى لو أعادت
        ~Value() {
            delete[] array_;
        }
    Value(const Value &val): array_(new Inner[NUM_INNER]) {
        for (int i = 0; i < NUM_INNER; ++i)
            array_[i] = val.array_[i];
    }
    Value &operator = (const Value &val) {
        for (int i = 0; i < NUM_INNER; ++i)
            array_[i] = val.array_[i];
        return *this;
    }
    // النقل يعني لا تخصيص للذاكرة
    // لا يمكن إطلاق اعتراض
    Value(Value &&val) noexcept: array_(val.array_) {
        // لقد أخذنا القيمة القديمة
        val.array_ = nullptr;
    }
    // لا يمكن رفع اعتراضات
    Value &operator = (Value &&val) noexcept {
        // ستُدمّر قريبا val حيلة ذكية، لأن
        // لقد استبدلنا بياناتنا ببياناته، وسيدمّر مدمّره بياناتنا
        std::swap(array_, val.array_);
    }
};

لا شك أننا نستطيع جعل مثل هذا النوع غير قابل للنسخ إن أردنا منع النسخ العميق مع السماح بنقل الكائن، انظر:

struct Inner {
    int i;
};
constexpr auto NUM_INNER = 5;
class Value {
    private:
        Inner *array_; // في العادة يكون لها دلالة مرجعية
    public:
        Value(): array_(new Inner[NUM_INNER]) {}
        // nullptr يُسمح بالنقل حتى لو أعادت
        ~Value() {
            delete[] array_;
        }
    Value(const Value &val) = delete;
    Value &operator = (const Value &val) = delete;
    // النقل يعني لا تخصيص للذاكرة
    // لا يمكن إطلاق اعتراض
    Value(Value &&val) noexcept: array_(val.array_) {
        // لقد أخذنا القيمة القديمة
        val.array_ = nullptr;
    }
    // لا يمكن رفع اعتراضات
    Value &operator = (Value &&val) noexcept {
        // ستُدمّر قريبا val حيلة ذكية، لأن
        // لقد استبدلنا  بياناتنا ببياناته، مدمّره سيدمّر بياناتنا

        std::swap(array_, val.array_);
    }
};

نستطيع أن نطبق قاعدة الصفر (Rule of Zero) من خلال استخدام مؤشّر فريد ‎unique_ptr‎:

struct Inner {
    int i;
};
constexpr auto NUM_INNER = 5;
class Value {
    private:
        unique_ptr<Inner []>array_; // نوع للنقل فقط
    public:
        Value(): array_(new Inner[NUM_INNER]) {}
        // لا داعي للحذف الصريح، أو حتى التصريح
        ~Value() =
        default; {
        delete[] array_;
    }
    //  لا داعي للحذف الصريح، أو حتى التصريح
    Value(const Value &val) =
        default;
    Value &operator = (const Value &val) =
        default;
    // تنفيذ النقل عنصرا بعنصر
    Value(Value &&val) noexcept =
        default;
    //  تنفيذ النقل عنصرا بعنصر
    Value &operator = (Value &&val) noexcept =
        default;
};

هذا الدرس جزء من سلسلة دروس عن C++‎.

ترجمة -بتصرّف- للفصل Chapter 26: References والفصل Chapter 27: Value and Reference Semantics من كتاب C++ Notes for Professionals





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


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



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

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

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


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

تسجيل الدخول

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


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