C++ / المتوسط / الدرس 8
النسخ والنقل ودلالات القيمة
افهم القيم المستقلة وانتقال الملكية دون افتراض محتويات غير محددة بعد النقل أو عدّ التحسينات ضمانات لغوية.
في هذه الصفحة
أهداف الدرس
- تمييز إنشاء النسخ والإسناد وإنشاء النقل.
- شرح ما تفعله std::move وحدود دورها.
- تطبيق قاعدة الصفر وفهم noexcept في أنواع القيمة.
النسخ ينشئ قيمة أخرى
تعني دلالات القيمة تصرف الكائن كقيمة مستقلة عند النسخ والإسناد والإتلاف. ينشئ إنشاء النسخ كائنًا جديدًا من قائم، ويستبدل إسناد النسخ قيمة كائن قائم أصلًا. تركّب حاويات الأنواع المالكة هذه العمليات طبيعيًا. ينسخ المثال الأول متجهًا ويعدّل النسخة فقط، فتظهر سلسلتان منطقيتان مستقلتان. لا تعمم ذلك على أعضاء المؤشرات الخام؛ فنسخ المؤشر ينسخ عنوانًا، ويبقى الكائن المشار إليه مشتركًا ما لم يعرّف الصنف سياسة نسخ عميق صراحة.
#include <iostream>
#include <vector>
int main() {
std::vector<int> original{3, 5};
auto copy = original;
copy[0] = 9;
std::cout << original[0] << " " << copy[0] << "\n";
}
3 9
لا يغير تعديل copy المتجه original لأن vector ينسخ عناصر int إلى تخزين مستقل الملكية.
الأسماء والموارد القابلة للنقل
يحدّد تعبير lvalue كائنًا ذا هوية مثل متغيّر مسمّى. قد يتيح rvalue عمليات مخصّصة للموارد المؤقتة أو القابلة للنقل. std::move تحويل ينتج تعبير xvalue؛ فلا ينقل بايتات بنفسه ولا يتلف كائنًا ولا يضمن اختيار عملية نقل. يظل معامل مرجع rvalue المسمى تعبير lvalue داخل دالته. وغالبًا لا يرتبط المصدر const بعملية النقل المعتادة غير الثابتة فيُنسخ بدلًا منها. افحص المنشئ المختار بدل افتراض أن كل std::move قليلة الكلفة.
استخدام الخصائص المضمونة بعد النقل فقط
يكون كائن المكتبة القياسية المنقول منه صالحًا عمومًا لكن قيمته غير محددة ما لم تمنح مواصفته ضمانًا أقوى. يمكن إتلافه والإسناد إليه، ويجب أن تستوفي العمليات الأخرى شروطها المعتادة. لا تطبع سلسلة منقولًا منها وتزعم ضمان سطر فارغ. يطبع المثال الثاني الوجهة فقط ثم يعيد ضبط المصدر صراحة قبل مراقبته. يكون unique_ptr المنقول منه فارغًا تحديدًا، لكن هذه الحقيقة لا تنطبق عمومًا على السلاسل والمتجهات والأصناف التي يعرّفها المستخدم.
#include <iostream>
#include <string>
#include <utility>
int main() {
std::string source{"report"};
std::string destination = std::move(source);
std::cout << destination << "\n";
source = "new";
std::cout << source << "\n";
}
report
new
تستقبل الوجهة report، ويعطي إسناد new المصدر قيمة معلومة؛ فلا يعتمد أي إخراج على محتويات غير محددة بعد النقل.
ترك الأعضاء المالكة تحدّد السلوك الافتراضي
فضّل الأعضاء الخاصة المولدة من المترجم للأصناف العادية التي تحتوي سلاسل وحاويات. إذا أدار صنف موردًا مباشرة ففكّر معًا في الإتلاف وإنشاء النسخ وإسناد النسخ وإنشاء النقل وإسناد النقل؛ وهذه قاعدة الخمسة. قد يحذف المالك المنفرد النسخ وينفّذ النقل أو يجعله افتراضيًا. قد يمنع التصريح بمدمّر توليد النقل ضمنيًا، فيغير مدمّر تسجيل يبدو بسيطًا الأداء. تتحدد العمليات الافتراضية وفق قدرات الأعضاء، وقد تُحذف إذا لم يدعم عضو العملية المطلوبة.
التعهد بسلوك الفشل بدقة
يساعد منشئ نقل لا يرمي فعلًا الحاويات على حفظ ضمانات قوية أثناء إعادة التخصيص. قد تنسخ التطبيقات العناصر بدل نقل يحتمل أن يرمي عند إتاحة النسخ. ضع noexcept فقط إذا كان التنفيذ كاملًا يحقق الوعد؛ إذ يستدعي الاستثناء المتسرب terminate. قد تستفيد إعادة قيمة جديدة من إنشاء prvalue المضمون منذ C++17، بينما يبقى تحسين إرجاع المتغيّر المسمى اختياريًا. تجنب عادة return std::move(local)، فقد تمنع هذا التحسين وتضيف تعقيدًا دون تحسين وضوح الملكية.
الأخطاء الشائعة وإصلاحها
توقع فراغ السلسلة المنقول منها.
- لماذا يحدث؟
- عومل تفصيل تطبيق مرصود كعقد.
- كيف تصلحه؟
- أعد ضبط المصدر قبل الاعتماد على قيمة معينة.
- كيف تتجنب تكراره؟
- اقرأ ضمان ما بعد النقل الخاص بالنوع.
نقل كائن const ما زال ينسخ.
- لماذا يحدث؟
- يحتاج منشئ النقل المعتاد مرجع مصدر غير ثابت.
- كيف تصلحه؟
- اسمح بالتعديل عند قصد النقل أو تقبل النسخ.
- كيف تتجنب تكراره؟
- استخدم const للثبات بدل تطبيق move آليًا.
نقل مفترض الأمان ينهي البرنامج عند الفشل.
- لماذا يحدث؟
- خرج استثناء من عملية noexcept.
- كيف تصلحه؟
- أزل الوعد الخاطئ أو اجعل جميع العمليات المستدعاة غير رامِية.
- كيف تتجنب تكراره؟
- راجع noexcept بصورة متعدية بما يشمل عمليات الأعضاء.
اختبر فهمك
1. بعد نقل متجه إلى آخر، تحتاج إلى استخدام المصدر مجددًا كمخزن عمل فارغ. اذكر تسلسلًا محمولًا والحقائق التي يمكن تأكيدها.
اعرض الحل المشروح
أنشئ الوجهة بالنقل ثم استدع source.clear(). يمكنك حينها تأكيد source.empty() وإلحاق عناصر عمل جديدة. لا تؤكد سعته أو حجمه السابق مباشرة بعد النقل إلا إذا نصت مواصفة العملية على ذلك. تحتوي الوجهة العناصر الأصلية المنقولة. لا تحتفظ بمؤشرات عناصر قديمة دون مراجعة قواعد العملية والمخصّص.
2. صنف يحتوي سلسلة ومتجهًا ويعرّف مدمّرًا لمسحهما فقط. اقترح تعديلًا واشرح أثره على الأعضاء الخاصة.
اعرض الحل المشروح
احذف المدمّر الزائد واعتمد على إتلاف الأعضاء. يتبع ذلك قاعدة الصفر ويسمح للمترجم بتوليد عمليات النسخ والنقل المناسبة وفق القواعد المعتادة. المسح الصريح غير ضروري لتحرير الموارد. وإذا لزم إتلاف خاص لاحقًا فراجع النسخ والنقل معًا بدل افتراض بقاء إتاحتهما كما كانت.
أهم ما تعلمته
- تتيح std::move الاختيار ولا تنفّذ الانتقال بنفسها.
- تتطلب القيم بعد النقل فهمًا خاصًا بالنوع.
- فضّل الأعضاء المولدة ووعود noexcept الصحيحة.