C++: دلالات النقل بعمق
آخر تحديث: 2026-08-26
في الدرس 44، أنهينا تعلم مزامنة تعدد المسارات.
الآن، سنتعمق في واحدة من أهم ميزات C++11 - دلالات النقل.
فهم دلالات النقل هو مفتاح فهم تحسين أداء C++ الحديثة حقاً.
1. نظرة عامة على دلالات النقل
(1) 1.1 لماذا نحتاج دلالات النقل؟
المشكلة: في C++98/03، نسخ الكائنات المؤقتة (القيم اليمنى) مضيعة.
مثال: نسخ كائنات مؤقتة (الصعوبة ⭐)
▶ مثال 2: استخدام حاوية STL (الصعوبة ⭐)
std::vector<int> createVector() {
std::vector<int> v = {1, 2, 3, 4, 5};
return v; // إرجاع كائن مؤقت
}
std::vector<int> dest = createVector(); // نسخ الكائن المؤقت، مضيعة!
الخرج:
Copy count: 0
حل دلالات النقل: "سرقة" موارد الكائن المؤقت بدلاً من نسخها.
(2) 1.2 القيمة اليسارية مقابل القيمة اليمنى
| الفئة | الوصف | مثال |
|---|---|---|
| قيمة يسارية | لها اسم، يمكن أخذ عنوانها | x في int x = 10; |
| قيمة يمنى | كائن مؤقت، على وشك التدمير | 10، x + 1، قيم إرجاع الدوال |
تشبيه من الحياة الواقعية:
- القيمة اليسارية = متغير مُسمّى (مثل "كتاب Zhang San")
- القيمة اليمنى = كائن مؤقت (مثل "كتاب تم شراؤه للتو" لم يُعطَ اسماً بعد)
2. مراجع القيم اليمنى
(1) 2.1 الصياغة الأساسية
تستخدم مراجع القيم اليمنى && ولا يمكنها الارتباط إلا بالقيم اليمنى.
مثال: مراجع القيم اليمنى (الصعوبة ⭐)
#include <iostream>
void process(int& x) {
std::cout << "Processing lvalue: " << x << std::endl;
}
void process(int&& x) {
std::cout << "Processing rvalue: " << x << std::endl;
}
int main() {
int a = 10;
process(a); // يستدعي process(int&)
process(20); // يستدعي process(int&&)
return 0;
}
(2) 2.2 std::move
يُستخدم std::move لتحويل قيمة يسارية إلى مرجع قيمة يمنى، مشيراً إلى "لم أعد بحاجة إلى هذا الكائن، يمكنك سرقة موارده."
مثال: استخدام std::move لنقل الموارد (الصعوبة ⭐⭐)
#include <iostream>
#include <vector>
#include <utility>
int main() {
std::vector<int> v1 = {1, 2, 3};
std::vector<int> v2 = std::move(v1); // بناء النقل: موارد v1 "سُرقت" بواسطة v2
std::cout << "v2 size: " << v2.size() << std::endl; // 3
std::cout << "v1 size: " << v1.size() << std::endl; // 0 (v1 فارغة بعد النقل)
return 0;
}
💡 نصيحة:
std::moveنفسه لا ينقل أي شيء - إنه مجرد تحويل نوع- النقل الفعلي يحدث في باني النقل أو عامل تعيين النقل
3. باني النقل وعامل تعيين النقل
(1) 3.1 لماذا نحتاج عمليات نقل مخصصة؟
المشكلة: عمليات النقل التي يُنشئها المترجم قد لا تكون فعالة (مثلاً للفئات ذات النسخ العميقة).
الحل: عرّف باني نقل مخصص وعامل تعيين نقل مخصص.
(2) 3.2 مثال: تطبيق عمليات النقل (الصعوبة ⭐⭐⭐)
#include <iostream>
#include <cstring>
class String {
private:
char* data;
size_t length;
public:
// باني
String(const char* str) {
length = strlen(str);
data = new char[length + 1];
strcpy(data, str);
std::cout << "Construct: " << data << std::endl;
}
// باني نسخ (نسخة عميقة)
String(const String& other) {
length = other.length;
data = new char[length + 1];
strcpy(data, other.data);
std::cout << "Copy construction: " << data << std::endl;
}
// باني نقل (سرقة الموارد)
String(String&& other) noexcept {
data = other.data; // سرقة المؤشر
length = other.length;
other.data = nullptr; // تعيين إلى قيمة فارغة، منع التحرير المزدوج
other.length = 0;
std::cout << "Move construction" << std::endl;
}
// مدمر
~String() {
delete data;
}
};
int main() {
String s1("Hello");
String s2 = std::move(s1); // يستدعي باني النقل
return 0;
}
الخرج:
Construct: Hello
Move construction
نتيجة التنفيذ:
Construct: Hello
Move construction
4. قواعد دلالات النقل
(1) 4.1 الخمسة الكبار
إذا كانت الفئة تحتاج إلى مدمر مخصص، أو باني نسخ، أو عامل تعيين نسخ، فغالباً تحتاج أيضاً إلى باني نقل وعامل تعيين نقل مخصصين.
| الدالة | الوصف |
|---|---|
| المدمر | تحرير الموارد |
| باني النسخ | نسخة عميقة |
| عامل تعيين النسخ | تعيين نسخة عميقة |
| باني النقل | سرقة الموارد |
| عامل تعيين النقل | تعيين سرقة الموارد |
(2) 4.2 قاعدة الصفر
أفضل ممارسة: إذا استخدمت مؤشرات ذكية لإدارة الموارد، لا تحتاج إلى تعريف أي من الخمسة الكبار (سينشئ المترجم تلقائياً إصدارات صحيحة).
class Person {
std::string name; // باستخدام string، يدعم النقل تلقائياً
std::vector<int> scores; // باستخدام vector، يدعم النقل تلقائياً
// لا حاجة لتعريف الخمسة الكبار!
}
5. التوجيه المثالي
(1) 5.1 ما هو التوجيه المثالي؟
التوجيه المثالي يعني تمرير المعاملات إلى دوال أخرى دون تغيير طبيعتها كقيمة يسارية/يمنى.
مثال: استخدام std::forward للتوجيه المثالي (الصعوبة ⭐⭐⭐)
#include <iostream>
#include <utility>
void process(int& x) {
std::cout << "Processing lvalue" << std::endl;
}
void process(int&& x) {
std::cout << "Processing rvalue" << std::endl;
}
template<typename T>
void wrapper(T&& arg) {
process(std::forward<T>(arg)); // توجيه مثالي
}
int main() {
int x = 10;
wrapper(x); // يُوجَّه كقيمة يسارية
wrapper(20); // يُوجَّه كقيمة يمنى
return 0;
}
6. مزايا أداء دلالات النقل
▶ مثال 1: مقارنة بين النسخ والنقل (الصعوبة ⭐⭐)
#include <iostream>
#include <vector>
#include <chrono>
int main() {
std::vector<std::vector<int>> v;
// اختبار النسخ
auto start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < 10000; i++) {
std::vector<int> temp(1000, 1);
v.push_back(temp); // نسخ
}
auto end = std::chrono::high_resolution_clock::now();
auto copy_time = std::chrono::duration<double>(end - start).count();
v.clear();
// اختبار النقل
start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < 10000; i++) {
std::vector<int> temp(1000, 1);
v.push_back(std::move(temp)); // نقل
}
end = std::chrono::high_resolution_clock::now();
auto move_time = std::chrono::duration<double>(end - start).count();
std::cout << "Copy time: " << copy_time << " seconds" << std::endl;
std::cout << "Move time: " << move_time << " seconds" << std::endl;
return 0;
}
الخرج:
Copy time: 0.5 seconds
Move time: 0.01 seconds
▶ مثال 3: std::move ينقل ملكية الموارد (الصعوبة ⭐)
#include <iostream>
#include <vector>
#include <string>
int main() {
std::string original = "Hello, C++!";
std::string moved = std::move(original);
std::cout << "moved: " << moved << std::endl;
std::cout << "original after move: \"" << original << "\"" << std::endl;
return 0;
}
الخرج:
moved: Hello, C++!
original after move: ""
❓ أسئلة شائعة
س: في أي حالة يكون الكائن بعد النقل؟ ج: الكائن بعد النقل يكون في حالة صالحة لكن غير محددة (عادة فارغة). يمكنك تعيين قيمة جديدة له، لكن لا يمكنك افتراض قيمته.
std::move) - إدراج عناصر الحاوية (emplace_back)📖 ملخص
| المفهوم | النقطة الأساسية |
|---|---|
| مرجع القيمة اليمنى | &&، يمكنه الارتباط فقط بالقيم اليمنى |
| std::move | يحوّل قيمة يسارية إلى مرجع قيمة يمنى |
| باني النقل | يسرق الموارد، لا نسخ |
| عامل تعيين النقل | يسرق الموارد للتعيين |
| التوجيه المثالي | std::forward، يحافظ على فئة القيمة |
📝 تمارين
-
أساسي (الصعوبة ⭐): أنشئ
vector<string>، أضف عدة نصوص باستخدامpush_back، ولاحظ الخرج لتأكيد حدوث النسخ. ثم قارن معemplace_back. -
متوسط (الصعوبة ⭐⭐): طبّق فئة
MyString(بكل من باني النسخ وباني النقل)، فعّل دلالات النقل عبرstd::moveفيmain، ولاحظ أي باني يُستدعى. -
متقدم (الصعوبة ⭐⭐⭐): طبّق نوعاً يدعم النقل فقط (مثل
unique_ptr)، عطّل باني النسخ وعامل تعيين النسخ، فعّل باني النقل وعامل تعيين النقل. اكتب كود اختبار للتحقق من أنه لا يمكن نسخه لكن يمكن نقله.
- القيمة اليسارية: تعبير له عنوان يمكن أخذه
- القيمة اليمنى: كائن مؤقت ليس له تخزين يمكن أخذ عنوانه
- std::move يحوّل قيمة يسارية إلى مرجع قيمة يمنى
- باني النقل: ينقل ملكية الموارد بدلاً من نسخها
- دلالات النقل تحسن الأداء بتجنب النسخ العميقة
الدرس التالي: المؤشرات الذكية المتقدمة (#46)