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

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

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

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

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

#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) الأداء بشكل كبير عند التعامل مع الكائنات كثيفة الاستهلاك للموارد مثل الحاويات أو السلاسل النصية. فبدلاً من النسخ العميق لميجابايت من البيانات، تقوم ببساطة بنقل ملكية المؤشر - وهي عملية ذات زمن ثابت بغض النظر عن الحجم.

challenge icon

التحدي

سهل

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

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

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

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

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

    • منشئ (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). عندما يعمل المدمر، قم بحذف البيانات فقط إذا لم يكن المؤشر فارغًا. قم بتضمين <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

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

جرّب بنفسك

#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: اطبع حالة كلا المخزنين المؤقتين:
    // الأصلي: [name] size=[size]
    // الجديد: [name] size=[size]
    
    return 0;
}
quiz iconاختبر نفسك

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

جميع دروس Object Oriented Programming