ما الذي يخفيه المصطلح الذكي "دلالات النقل (Move Semantics)"؟
\n\nتخيل أنك تنتقل إلى شقة جديدة. لديك أريكة ضخمة وثقيلة. ما الأسرع: جرّها بنفسك عبر خمسة أدوار من السلالم، أم مجرد تسليم المفاتيح لعمال النقل وقول: "يا شباب، الأريكة لكم، افعلوا بها ما تشاؤون، أنا لا أحتاجها بعد الآن"؟ الخيار الثاني هو بالضبط دلالات النقل (move semantics) في لغة C++.
\n\nقبل ظهور معيار C++11، كان المبرمجون ينسخون البيانات بشكل مؤلم. في كل مرة كنت تُرجع فيها متجهًا (vector) أو سلسلة نصية (string) من دالة، كان المترجم ينشئ نسخة طبق الأصل. إذا كان المتجه يحتوي على مليون عنصر، كان ينسخ كل عنصر بأمانة. كان هذا بطيئًا وغير فعال، ويشبه إعادة كتابة كتاب يدويًا بدلاً من إعطاء الأصل.
\n\nدلالات النقل (Move Semantics) هي آلية تسمح بنقل الموارد (الذاكرة، واصفات الملفات، المقابس) من كائن إلى آخر دون نسخ مكلف. بعد النقل، يبقى الكائن الأصلي في حالة "فارغة" ولكن صحيحة، ويمكن تدميره بأمان أو تعيين قيمة جديدة له.
\n\nفي هذه المقالة، سنشرح مراجع rvalue، ودالة std::move، ومنشئات النقل (move constructors) وعوامل الإسناد (move assignment operators) — بكلمات بسيطة وأمثلة عملية.
\n\nلماذا يبطئ الكود بدون دلالات النقل؟
\n\nلنلقِ نظرة على مثال تقليدي كان يعاني منه جميع مبرمجي C++ قبل عام 2011:
\n\n#include <iostream>\n#include <vector>\n#include <string>\n\nstd::vector<std::string> createBigVector() {\n std::vector<std::string> vec(1000000, "Hello");\n return vec; // كارثة! نسخ مليون سلسلة!\n}\n\nint main() {\n std::vector<std::string> data = createBigVector();\n std::cout << "Size: " << data.size() << std::endl;\n return 0;\n}\n\nماذا يحدث هنا؟ الدالة createBigVector تنشئ متجهًا، تملؤه بالبيانات، ثم تُرجعه. المترجم (بدون تحسينات) يفعل التالي:
- \n
- ينشئ المتجه داخل الدالة. \n
- ينسخ المتجه بأكمله إلى القيمة المُعادة. \n
- يدمر المتجه الأصلي. \n
- في
main، ينسخ هذه القيمة إلى المتغيرdata. \n
النتيجة: عمليتا نسخ كاملتان لمليون سلسلة. هذا ليس بطيئًا فحسب، بل كارثة على الأداء.
\n\nRvalue و Lvalue: عالمان في C++
\n\nلفهم دلالات النقل، يجب أن تتعرف على مفهومين أساسيين: lvalue و rvalue.
\n\n- \n
- Lvalue (قيمة يسارية) — كائن له عنوان في الذاكرة واسم. يمكنك أخذ عنوانه (
&obj). مثال: المتغيرint x = 5;—xهو lvalue. \n - Rvalue (قيمة يمينية) — قيمة مؤقتة ليس لها اسم ولا يمكن أن تكون على يسار عامل الإسناد. مثال:
5،x + 3، نتيجة دالةgetTemp(). لا يمكنك كتابة&(x + 3). \n
سابقًا، لم تكن مراجع rvalue (التي يُرمز لها بـ &&) موجودة. كان المترجم يرى كائنًا مؤقتًا ويقول: "حسنًا، هذا rvalue، الشيء الوحيد الذي يمكنني فعله هو نسخه أو تدميره". أعطت دلالات النقل للمترجم خيارًا ثالثًا: سرقة الموارد من rvalue، لأنه سيموت على أي حال قريبًا.
كيف يعمل std::move ومراجع rvalue؟
\n\nدالة std::move هي مجرد تحويل نوع (type cast). هي لا تنقل البيانات فعليًا، بل تحوّل lvalue إلى rvalue، مما يعطي المترجم الإذن "بسرقة" الكائن.
\n\nلننظر إلى كلاس بسيط يدير الذاكرة الديناميكية:
\n\n#include <iostream>\n#include <cstring>\n\nclass MyString {\nprivate:\n cha