دلالات النقل و 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) - وهي عملية ذات زمن ثابت بغض النظر عن الحجم.
التحدي
سهللنقم ببناء فئة 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").- منشئ (constructor) يأخذ اسمًا من نوع
DataBuffer.cpp: قم بتنفيذ جميع الدوال المصرح عنها في ملف الترويسة (header). عندما يعمل المدمر، قم بحذف البيانات فقط إذا لم يكن المؤشر فارغًا (null). قم بتضمين<iostream>للمخرجات.main.cpp: اقرأ مدخلين:- اسم للمخزن المؤقت الخاص بك (string)
- حجم للمخزن المؤقت الخاص بك (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;
}
يتضمن هذا الدرس اختبارًا قصيرًا. ابدأ الدرس للإجابة عليه وتتبّع تقدمك.
جميع دروس Object Oriented Programming
1أساسيات الـ OOP
الملفات الخارجيةبناء وتجميع (Compilation) لغة C++ملفات الـ Header وملفات الـ Sourceالـ Namespaces والـ Scopeمقدمة في الـ OOP في C++الـ Classes مقابل الـ Objectsالمؤشر 'this'الـ Methods (الدوال الأعضاء)الـ Attributes (بيانات الأعضاء)أساسيات الـ Ctors والـ Dtorsمراجعة - آلة حاسبة بسيطة4خصائص الـ Class
أعضاء الـ Instance مقابل الـ Staticدوال الـ Getters والـ Settersدوال الأعضاء الثابتة (Const)الكلمة المفتاحية Mutableالدوال والمتغيرات الساكنة (Static)الدوال والفئات الصديقة (Friend)مراجعة - مدير الحساب البنكي7الوراثة
الوراثة الأساسيةمستويات الوصول في الوراثةترتيب استدعاء الـ Ctor والـ Dtorإعادة تعريف الدوال (Method Overriding)الدوال الافتراضية والـ VTableالوراثة المتعددةالوراثة الافتراضيةمراجعة - هيكلية الموظفين10نظرة عامة على STL
نظرة عامة وفلسفة STLحاويات STLالـ Iteratorsخوارزميات STLالـ Functors وتعبيرات Lambdaمراجعة - تكرار الكلمات13أنماط التصميم - الجزء الأول
مقدمة في أنماط التصميمنمط Singletonنمط Factory و Abstract Factoryنمط Builderنمط Observerنمط Strategy2إدارة الذاكرة
ذاكرة Stack مقابل Heapالمؤشرات والمراجعالذاكرة الديناميكية (new/delete)المؤشرات الذكية في C++RAII في C++مراجعة - مدير المصفوفات الديناميكية5التغليف (Encapsulation)
محددات الوصول في C++محددات الوصول بشكل متعمقإخفاء المعلوماتStruct مقابل Classالأصناف المتداخلة والداخليةمراجعة - نظام سجلات الطلاب8تعدد الأشكال (Polymorphism)
تعدد الأشكال: وقت التجميع مقابل وقت التشغيلالتحميل الزائد للدوال (Function Overloading)مراجعة الدوال الافتراضية (Virtual Functions)الدوال الافتراضية البحتة (Pure Virtual Functions)الأصناف المجردة (Abstract Classes)تصميم الواجهات (Interface) في C++التحويل الديناميكي (Dynamic Casting) و RTTIملخص - حاسبة الأشكال11مفاهيم OOP المتقدمة
التركيب مقابل الوراثةالـ Mixins عبر CRTPنمط Pimplمحو النوع (Type Erasure)أصناف Enum والأنواع القويةمعالجة الاستثناءات في OOPتسلسلات الاستثناءات المخصصة14أنماط التصميم - الجزء الثاني
نمط الأمرنمط المحولنمط المزيننمط قالب الطريقةنمط الحالةنمط التركيبRAII كنمط3المنشئات والموادم
المنشئ الافتراضيالمنشئ ذو المعاملاتمنشئ النسخمنشئ النقلقوائم تهيئة المنشئالمنشئات المفوضةتعمق في الموادمقاعدة الثلاثة / الخمسة / الصفرمراجعة - فئة String6تحميل العوامل (Operator Overloading)
مقدمة في تحميل العواملتحميل العوامل الحسابيةتحميل عوامل المقارنةعوامل Streamتحميل عامل التعيينتحميل العوامل [] و ()عوامل تحويل النوعمراجعة - Matrix Class9القوالب
قوالب الدوالقوالب الأصنافتخصيص القوالبالقوالب المتغيرةأساسيات SFINAE و Type Traitsمراجعة - الحاويات العامة12ميزات C++ الحديثة
دلالات النقل و Rvaluesالتمرير المثاليتعبيرات Lambda بعمقstd::function و std::bindconstexpr و constevalالارتباطات المهيكلةoptional و variant و any