Move Semantics — C++ المتقدم
دلالات النقل (Move Semantics)
تعلمت في دورة OOP قاعدة الخمسة. الآن نفهم لماذا وُجد النقل وكيف تستفيد منه دون كتابة دوال نقل بنفسك غالباً.
المشكلة
#include <string>
#include <vector>
using namespace std;
vector<string> loadNames() { return vector<string>(1000000, "اسم"); } // مليون نص
vector<string> names = loadNames(); // قبل C++11: كان هذا قد ينسخ مليون نص!الكائن المؤقت العائد سيموت فوراً بعد السطر، فنسخه هدر. الأذكى: سرقة ذاكرته الداخلية (نقل مؤشر واحد).
lvalue و rvalue (مبسّطاً)
- lvalue: شيء له اسم وعنوان ويعيش بعد التعبير: x، names، v[0].
- rvalue: قيمة مؤقتة بلا اسم ستختفي: 42، x + 1، loadNames().
المرجع T&& يرتبط بالـ rvalues فقط. لذلك يختار المترجم:
#include <string>
using std::string;
void take(const string& s); // للـ lvalues: انسخ إن احتجت
void take(string&& s); // للمؤقتات: يمكنك سرقة محتواهاstd::move لا تنقل شيئاً!
std::move(x) مجرد تحويل نوع: "عامل x كأنه مؤقت، أنا لن أحتاجه". النقل الفعلي يحدث عندما يستقبله مُنشئ نقل أو إسناد نقل.
#include <iostream>
#include <string>
#include <utility>
#include <vector>
using namespace std;
int main() {
string a = "نص طويل جداً لا نريد نسخه";
string b = move(a); // نقل: b أخذت الذاكرة
cout << "b = " << b << '\n';
cout << "a بعد النقل: '" << a << "'\n"; // صالح لكن غير محدد القيمة (عملياً فارغ)
vector<string> list;
string item = "عنصر";
list.push_back(move(item)); // ينقل بدلاً من النسخ
}بعد النقل الكائن المصدر في حالة "صالحة لكن غير محددة": يمكنك تدميره أو إسناد قيمة جديدة له، لا تعتمد على محتواه.
متى يحدث النقل تلقائياً؟
1. من الكائنات المؤقتة: v.push_back(string("x")).
2. عند return لمتغير محلي (إن لم يحدث حذف النسخ).
3. بعد std::move صريحة.
حذف النسخ (Copy Elision)
#include <vector>
std::vector<int> make() {
return std::vector<int>(1000); // C++17: مضمون — يُبنى مباشرة في مكان المستقبل
}
std::vector<int> make2() {
std::vector<int> v(1000);
return v; // NRVO غالباً، وإلا نقل تلقائي
// return std::move(v); ← ❌ لا تفعل: يمنع NRVO
}أعد الكائنات بالقيمة بلا خوف؛ لا تكتب return std::move(local).
معاملات "خذ بالقيمة ثم انقل"
عندما تريد الدالة الاحتفاظ بنسخة من المعامل (مثل مُنشئ يخزنه):
#include <string>
#include <utility>
using namespace std;
class User {
string name;
public:
explicit User(string n) : name(move(n)) {} // نسخة واحدة للـ lvalue، صفر نسخ للمؤقت
};
// string s = "سارة"; User a(s); ← نسخة واحدة (s تبقى سليمة)
// User b(string("عمر")); ← لا نسخ أبداً: نقلتان رخيصتانnoexcept مهم هنا
دوال النقل يجب أن تكون noexcept. عند توسّع vector، إن كان مُنشئ النقل قد يرمي، يفضّل vector النسخ للحفاظ على الضمان القوي — فتخسر الفائدة كلها.
ملخص عملي
- استخدم أنواع المكتبة القياسية (Rule of Zero) — كلها تدعم النقل.
- std::move عند تمرير شيء لن تستخدمه بعد الآن.
- أعد بالقيمة.
- اكتب دوال النقل بنفسك فقط في الأصناف منخفضة المستوى التي تدير موارد، واجعلها noexcept.
تمرين
شغّل المثال الكامل وعدّ كم مرة طُبع "Copy" وكم مرة "Move". ثم أزل noexcept من مُنشئ النقل في MyVector وأضف vector<MyVector> all; for (int i = 0; i < 4; i++) all.emplace_back(10); — ماذا تغيّر في الرسائل أثناء توسّع الـ vector؟
#include <iostream>
#include <string>
#include <vector>
#include <utility>
#include <algorithm>
using namespace std;
class MyVector {
private:
int* data;
size_t size;
void log(const string& msg) const {
cout << msg << " (size=" << size << ")" << endl;
}
public:
// Constructor
MyVector(size_t n = 0) : size(n) {
data = n > 0 ? new int[n]() : nullptr;
log("📦 Constructor");
}
// Destructor
~MyVector() {
log("🗑️ Destructor");
delete[] data;
}
// Copy Constructor
MyVector(const MyVector& other) : size(other.size) {
data = new int[size];
copy(other.data, other.data + size, data);
log("📋 Copy Constructor");
}
// Copy Assignment
MyVector& operator=(const MyVector& other) {
log("📋 Copy Assignment");
if (this != &other) {
delete[] data;
size = other.size;
data = new int[size];
copy(other.data, other.data + size, data);
}
return *this;
}
// Move Constructor
MyVector(MyVector&& other) noexcept : data(other.data), size(other.size) {
other.data = nullptr;
other.size = 0;
log("🚀 Move Constructor");
}
// Move Assignment
MyVector& operator=(MyVector&& other) noexcept {
log("🚀 Move Assignment");
if (this != &other) {
delete[] data;
data = other.data;
size = other.size;
other.data = nullptr;
other.size = 0;
}
return *this;
}
size_t getSize() const { return size; }
};
MyVector createVector(size_t n) {
return MyVector(n); // C++17: حذف نسخ مضمون — يُبنى في مكانه
}
class Widget {
string name;
vector<int> data;
public:
// Constructor يستقبل by value وينقل
Widget(string n, vector<int> d)
: name(move(n)), data(move(d)) {
cout << "Widget created: " << name << endl;
}
};
int main() {
cout << "=== Copy vs Move ===" << endl;
MyVector v1(1000);
cout << "\n--- Copy ---" << endl;
MyVector v2 = v1; // Copy Constructor
cout << "\n--- Move ---" << endl;
MyVector v3 = move(v1); // Move Constructor
cout << "v1 size after move: " << v1.getSize() << endl; // 0
cout << "\n--- Function Return ---" << endl;
MyVector v4 = createVector(500); // لا نسخ ولا نقل (حذف النسخ المضمون)
cout << "\n--- Assignment ---" << endl;
MyVector v5(100);
v5 = move(v4); // Move Assignment
// ===== std::move مع STL =====
cout << "\n=== std::move مع STL ===" << endl;
vector<string> source = {"أ", "ب", "ج", "د"};
vector<string> dest;
// نقل عنصر
dest.push_back(move(source[0]));
cout << "source[0] بعد النقل: '" << source[0] << "'" << endl; // فارغ
// نقل كامل
vector<string> source2 = {"1", "2", "3"};
vector<string> dest2 = move(source2);
cout << "source2 size بعد النقل: " << source2.size() << endl;
// ===== التمرير بالقيمة ثم النقل (sink parameter) =====
cout << "\n=== Widget ===" << endl;
string name = "Widget1";
vector<int> data = {1, 2, 3, 4, 5};
Widget w1(name, data); // copies (name, data موجودين)
cout << "name بعد w1: " << name << endl; // لا يزال موجود
Widget w2(move(name), move(data)); // moves
cout << "name بعد w2: '" << name << "'" << endl; // فارغ
cout << "\n=== النهاية ===" << endl;
return 0;
}اختبر فهمك