ينصّ المعيار على ضرورة نسخ الكائنات أو نقلها في بعض المواضع من أجل تهيئتها، وإهمال النسخ (Copy elision) الذي يسمى أحيانًا تحسين القيمة المُعادة (return value optimization) هو تحسينٌ يسمح للمُصرِّف بتجنّب النسخ أو النقل في ظروف معيّنة، حتى لو كان المعيار ينصّ على ذلك. انظر الدالة التالية:
std::string get_string() { return std::string("I am a string."); }
ووفقًا للمعيار فينبغي أن تُهيِّئ هذه الدالّة سلسلةً نصيةً std::string
مؤقتة، ثم تنسخها أو تنقلها إلى الكائن المُعاد، ثم تدمّر السلسلة النصية المؤقتة، والمعيار واضح جدًا بخصوص كيفية تأويل الشيفرة.
و"إهمال النسخ" قاعدة تسمح لمصرّف C++ بتجاهل إنشاء النسخة المؤقتة ثمّ نسخها وتدميرها لاحقًا، وهذا يعني أنّ االمُصرِّف يمكن أن يأخذ تعبير التهيئة (initializing expression) الخاص بالكائن المؤقت ويهيّئ القيمة المُعادة من الدالة منه مباشرة. وهذا أفضل أداءً.
لكن رغم هذا فإن له تأثيران واضحان على المستخدم:
- يجب أن يحتوي النوع على منشئ النسخ / النقل الذي سيُستدعى، فيجب أن يكون النوع قادرًا على النسخ أو النقل حتى لو إهمال المُصرِّف النسخ / النقل.
- الآثار الجانبية لمُنشئات النسخ / النقل غير مضمونة في الظروف التي يمكن أن يحدث فيها التـَّرك، انظر المثال التالي:
الإصدار ≥ C++ 11
struct my_type { my_type() = default; my_type(const my_type & ) { std::cout << "Copying\n"; } my_type(my_type && ) { std::cout << "Moving\n"; } }; my_type func() { return my_type(); }
وستكون نتيجة استدعاء func
هي عدم طباعة "Copying" أبدًا بما أن العنصرَ المؤقتَ قيمةٌ يُمنى (rvalue) والنوع my_type
قابل للنقل (moveable type). فهل ستُطبَع إذن العبارة "Moving"؟
في الواقع، وبدون قاعدة إهمال النسخ، فإن هذا سيكون مطلوبًا دومًا لطباعة "Moving"، لكن في ظل وجود قاعدة إهمال النسخ فقد يُستدعى مُنشئ النقل (move constructor) أو لا، فالأمر يتعلّق بالتنفيذ (implementation-dependent)، وعليه لا تستطيع الاعتماد على استدعاء منشئ النسخ / النقل في السياقات التي يكون فيها إهمال النوع ممكنًا.
ونظرًا لأنّ الهدف من إهمال النوع هو تحسين الأداء، فقد لا يدعم المصرّف الإهمال في جميع الحالات، ويجب أن يدعم النوع العملية المتروكة بغض النظر عمّا إذا كان المُصرِّف سيفعّل قاعدة الإهمال في حالة معيّنة أم لا، لهذا يجب على النوع أن يحتوي مُنشئ نسخ حتّى في حال إهمال إنشاء النسخ (copy construction)، رغم أنّه لن يُستدعى على أي حال.
"إهمال النسخ" المضمون (Guaranteed copy elision)
الإصدار ≥ C++ 17
ذكرنا من قبل أن الهدف من الإهمال (elision) هو التحسين، ورغم أنّ كل المُصرِّفات تقريبًا تدعم إهمال النسخ في الحالات البسيطة، إلا أنّ إهمال النسخ لا يزال يمثّل عبئًا خاصًا على المستخدمين. فمثلًا لا يزال على النوع الذي سيُإهمال نسخه أو نقله أن يحتوي على عملية النسخ أو النقل التي ستُإهمال. مثلّا:
std::mutex a_mutex; std::lock_guard < std::mutex > get_lock() { return std::lock_guard < std::mutex > (a_mutex); }
قد يكون ذلك مفيدًا في الحالات التي يكون فيها a_mutex
كائنَ مزامنة ممسوك بشكل مخصوص (privately held) من قِبل نظام مُعيّن، لكن قد يرغب مستخدم خارجيً في تأمين قفل نطاقي (scoped lock) عليه.
لكن هذا غير جائز أيضًا، إذ لا يمكن نسخ أو نقل std::lock_guard
، وعلى الرغم من أنّ كل مصرّفات C++ تقريبًا ستهمل النسخ أو النقل، إلّا أنّ المعيار يستوجب أن يحتوي النوع على هذه العملية. كذلك فإن الإصدار C++17 يفرض إجراء عملية الإهمال عبر إعادة تعريف معاني بعض التعبيرات لمنع إجراء أي عملية نسخ أو نقل، انظر الشيفرة أعلاه.
وكانت هذه الشيفرة تنص قبل C++ 17 على إنشاء كائن مؤقت ثم استخدامه لإجراء عملية النسخ أو النقل إلى القيمة المُعادة، لكن يمكن إهمال نسخ الكائن المؤقت، أمّا في C++ 17 فلن يُنشأ كائن مؤقت على الإطلاق.
في C++ 17، عند استخدام تعبير يميني (prvalue) لتهيئة كائن من نفس نوع التعبير، فلن يُنشئ كائنًا مؤقتًا، بل سيهيّئ التعبير ذلك الكائن مباشرة، وفي حال إعادة قيمة من نفس نوع القيمة المُعادة فلن تحتاج إلى كتابة مُنشئ نسخ أو نقل، ومن ثم فيمكن أن تعمل الشيفرة أعلاه بدون مشاكل، بموجب قواعد C++ 17.
ومعلوم أن قواعد C++ 17 مناسبة في الحالات التي يتطابق فيها نوع القيمة اليمينية الخالصة (prvalue) مع النوع الذي تتم تهيئته، لذا وبالنظر إلى الدالة get_lock
أعلاه، لن تكون هناك حاجة إلى النقل أو النسخ:
std::lock_guard the_lock = get_lock();
ونظرًا لأنّ الدالة get_lock
تعيد تعبيرًا يمينيا خالصًا (prvalue) يُستخدم لتهيئة كائن من نفس النوع، فلن يحدث أي نسخ أو نقل. واعلم أن هذا التعبير لن ينشئ كائنًا مؤقتًا؛ بل سيُستخدَم لتهيئة the_lock
مباشرة. كما لا يوجد أيّ إهمال لأنّه لا يوجد نسخ / نقل ليُهمل من الأساس.
وبناء عليه يكون مصطلح "إهمال النسخ المضمون" (guaranteed copy elision) إذًا تسمية خاطئة، ولكنّه الاسم المُقترح في معيار C++ 17، وهو لا يضمن الإهمال أبدًا، وإنّما يلغي النسخ أو النقل بالكلية ويعيد تعريف C++ بحيث لا يكن هناك نسخ أو نقل لكَي يُترَك من الأساس. لا تعمل هذه الميزة إلا في الحالات التي تتضمن تعبيرًا يمينيًا خالصًا (prvalue).
وعلى هذا النحو، ستستخدم هذه الشيفرة قواعد الإهمال المعتادة:
std::mutex a_mutex; std::lock_guard < std::mutex > get_lock() { std::lock_guard < std::mutex > my_lock(a_mutex); // افعل شيئًا ما return my_lock; }
صحيح أنّ هذه الحالة صالحة لإهمال النسخ، إلّا إنّ قواعد C++ 17 لا توقف النسخ أو النقل في هذه الحالة، وعليه فيجب أن يكون للنوع مُنشئ نسخ/نقل لاستخدامه لتهيئة القيمة المُعادة. وبما أنّ lock_guard
لا تحتوي على ذلك، فسيحدث خطأ في التصريف.
ويُسمح للتنفيذات (Implementations) برفض إهمال النسخ عند تمرير أو إعادة كائن من نوع قابل للنسخ، والغرض من هذا هو السماح بنقل مثل تلك الكائنات في السجلات (registers)، التي قد تفرضها بعض واجهات التطبيقات الثنائيّة (Application binary interface أو اختصارًا ABIs) لأجل استدعائها.
struct trivially_copyable { int a; }; void foo(trivially_copyable a) {} foo(trivially_copyable {}); // إهمال النسخ ليس إجباريًا
إهمال المُعاملات (Parameter elision)
إذا مُرِّر وسيط إلى دالة وكان الوسيط تعبيرًا يمينيًا خالصًا (prvalue expression) لنوع مُعامل الدالّة ولم يكن مرجعًا، فيمكن إهمال إنشاء القيمة اليمينيّة الخالصة (prvalue).
void func(std::string str) { ... } func(std::string("foo"));
في الشيفرة أعلاه، ننشئ سلسلة نصية string
مؤقتة ثم ننقلها إلى مُعامل الدالة str
، ويسمح إهمال النسخ لهذا التعبير بإنشاء الكائن مباشرة في str
، بدلًا من استخدام نقل كائن مؤقت (temporary+move)، وهذا مفيد في تحسين الحالات التي يُصرَّح فيها عن مُنشئ صريح explicit
.
على سبيل المثال، كان بإمكاننا كتابة ما ورد أعلاه على هيئة func("foo")
، ذلك أنّ [السلاسل النصية](رابط الفصل 47) تحتوي على مُنشئ ضمني (implicit) يقوم بالتحويل من *const char
إلى string
، ولو كان ذلك المُنشئ صريحًا explicit
، لكان علينا استخدام كائن مؤقت لاستدعاء المُنشئ الصريح. ومن هذا نرى أن إهمال النسخ يُعفينا من القيام بعمليّات نسخ / نقل لا داعي لها.
إهمال القيمة المعادة (Return value elision)
إذا أعيد تعبير يميني خالص (prvalue) من دالة وكان نوع ذلك التعبير مساويًا لنوع القيمة المُعادة من الدالّة، فيمكن إهمال النسخ من القيمة اليمينيّة الخالصة المؤقتة:
std::string func() { return std::string("foo"); }
ستهمل جميع المُصرِّفات تقريبًا إنشاء الكائن المؤقت في هذه الحالة.
إهمال القيمة المُعادة المُسمّاة (Named return value elision)
إذا أعيد تعبير قيمة يساري (lvalue expression) من دالّة، وكانت تلك القيمة:
-
تمثل متغيّرًا تلقائيًا محليًا لتلك الدالّة، والذي سيُدمَّر بعد
return
، - ولم يكن المتغيّر التلقائي مُعامل دالة،
- ونوع المتغيّر يساوي نوع القيمة المُعادة من الدالّة.
ففي مثل هذه الحالة، يمكن إهمال النسخ / النقل من القيمة:
std::string func() { std::string str("foo"); // افعل شيئا ما return str; }
هناك حالات أخرى أكثر تعقيدًا مؤهّلة لقاعدة الإهمال، ولكن كلّما زاد التعقيد، قلّت احتمالية قيام المُصرِّف بتنفيذ قاعدة الإهمال:
std::string func() { std::string ret("foo"); if (some_condition) { return "bar"; } return ret; }
بإمكان المُصرِّف أن يهمل ret
، لكنّ ذلك مستبعد. أيضًا، وكما ذكرنا سابقًا، فلا يُسمح بالإهمال بالنسبة لمُعاملات القيمة.
std::string func(std::string str) { str.assign("foo"); // افعل شيئًا ما return str; // الإهمال غير ممكن }
إهمال التهيئة الاستنساخية (Copy initialization elision)
إذا كنت تستخدم تعبيرًا يمينيًا خالصًا (prvalue) لتهيئة متغيّر استنساخيًا (copy initialize)، وكان نوع ذلك المتغيّر يساوي نوع التعبير اليميني الخالص (prvalue)، فيمكن حينئذٍ إهمال النسخ.
std::string str = std::string("foo");
التهيئة النسخية تحول السطر أعلاه إلى std::string str("foo");
مع بعض الاختلافات الطفيفة. ويحدث نفس الشيء مع القيم المعادة:
std::string func() { return std::string("foo"); } std::string str = func();
ستستدعي الشيفرة أعلاه منشئ النقل الخاص بالسلاسل النصية مرّتين إذا لم تُطبَّق قاعدة إهمال النسخ، أمّا إذا طُبِّقت فسيُستدعى مرّة واحدة على الأكثر، وستختار معظم المُصرِّفات الخيار الأخير.
هذا الدرس جزء من سلسلة دروس عن C++.
ترجمة -وبتصرّف- للفصل Chapter 109: Copy Elision من الكتاب C++ Notes for Professionals
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.