Menu
Coddy logo textTech
flag Ar iconالعربيةdown icon

دلالات النقل و Rvalues

جزء من قسم Object Oriented Programming في رحلة C++ على Coddy — الدرس 83 من 104.

في لغة ++C، كل تعبير هو إما lvalue (له هوية مستمرة، ويمكن الوصول إلى عنوانه) أو rvalue (مؤقت، وعلى وشك التدمير). فهم هذا التمييز يفتح الباب أمام move semantics - وهو تحسين قوي يتجنب عمليات النسخ غير الضرورية.

يرتبط مرجع rvalue، الذي يتم التصريح عنه بـ &&، بشكل خاص بالكائنات المؤقتة. يتيح لك هذا "سرقة" الموارد من الكائنات التي هي على وشك الاختفاء على أي حال:

#include <iostream>
#include <utility>

class Buffer {
    int* data;
    size_t size;
public:
    Buffer(size_t s) : data(new int[s]), size(s) {
        std::cout << "Constructed\n";
    }
    
    // منشئ النقل - يسرق الموارد
    Buffer(Buffer&& other) noexcept 
        : data(other.data), size(other.size) {
        other.data = nullptr;  // اترك المصدر في حالة صالحة
        other.size = 0;
        std::cout << "Moved\n";
    }
    
    ~Buffer() { delete[] data; }
};

int main() {
    Buffer b1(1000);
    Buffer b2(std::move(b1));  // يستدعي منشئ النقل
}

لا تقوم الدالة std::move بنقل أي شيء فعليًا - فهي ببساطة تقوم بتحويل lvalue إلى rvalue reference، مما يشير إلى أنك مستعد للتخلي عن موارد الكائن. عملية النقل الفعلية تحدث في move constructor أو move assignment operator.

تعمل دلالات النقل (Move semantics) على تحسين الأداء بشكل كبير عند التعامل مع الكائنات كثيفة الاستهلاك للموارد مثل الحاويات (containers) أو السلاسل النصية (strings). فبدلاً من إجراء نسخ عميق (deep copying) لميجابايت من البيانات، يمكنك ببساطة نقل ملكية المؤشر (pointer ownership) - وهي عملية ذات زمن ثابت بغض النظر عن الحجم.

challenge icon

التحدي

سهل

لنقم ببناء فئة DataBuffer لإدارة الموارد والتي توضح دلالات النقل (move semantics) بشكل عملي. سترى كيف أن نقل الموارد بدلاً من نسخها يمكن أن يحسن الكفاءة بشكل كبير عند نقل ملكية الذاكرة المخصصة ديناميكيًا.

ستقوم بتنظيم الكود الخاص بك عبر ثلاثة ملفات:

  • DataBuffer.h: قم بتعريف فئة DataBuffer التي تدير مصفوفة من الأعداد الصحيحة المخصصة ديناميكيًا.

    يجب أن تحتوي الفئة الخاصة بك على أعضاء خاصين (private) لمؤشر البيانات (int*)، والحجم (size_t)، واسم (std::string) للمساعدة في تتبع أي مخزن مؤقت هو المقصود أثناء العمليات.

    قم بالتصريح عما يلي:

    • منشئ (constructor) يأخذ اسمًا من نوع std::string وحجمًا من نوع size_t، ويخصص المصفوفة، ويطبع: [name] constructed with size [size]
    • منشئ نقل (move constructor) يأخذ مرجع rvalue، ويستولي على الموارد، ويطبع: [name] moved from [source_name] (حيث يأخذ المخزن المؤقت المنقول إليه اسم المصدر)
    • مدمر (destructor) يطبع [name] destroyed (أو empty destroyed إذا تم النقل من المخزن المؤقت)
    • دالة getSize() تعيد الحجم الحالي
    • دالة getName() تعيد اسم المخزن المؤقت

    تذكر أن تضع علامة noexcept على منشئ النقل الخاص بك وتترك الكائن المصدر في حالة فارغة صالحة (nullptr، الحجم 0، والاسم "empty").

  • DataBuffer.cpp: قم بتنفيذ جميع الدوال المصرح عنها في ملف الترويسة (header). عندما يعمل المدمر، قم بحذف البيانات فقط إذا لم يكن المؤشر فارغًا (null). قم بتضمين <iostream> للمخرجات.
  • main.cpp: اقرأ مدخلين:
    1. اسم للمخزن المؤقت الخاص بك (string)
    2. حجم للمخزن المؤقت الخاص بك (integer)

    قم بإنشاء DataBuffer بالاسم والحجم المعطيين. ثم قم بإنشاء مخزن مؤقت ثانٍ عن طريق النقل من الأول باستخدام std::move(). بعد النقل، اطبع حالة كلا المخزنين المؤقتين:

    • Original: [name] size=[size]
    • New: [name] size=[size]

    قم بتضمين <utility> من أجل std::move.

على سبيل المثال، مع المدخلات Alpha و 100:

Alpha constructed with size 100
Alpha moved from Alpha
Original: empty size=0
New: Alpha size=100
Alpha destroyed
empty destroyed

مع المدخلات Buffer و 50:

Buffer constructed with size 50
Buffer moved from Buffer
Original: empty size=0
New: Buffer size=50
Buffer destroyed
empty destroyed

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

ورقة مرجعية

تعبيرات C++ هي إما lvalues (دائمة، قابلة للعنونة) أو rvalues (مؤقتة). تعمل دلالات النقل (Move semantics) على تحسين الأداء عن طريق نقل الموارد من الكائنات المؤقتة بدلاً من نسخها.

مراجع القيم اليمينية (Rvalue References)

يتم الإعلان عنها باستخدام &&، وترتبط مراجع rvalue بالكائنات المؤقتة:

Buffer(Buffer&& other)  // Rvalue reference parameter

منشئ النقل (Move Constructor)

يقوم بسرقة الموارد من كائن مؤقت. قم بتمييزه بـ noexcept واترك المصدر في حالة فارغة صالحة:

Buffer(Buffer&& other) noexcept 
    : data(other.data), size(other.size) {
    other.data = nullptr;  // Leave source valid
    other.size = 0;
}

std::move

يقوم بتحويل lvalue إلى مرجع rvalue، مما يشير إلى الاستعداد لنقل الموارد. قم بتضمين <utility>:

Buffer b1(1000);
Buffer b2(std::move(b1));  // Triggers move constructor

لا يقوم std::move بنقل أي شيء بنفسه - يحدث النقل الفعلي في منشئ النقل أو عامل تعيين النقل (move assignment operator).

الفوائد

توفر دلالات النقل نقلاً للموارد في وقت ثابت بغض النظر عن حجم الكائن، مما يتجنب عمليات النسخ العميق المكلفة لهياكل البيانات الكبيرة.

جرّب بنفسك

#include <iostream>
#include <string>
#include <utility>
#include "DataBuffer.h"

int main() {
    std::string name;
    int size;
    
    std::cin >> name;
    std::cin >> size;
    
    // TODO: قم بإنشاء DataBuffer بالاسم والحجم المعطيين
    
    // TODO: قم بإنشاء مخزن مؤقت (buffer) ثانٍ عن طريق النقل من الأول باستخدام std::move()
    
    // TODO: اطبع حالة كلا المخزنين المؤقتين:
    // Original: [name] size=[size]
    // New: [name] size=[size]
    
    return 0;
}
quiz iconاختبر نفسك

يتضمن هذا الدرس اختبارًا قصيرًا. ابدأ الدرس للإجابة عليه وتتبّع تقدمك.

جميع دروس Object Oriented Programming