تعرف على: std::move() — ليس كما يبدو
\n\nتخيل موقفًا: لقد كتبت class يدير مصفوفة كبيرة من البيانات. كل شيء يعمل، ولكن مع كل نسخ للكائن يحدث تكرار كامل لميغابايتات من المعلومات. هذا بطيء ومُبذّر. ما الحل؟ في C++11 ظهر حل أنيق — دلالات النقل وبطلها الرئيسي std::move().
في عام 2025، عندما تكون الأداء وإدارة الذاكرة الفعالة أمرًا بالغ الأهمية، يصبح فهم std::move() مهارة إلزامية لكل مطور C++. لكن المفارقة: على الرغم من الاسم، std::move() لا ينقل أي شيء. إنه فقط يجعل الكائن «جاهزًا للانتقال».
في هذه المقالة، سنشرح ما يفعله std::move() بالفعل، ومتى يجب استخدامه، ومتى من الأفضل الامتناع. استعد — سيكون هناك الكثير من أمثلة الكود ولا نظرية مملة.
ما هو std::move() في الواقع؟
\n\nدعنا نبدد الأسطورة الرئيسية فورًا. std::move() ليست دالة تنقل البيانات فعليًا من كائن إلى آخر. إنه تحويل نوع (cast) يحول lvalue إلى rvalue-reference.
إليك تنفيذها المبسط (لا تقلق، إنه بسيط):
\n\ntemplate<typename T>\ntypename std::remove_reference<T>::type&& move(T&& arg) noexcept {\n return static_cast<typename std::remove_reference<T>::type&&>(arg);\n}\n\n\nكل ما يفعله std::move() هو أخذ الكائن الخاص بك ويقول للمترجم: «انظر، هذا كائن مؤقت، يمكنك سرقته». ومن ثم تقوم المنشئات وعوامل الإسناد ذات دلالات النقل بعملها.
lvalue مقابل rvalue: شرح سريع
\n\nلفهم std::move()، يجب التمييز بين نوعين من التعبيرات:
- \n
- lvalue (قيمة يسارية) — كائن له اسم وعنوان في الذاكرة. مثال:
int x = 5;—xهو lvalue. \n - rvalue (قيمة يمنى) — كائن مؤقت «يعيش» فقط في التعبير الحالي. مثال:
5، نتيجة دالةgetTemp()، أوx + 3. \n
تعمل دلالات النقل مع rvalue: إذا كان الكائن مؤقتًا، فلماذا ننسخه؟ يمكننا ببساطة «سرقة» موارده (مؤشرات، مخازن مؤقتة، واصفات ملفات) وترك الكائن الأصلي في حالة «فارغة».
\n\nمتى تستخدم std::move(): سيناريوهات عملية
\n\nالآن بعد أن عرفنا النظرية، دعنا نستعرض الحالات الحقيقية التي يكون فيها std::move() مفيدًا.
1. تمرير الكائنات الكبيرة إلى الدوال
\n\nتخيل أن لديك class BigData يحتوي على مصفوفة من مليون عنصر. تمريرها بالقيمة دون نقل هو انتحار:
#include <iostream>\n#include <vector>\n#include <string>\n\nclass BigData {\n std::vector<int> data;\npublic:\n BigData(size_t size) : data(size) {}\n \n // منشئ النقل\n BigData(BigData&& other) noexcept : data(std::move(other.data)) {\n std::cout << "Move constructor called\\n";\n }\n \n // منشئ النسخ\n BigData(const BigData& other) : data(other.data) {\n std::cout << "Copy constructor called\\n";\n }\n};\n\nvoid processData(BigData obj) {\n // العمل مع البيانات\n}\n\nint main() {\n BigData big(1000000);\n processData(std::move(big)); // سيتم استدعاء منشئ النقل\n // بعد ذلك big في حالة "فارغة"\n return 0;\n}\n\n\nبدون std::move()، كان سيتم استدعاء منشئ النسخ مع تكرار كامل للمليون عنصر. مع النقل — فقط ننقل المؤشرات. الفرق — أسرع بآلاف المرات.