اذهب إلى المحتوى

محمد بغات

الأعضاء
  • المساهمات

    177
  • تاريخ الانضمام

  • تاريخ آخر زيارة

  • عدد الأيام التي تصدر بها

    6

كل منشورات العضو محمد بغات

  1. النطاقات Scopes المتغيرات العامة Global variables إذا أردت التصريح عن نسخة واحدة من متغير ما، وكانت هذه النسخة متاحة للوصول في عدة ملفات مصدرية source files، فمن الممكن أن نجعلها في النطاق العام global scope باستخدام الكلمة المفتاحية extern، فهي تخبر المصرِّف بوجود تعريف لهذا المتغير في مكان ما داخل الشيفرة الحالية، لذا فمن الممكن استخدامه في أي مكان، كما ستُجرى جميع عمليات القراءة / الكتابة في نفس المكان من الذاكرة. يتألّف المثال التالي من عدّة ملفات مصدرية: الملف my_globals.h: #ifndef __MY_GLOBALS_H__ #define __MY_GLOBALS_H__ extern int circle_radius; // سيُعرَّف في مكان ما circle_radius تخطر المصرّف بأنّ #endif الملف foo1.cpp: #include "my_globals.h" int circle_radius = 123; // extern تعريف المتغير الخارجي الملف main.cpp: #include "my_globals.h" #include <iostream> int main() { std::cout << "The radius is: " << circle_radius << "\n"; ' return 0; } سينتج الخرج التالي: The radius is: 123 المتغيرات المضمنة Inline variables يُسمح بتعريف مُتغير مُضمّن Inline variable في عدّة وحدات ترجمة دون انتهاك قاعدة التعريف الواحد. وفي حال تعريف متغيّر مُضمّن أكثر من مرة، فسيَدمِج الرابط linker كلّ تَعاريفه في كائن واحد في البرنامج النهائي. يمكن تعريف حقل ساكن static data member في صنف إن صُرِّح عنه بأنّه مُضمّن ‎inline‎. على سبيل المثال، يمكن تعريف الصنف في المثال التالي في الترويسة. ملاحظة: قبل C++‎ 17، كان من الضروري توفير ملفّ ‎.cpp‎ لاحتواء تعريف Foo::num_instances كي نضمن ألا يُعرّف أكثر من مرّة واحدة، لكن في C++‎ 17، أصبحت مُختلف تعريفات المتغيّر المضمن ‎Foo::num_instances‎ تشير إلى نفس الكائن ‎int‎. // not thread-safe قد لا يكون متوافقا مع المسالك class Foo { public: Foo() { ++num_instances; }~Foo() { --num_instances; } inline static int num_instances = 0; }; وفي حالّة خاصة، تكون الحقول الساكنة للتعابير الثابتة constexpr‎ مضُمّنة بطريقة غير صريحة implicitly. class MyString { public: MyString() { /* ... */ } // ... static constexpr int max_size = INT_MAX / 2; }; صار التعريف constexpr int MyString::max_sizeضروريًا في كل وحدات الترجمة في إصدار C++ 1. نطاق كتلة بسيط يبدأ نطاق متغيّر ما في كتلة ‎{ ... }‎ بعد التصريح وينتهي عند نهاية الكتلة، فإذا كانت هناك كتلة متشعّبة nested block، فتستطيع الكتلة الداخلية إخفاء نطاق المتغيّر الذي صُرِّح عنه في الكتلة الخارجية. { int x = 100; // ^ // يبدأ هنا `x` نطاق // } // <- ينتهي هنا `x` نطاق في حال التصريح عن متغير جديد داخل الكتلة المُتشعِّبة، وكان ذلك المتغير يحمل اسم متغير موجود في الكتلة الخارجية، فإنه يخفي المتغير الأول الذي في الكتلة الخارجية. { int x = 100; { int x = 200; std::cout << x; // → 200 } std::cout << x; // → 100 } قاعدة التعريف الواحد ODR انتهاك قاعدة ODR عبر تحليل التحميل الزائد يمكن انتهاك ODR إذا لم يشِر البحث عن الأسماء إلى نفس الكيان حتى في حال استخدام مقاطع tokens متماثلة للتعبير عن دالة مضمّنة معيّنة. لاحظ الدالة ‎func‎ في المثال التالي: header.h void overloaded(int); inline void func() { overloaded('*'); } foo.cpp ، تشير overloaded إلى (void overloaded(char: #include "header.h" void foo() { func(); } bar.cpp، تشير overloaded إلى (void overloaded(char: void overloaded(char); // أخرى “include” قد تأتي من عبارات تضمين #include "header.h" void bar() { func(); } لدينا انتهاك هنا لقاعدة ODR، إذ يشير المقطع ‎overloaded‎ إلى عدّة كيانات مختلفة في نفس وحدة الترجمة. مضاعفة الدوال المعرفة Multiply defined function إنّ أهم نتيجة لقاعدة التعريف الواحد هي أنّ الدوال غير المضمّنة ذات الارتباط الخارجي non-inline functions with external linkage يجب ألا تُعرَّف إلا مرّة واحدة في البرنامج، لكن يمكن التصريح عنها عدّة مرات. لذا لا ينبغي تعريف مثل هذه الدوال في الترويسات، إذ يمكن إدراج الترويسة عدّة مرات من قبل عدة وحدات ترجمة مختلفة. foo.h‎: #ifndef FOO_H #define FOO_H #include <iostream> void foo() { std::cout << "foo"; } void bar(); #endif foo.cpp: #include "foo.h" void bar() { std::cout << "bar"; } main.cpp‎: #include "foo.h" int main() { foo(); bar(); } في هذا البرنامج، عُرِّفت الدالّة الأبسط ‎foo‎ في الترويسة ‎foo.h‎، والتي ضُمِّنت مرّتين: مرّة من قبل ‎foo.cpp‎ ومرّة أخرى من قبل ‎main.cpp‎، ولذلك تحتوي كل وحدة ترجمة على تعريف ‎foo‎ الخاص بها. لاحظ أنّ دروع التضمين include guards في ‎foo.h‎ لا تمنع حدوث ذلك، إذ أنّ الملفين ‎foo.cpp‎ و ‎main.cpp‎ يشتملان على ‎foo.h‎ بشكل منفصل، وعلى الأرجح سينتج عن محاولة إنشاء هذا البرنامج خطأٌ في وقت الربط link-time error بسبب تعريف ‎foo‎ أكثر من مرّة. ويجب التصريح عن الدوال في الترويسات وتعريفها في ملفات ‎.cpp‎ المقابلة لتجنّب مثل هذه الأخطاء، مع بعض الاستثناءات. الدوال المضمنة Inline functions يمكن تعريف دالّة مُضمّنة ‎inline‎ في عدّة وحدات ترجمة بشرط أن تكون جميع تَعاريفها متطابقة، ويجب أن تُعرّف أيضًا في كل وحدة ترجمة تُستخدم فيها، لذا يجب تعريف الدوال المضمّنة في الترويسات، ولا توجد حاجة إلى ذكرها في ملف التنفيذ implementation file. سيتصرّف البرنامج كما لو كان هناك تعريف واحد للدالّة. foo.h‎: #ifndef FOO_H #define FOO_H #include <iostream> inline void foo() { std::cout << "foo"; } void bar(); #endif foo.cpp: #include "foo.h" void bar() { // تعريفات أخرى أكثر تعقيدا } main.cpp‎: #include "foo.h" int main() { foo(); bar(); } في هذا المثال، عُرِّفت الدالّة ‎foo‎ بصورة مُضمّنة inline في الترويسة، أمّا الدالّة الأكثر تعقيدًا ‎bar‎ فهي غير مُضمنة، وقد عُرِّفت في ملف التقديم. وتحتوي كل من وحدتي الترجمة foo.cpp‎ و ‎main.cpp على تعريفات لـ ‎foo‎، وهذا لن يسبّب مشكلة لأنّ ‎foo‎ مضمّنة. تكون الدوالّ المُعرَّفة داخل تعريف صنف ما -والتي قد تكون دوالًا تابعة أو صديقة- مضمنة بشكل غير صريح، لذا يمكن تعريف الدوال التابعة للفصل داخل تعريف الصنف، رغم أنّه يمكن إدراج التعاريف في وحدات ترجمة متعددة: // in foo.h class Foo { void bar() { std::cout << "bar"; } void baz(); }; // in foo.cpp void Foo::baz() { // التعريف } عُرِّفت الدالّة ‎Foo::baz‎ خارج السطر، وعليه تكون غير مُضمّنة، ويجب ألا تُعُرِّف في الترويسة. البحث باسم الوسيط Argument Dependent Name Lookup عند البحث عن الدوال، تُجمع "الأصناف المرتبطة" associated classes، و"فضاءات الأسماء المرتبطة" associated namespaces التي تحقق أحد الشروط التالية، وفقًا لنوع الوسيط ‎T‎. عند البحث عن الدوال فإننا نجدها عبر جمع "الأصناف المرتبطة" associated classes أولًا، وكذلك "فضاءات الأسماء المرتبطة" associated namespaces، التي تحقق شرطًا أو أكثر من الشروط أدناه وفقًا لنوع الوسيط ‎T‎، لكن سنعرض قبل هذا القواعد الخاصة بأسماء الأصناف والتعداد وقوالب أسماء الأصناف: إذا كان ‎T‎ صنفًا مُتشعِّبا nested class، أو عضوَ تعدادٍ member enumeration، فسيُضاف الصنف المحيطة به إلى مجموعة البحث. إذا كان ‎T‎ تعدادًا -قد يكون أيضًا عضوًا من صنف-، يُضاف أعمق فضاء اسم - innermost namespace - داخله إلى مجموعة البحث. إذا كان ‎T‎ صنفًا -قد يكون مُتشعِّبًا أيضًا-، تُضاف جميع أصنافه الأساسية، وكذلك الصنف نفسه، وأعمق فضاءات الاسم في جميع الأصناف المرتبطة. إذا كان ‎T‎ من النوع ‎ClassTemplate<TemplateArguments>‎ -وهو صنف أيضًا-، تُضاف الأصناف وفضاءات الاسم المرتبطة بوسائط نوع القالب، وفضاء الاسم الخاص بوسائط قالب القالب template template argument، والصنف المحيط بوسائط قالب القالب، إذا كان وسيط القالبِ template argument قالبَ عضوٍ member template. هناك بعض القواعد التي تخصّ الأنواع المضمّنة أيضًا: إذا كان ‎T‎ مؤشّرًا يشير إلى نوع ‎U‎، أو مصفوفةً مؤلّفة من عناصر من النوع ‎U‎، تُضاف أصناف وفضاءات الأسماء المرتبطة بـ ‎U‎ إلى مجموعة البحث. في المثال: void (*fptr)(A);f(fptr);‎، تُضاف فضاءات الأسماء والأصناف المرتبطة بـ ‎void(A)‎ (انظر القاعدة التالية). إذا كان ‎T‎ نوع دالّة، تُضاف الأصناف وفضاءات الأسماء المرتبطة بأنواع المُعاملات والقيمة المعادة. في المثال: void(A)‎، تُضاف فضاءات الأسماء والأصناف المرتبطة بـ A. إذا كان ‎T‎ مؤشّرًا يشير إلى عضو، تُضاف الأصناف وفضاءات الأسماء المرتبطة بنوع ذلك العضو (قد تنطبق على كل من مؤشّرات التوابع ومؤشّرات الحقول). في مثال ‎‎B A::*p; void (A::*pf)(B); f(p);f(pf);‎؛ تُضاف فضاءات الأسماء والأصناف المرتبطة بـ A‎ و B‎ وvoid(B)‎، والتي تطبّق القواعد أعلاه التي تخصّ أنواع الدوال. يُعثَر على جميع الدوال والقوالب داخل جميع فضاءات الأسماء المرتبطة من خلال البحث باسم الوسيط، أو البحث المعتمد على الوسيط argument dependent lookup. ويُعثَر على الدوال الصديقة friend functions لنطاق الأسماء المُصرَّح عنها في الأصناف المرتبطة، والتي عادة ما تكون غير مرئية. بالمقابل، تُتجَاهل الموجّهات directives. وتكون جميع الاستدعاءات في المثال التالي صالحة حتى دون الحاجة إلى تأهيل ‎f‎ بفضاء الاسم عند الاستدعاء. namespace A { struct Z {}; namespace I { void g(Z); } using namespace I; struct X { struct Y {}; friend void f(Y) {} }; void f(X p) {} void f(std::shared_ptr < X > p) {} } // أمثلة على الاستدعاءات f(A::X()); f(A::X::Y()); f(std::make_shared < A::X > ()); g(A::Z()); // "using namespace I;"غير صالح، يُتجاهل حزم المعاملات Parameter packs قالب بحزمة معاملات template with a parameter pack template < class...Types > struct Tuple {}; حزمة المُعاملات parameter pack هي مُعاملُ قالبٍ template parameter يقبل وسيط قالب أو أكثر، أو لا يقبل على الإطلاق، ويكون القالب متغايرًا variadic إذا كان يحتوي على حزمة مُعاملات واحدة على الأقل. توسيع حزمة معاملات يُوسَّع نمط ‎parameter_pack ...‎ إلى قائمة من البدائل لحزمة ‎parameter_pack‎ مفصولة بالفاصلة الأجنبية ,، وذلك مع كل معامل من معاملاتها: template<class T> // أساس التكرار void variadic_printer(T last_argument) { std::cout << last_argument; } template<class T, class ...Args> void variadic_printer(T first_argument, Args... other_arguments) { std::cout << first_argument << "\n"; variadic_printer(other_arguments...); // توسيع حزمة المعاملات } عند استدعاء الشيفرة أعلاه مع ‎variadic_printer(1, 2, 3, "hello");‎، ستطبع: 1 2 3 hello فواصل الأرقام Digit separators من الصعب قراءة القيم العددية المؤلّفة من من أرقام كثيرة، فمثلًا: حاول نُطق 7237498123 قارن بين 237498123 وبين 237499123 من حيث التساوي. حدّد العدد الأكبر من بين 237499123 و 20249472. وقد عرَّف الإصدار ‎C‎++ 14 علامةَ الاقتباس ‎'‎ على أساس فاصلة للأرقام، بحيث تُستخدَم داخل الأعداد والقيم مصنَّفة النوع المُعرَّفة من قبل المستخدِم user-defined literals لتسهيل قراءة الأعداد الكبيرة. انظر: الإصدار ≥ C++‎ 14 long long decn = 1'000'000'000ll; long long hexn = 0xFFFF'FFFFll; long long octn = 00'23'00ll; long long binn = 0b1010'0011ll; تُتجَاهل علامة الاقتباس المُفردة عند تحديد القيمة. مثلًا: القيم مصنَّفة النوع التالية: ‎1048576‎ و ‎1'048'576‎ و ‎0X100000‎ و ‎0x10'0000‎ و ‎0'004'000'000‎ كلها لها نفس القيمة. القيم مصنفة النوع التالية ‎1.602'176'565e-19‎ و ‎1.602176565e-19‎ لهما نفس القيمة. ولا توجد أهمية تُذكر لموضع علامات الاقتباس المُفردة، فمثلًا، كل التعابير التالية متكافئة: الإصدار ≥ C++‎ 14 long long a1 = 123456789ll; long long a2 = 123'456'789ll; long long a3 = 12'34'56'78'9ll; long long a4 = 12345'6789ll; كذلك يُسمح باستخدام الفاصلة في القيم مصنَّفة النوع والمُعرّفة من المستخدم: الإصدار ≥ C++‎ 14 std::chrono::seconds tiempo = 1'674'456s + 5'300h; موازنة مع إصدارات C++‎ المختلفة اعلم أنه يمكن تنفيذ تكرار حلقي على حاوية التسلسل c في لغة ++C باستخدام الفهارس على النحو التالي: for (size_t i = 0; i < c.size(); ++i) c[i] = 0; هذه الصيغة -رغم بساطتها- عرضة لكثير من الأخطاء الشائعة، مثل استخدام عامل مقارنة خاطئ، أو متغيّر فهرسة غير صحيح: for(size_t i = 0; i <= c.size(); ++j) c[i] = 0; ^~~~~~~~~~~~~~^ كذلك يمكن التكرار على الحاويات باستخدام المُكرّرات iterators، مع عيوب مماثلة: for(iterator it = c.begin(); it != c.end(); ++it) (*it) = 0; وقد قدّمت C++‎ 11 حلقة for النطاقية، إضافة إلى الكلمة المفتاحية ‎auto‎، واللّتان سمحتا بكتابة شيفرات مثل: for(auto& x : c) x = 0; وهنا، يكون المُعاملان الوحيدان هما الحاوية ‎c‎، ومتغيّر ‎x‎ لتخزين القيمة الحالية. هذا يمنع الأخطاء المشار إليها سابقا. ووفقًا لمعيار C++‎ 11، فإنّ التنفيذ الأساسي underlying implementation يكافئ: for(auto begin = c.begin(), end = c.end(); begin != end; ++begin) { // ... } في مثل هذا التنفيذ فإن التعبير ‎auto begin = c.begin(), end = c.end();‎ يجبر ‎begin‎ و ‎end‎ على أن يكونا من نفس النوع، بينما لا تُزاد قيمة ‎end‎ ولا تحصَّل dereferenced، ومن ثم فإنّ حلقة for النطاقية لا تعمل إلا على الحاويات المُعرَّفة بواسطة زوج مُكرّر/ مُكرّر iterator/iterator. وقد خفّف معيارُ C++‎ 17 هذا القيد عن طريق تعديل التنفيذ: auto begin = c.begin(); auto end = c.end(); for(; begin != end; ++begin) { // ... } هنا يُسمح بأن تكون ‎begin‎ و ‎end‎ من نوعين مختلفين طالما أنه يمكن مقارنتهما للتحقّق من أنّهما غير متساويين. هذا يسمح بالتكرار على العديد من الحاويات مثل الحاويات المعرّفة عبر زوج مُكرّر/حارس iterator/sentinel. هذا الدرس جزء من سلسلة دروس عن C++‎. ترجمة -بتصرّف- للفصول Chapter 64: Inline variables Chapter 117: Scopes Chapter 120: One Definition Rule (ODR)‎ Chapter 122: Argument Dependent Name Lookup Chapter 129: Parameter packs Chapter 135: Digit separators Chapter 137: Side by Side Comparisons of classic C++ examples solved via C++ vs C++11 vs C++14 vs C++1 من كتاب C++ Notes for Professionals
  2. تُعرف اللغتان C وC++‎ بانّ أداءهما عال جدًّا - ويُعزى ذلك في الغالب إلى إمكانية التخصيص المكثّف للشيفرة، إذ يُسمح للمستخدم بتحسين الأداء عبر اختيار بنية الشيفرة وكيفية تنفيذها. وإن أردت تحسين الشيفرة فمن المهم أن تفهمها وتعرف كيفية استخدامها. وتشمل بعض أخطاء التحسين الشائعة ما يلي: التحسين السابق لأوانه: قد يسوء أداء الشيفرات المعقّدة بعد التحسين ممّا يضيع الوقت ويقلل الكفاءة. يجب أن تكون الأولوية هي كتابة شيفرة صحيحة وقابلة للصيانة، وليس شيفرة مُحسَّنة. التحسين في الحالات غير المناسبة: إضافة تحميل زائد لأجل حالة احتمال حدوثها 1% لا يستحقّ أن تبطئ لأجله 99% من الحالات الأخرى. التحسين المصغّر Micro-optimization: تقوم المُصرِّفات بهذا الأمر بفعالية كبيرة، كما أن التحسين المصغَّر يمكن أن يضرّ بقدرة المُصرِّفات على زيادة تحسين الشيفرة. وبعض أهداف التحسين النموذجية ما يلي: تقليل كمية العمل المطلوبة. استخدام خوارزميات / بنيات أكثر فاعلية. استغلال العتاد بشكل أفضل. لكن قد تكون لعمليات التحسين تأثيرات جانبية سلبية، مثل: الاستخدام المكثّف للذاكرة. تعقيد الشيفرة وصعوبة قراءتها أو صيانتها. تعقيد الواجهات البرمجية API وتصميمات الشيفرة. على أي حال، غالبًا ما يُعدِّل المُصرِّفُ البرنامجَ عند التصريف لتحسين أدائه، وهذا مسموح به وفقًا لقاعدة "كما لو" as-if rule. والتي تسمح بإجراء التغييرات والتحويلات التي لا تغيّر السلوك الملاحظ للبرنامج. تحسين الصنف الأساسي الفارغ Empty Base Class Optimization لا يمكن أن يشغل كائن ما أقل من بايت واحد، وإلّا لكانت أعضاء المصفوفات التي تضم عناصر من نفس النوع تحتل العنوان نفسه. لهذا، تكون ‎sizeof(T)>=1‎ صحيحة دائما. وكذلك لا يمكن أن يكون الصنف المشتق أصغر من صنفه الأساسي base class. لكن إن كان الصنف الأساسي فارغًا، فلن يُضاف حجمه بالضرورة إلى الصنف المشتق: class Base {}; class Derived: public Base { public: int i; }; وليس من الضروري هنا تخصيص بايت للصنف ‎Base‎ داخل ‎Derived‎ حتى يحتلّ كل كائن -بغض النظر عن نوعه- عنوانًا مميّزًا. وفي حال إجراء تحسين للصنف الأساسي الفارغ -ولم تكن هناك حاجة إلى الحشو padding-، فستكون العبارة ‎sizeof(Derived) ==sizeof(int)‎ صحيحة، أي أنّه لن يُجرى أيّ تخصيص إضافي للصنف الفارغ. هذا ممكن حتّى مع الأصناف الأساسية base classes المتعددة، إذ لا يمكن لعدّة أًصناف أساسية في ++C أن يكون لها نفس النوع، وعليه فلن تنشأ مشكلة من ذلك. لاحظ أنّه لا يمكن إجراء ذلك إلا إذا كان نوع العضو الأول من ‎Derived‎ مختلفًا عن كلّ الأصناف الأساسية، وهذا يشمل كل الأصناف الأساسية المشتركة بشكل مباشر أو غير مباشر. وإذا كان له نفس نوع أحد أصنافه الأساسية -أو كانت هناك صنف أساسية مشترك)- فيلزم على الأقل تخصيص بايت واحد لضمان ألّا يحتلّ كائنان مختلفان من نفس النوع العنوان نفسه. التحسين عن طريق تقليل الشيفرة المنفذة الأسلوب الأوضح للتحسين هو تقليل الشيفرة المنفّذة، فعادة ما يسرِّع التنفيذَ دون تغيير التعقيد الزمني للشيفرة. لكن رغم أنّ هذه الطريقة تسرّع البرنامج، فلن تظهر فائدتها إلّا في الشيفرات التي تُستدعى كثيرًا. هذه بعض الأمثلة على تقليل الشيفرات المنفّذة: إزالة الشيفرات عديمة الفائدة void func(const A *a); // دالة ما // تخصيص غير ضروري للذاكرة + إلغاء تخصيص النسخة auto a1 = std::make_unique<A>(); func(a1.get()); // استخدام المكدّس يمنع auto a2 = A{}; func(&a2); الإصدار ≥ C++‎ 14 منذ الإصدار C++‎ 14، يُسمح للمُصرِّفات بتحسين هذه الشيفرة لإزالة تخصيص الذاكرة، ومطابقة التحرير matching deallocation. تنفيذ الشيفرة مرة واحدة فقط std::map<std::string, std::unique_ptr<A>> lookup; // إدارج/بحث بطيئ // داخل هذه الدالة، سنمر على القاموس مرّتين لأجل البحث عن العنصر، ومرّة ثالثة إن لم يكن موجودا const A *lazyLookupSlow(const std::string &key) { if (lookup.find(key) != lookup.cend()) lookup.emplace_back(key, std::make_unique<A>()); return lookup[key].get(); } // داخل هذه الدالة، سيحصل نفس التأثير الذي يحدث في النسخة الأبطأ لكن مع مضاعفة السرعة // لأنّنا سنمر على القاموس مرة واحدة فقط const A *lazyLookupSlow(const std::string &key) { auto &value = lookup[key]; if (!value) value = std::make_unique<A>(); return value.get(); } يمكن استخدام أسلوب مشابه للتحسين عبر تنفيذ implement إصدار مستقر من ‎unique‎: std::vector<std::string> stableUnique(const std::vector<std::string> &v) { std::vector<std::string> result; std::set<std::string> checkUnique; for (const auto &s : v) { // يُعاد الإدراج إن كان ناجحا، لذلك يمكن أن نعرف ما إذا كان العنصر موجودًا أم لا // هذا يمنع الإدراج، والذي سيمر على القاموس مرّة لكل عنصر فريد // على عناصر مكرّرة v يُكسِبنا هذا حوالي نصف الوقت إن لم تحتو if (checkUnique.insert(s).second) result.push_back(s); } return result; } منع عمليات إعادة التخصيص والنسخ أو النقل عديمة الفائدة منعنا في المثال السابق عمليّات البحث في std::set، بيْد أنّ هذا غير كافٍ، فما يزال المتجه ‎std::vector‎ يحتوي على خوارزمية مكلّفة ونامية ستحتاج إلى نقل ذاكرتها، يمكن منع هذا عبر حجز الحجم المناسب. في المثال التالي، نضمن عدم حدوث أي نسخ أو نقل في المتجه لأن سعته تساوي الحد الأقصى للأعداد التي سندرجها، وإن افترضنا عدم حدوث أي تخصيص من الحجم صفر، وأن تخصيص كتلة كبيرة من الذاكرة يأخذ نفس الوقت الذي يأخذه تخصيص حجم صغير فهذا لن يبطئ البرنامج. ملاحظة: يمكن أن تنتبه المصرفات لهذا الأمر وتحذف شيفرة التحقق من الشيفرة المولَّدة. std::vector<std::string> stableUnique(const std::vector<std::string> &v) { std::vector < std::string > result; result.reserve(v.size()); std::set < std::string > checkUnique; for (const auto &s : v) { // انظر المثال أعلاه if (checkUnique.insert(s).second) result.push_back(s); } return result; } استخدام حاويات أكفأ استخدام بنيات البيانات الصحيحة في الوقت المناسب يمكن أن يحسّن التعقيد الزمني للشيفرة، انظر المثال التالي حيث يساوي تعقيد stableUnique هنا (N log(N، حيث N أكبر من عدد العناصر في v: // log(N) > insert complexity of std::set std::vector<std::string> stableUnique(const std::vector<std::string> &v) { std::vector<std::string> result; std::set<std::string> checkUnique; for (const auto &s : v) { // انظر فقرة التحسين عبر تقليل الشيفرة المنفّذة if (checkUnique.insert(s).second) result.push_back(s); } return result; } يمكننا تخفيض تعقيد التقديم إلى N عبر استعمال حاوية تستخدم تنفيذًا مختلفًا لتخزين عناصرها، مثل قاموس hash بدلًا من شجرة tree. وهناك أثر جانبي إضافي أيضًا، وهو تقليل استدعاء مُعامل المقارنة على السلاسل النصية، لأنّنا لن نحتاج إلى استدعائها إلّا في حال كانت ستُدرج السلسلة النصية في نفس المجموعة. انظر المثال التالي حيث يكون تعقيد stableUnique مساويًا لـ (N log (N، حيث N أكبر من عدد العناصر في v: // أكبر من 1 std::unordered_set تعقيد الإدراج في std::vector<std::string> stableUnique(const std::vector<std::string> &v) { std::vector<std::string> result; std::unordered_set<std::string> checkUnique; for (const auto &s : v) { // انظر فقرة التحسين عبر تقليل الشيفرة المنفّذة if (checkUnique.insert(s).second) result.push_back(s); } return result; } تحسين الكائنات الصغيرة Small Object Optimization تحسين الكائنات الصغيرة هي تقنية تُستخدم في بنيات البيانات منخفضة المستوى، مثل ‎std::string‎ (يشار إليها أحيانًا باسم تحسين السلاسل النصية القصيرة/الصغيرة)، ويُقصد بها استخدام مساحة مكدّس كمخزن مؤقت buffer بدلًا من استخدام ذاكرة مخصّصة عندما يكون المحتوى صغيرًا بدرجة كافية لتسَعَه المساحة المحجوزة. وسنحاول منع تخصيص مساحة في الكومة heap، لكنّ ثمن هذا هو إضافة حمل إضافي overhead على الذاكرة، وإجراء بعض الحسابات الإضافية. تعتمد فوائد هذه التقنية على نوع الاستخدام، ويمكن أن تضرّ بالأداء في حال استخدامها بشكل غير صحيح. هذا مثال على ذلك، الطريقة التالية ساذجة لكتابة سلسلة نصية باستخدام هذا التحسين: #include <cstring> class string final { constexpr static auto SMALL_BUFFER_SIZE = 16; bool _isAllocated{false}; ///< تذكّر إذا كنّا قد خصّصنا الذاكرة char *_buffer{nullptr}; ///< مؤشر يشير إلى المخزن المؤقت الذي نستخدمه char _smallBuffer[SMALL_BUFFER_SIZE]= {'\0'}; ///< SMALL OBJECT مساحة المكدّس المُستخدمة لأجل الكائن الصغير OPTIMIZATION public: ~string() { if (_isAllocated) delete [] _buffer; } explicit string(const char *cStyleString) { auto stringSize = std::strlen(cStyleString); _isAllocated = (stringSize > SMALL_BUFFER_SIZE); if (_isAllocated) _buffer = new char[stringSize]; else _buffer = &_smallBuffer[0]; std::strcpy(_buffer, &cStyleString[0]); } string(string &&rhs) : _isAllocated(rhs._isAllocated) , _buffer(rhs._buffer) , _smallBuffer(rhs._smallBuffer) //< غير ضروري عند التخصيص { if (_isAllocated) { // منع الحذف المزدوج للذاكرة rhs._buffer = nullptr; } else { // نسخ البيانات std::strcpy(_smallBuffer, rhs._smallBuffer); _buffer = &_smallBuffer[0]; } } // حَذفنا التوابع الأخرى، مثل منشئ النسخ ومعامل الإسناد، لأجل تحسين القراءة والوضوح }; كما ترى في الشيفرة أعلاه، فقد أُضيفت بعض التعقيدات الإضافية لمنع تنفيذ بعض عمليات ‎new‎ و ‎delete‎. وأصبح لدى الصنف مساحة أكبر للذاكرة، وقد لا يستخدَمها إلا في بضع حالات فقط. غالبًا ما سيُحاول ترميزَ القيمة البوليانية ‎_isAllocated‎ داخل المؤشّر ‎_buffer‎ عبر معالجة البتّات من أجل تقليل حجم النُسخة (intel 64 bit: يمكن أن يقلّل الحجم بمقدار 8 بايت)، لكن هذا التحسين غير ممكن إلا إن كانت قواعد المحاذاة الخاصّة بالمنصة معروفة. حالات الاستخدام نظرًا لأنّ هذا التحسين يضيف الكثير من التعقيد، فليس من المستحسن استخدامه في كل الأصناف. وستُصادفه غالبًا في بنيات البيانات منخفضة المستوى شائعة الاستخدام. قد تجد بعض الاستخدامات في ‎std::basic_string<>‎ و ‎std::function<>‎ في تطبيقات implementations المكتبة القياسية الشائعة في C++‎ 11. وبما أن هذا التحسين لا يمنع تخصيصات الذاكرة إلا عندما تكون البيانات المُخزّنة أصغر من القيمة المضافة، فستظهر فائدته بالخصوص في الأصناف التي تُستخدم غالبًا مع البيانات الصغيرة. لكن هناك عيب فيه وهو الحاجة إلى جهد إضافي عند تحريك المخزن المؤقت، وهذا يجعل عمليّات النقل أكثر كلفة موازنة بالحالات التي لا يُستخدم المخزن المؤقّت فيها، خاصة عندما يحتوي المخزن المؤقّت على نوع بيانات غير قديم non-POD type. توسيع التضمين والتضمين توسيع التضمين Inline expansion، أو التضمين inlining وحسب، هي تقنية تحسين يستخدمها المُصرِّف تقوم على تعويض استدعاء دالة بمتن تلك الدالّة. هذا يقلّل من الحمل الزائد overhead لاستدعاء الدوالّ، ولكن على حساب الذاكرة، إذ أنّ الدالّة قد تُكرّر عدّة مرات. // :المصدر int process(int value) { return 2 * value; } int foo(int a) { return process(a); } // :البرنامج بعد التضمين int foo(int a) { return 2 * a; // foo() في process() نسخ متن } تُستخدم تقنية التضمين غالبًا مع الدوال الصغيرة، حيث يكون الحمل الزائد لاستدعاء الدالّة صغيرًا مقارنةً بحجم جسم الدالّة. تحسين الصنف الأساسي الفارغ Empty base optimization يجب أن لا يقلّ حجم أي كائن أو عضو من كائن فرعي عن القيمة 1، حتى لو كان ذلك النوع فارغًا (أي حتى لو لم يحتوٍ الصنف ‎class‎ أو البنية ‎struct‎ على أعضاء بيانات غير ساكنة)، وذلك لأجل ضمان أن تكون عناوين الكائنات المختلفة التي تنتمي لنفس النوع مختلفة دائمًا. بالمقابل، فإنّ القيود على الكائنات الفرعية من الصنف الأساسي base class subobjects أقل بكثير، ويمكن تحسينها بشكل كامل انطلاقًا من مخطط الكائن: #include <cassert> struct Base {}; // صنف فارغ struct Derived1: Base { int i; }; int main() { // حجم أيّ كائن من صنف فارغ يساوي 1 على الأقل assert(sizeof(Base) == 1); // تطبيق تحسين الصنف الفارغ assert(sizeof(Derived1) == sizeof(int)); } يُستخدم تحسين الصنف الفارغ عادةً في أصناف المكتبة القياسية الواعية بتخصيص كل من الذاكرة: std::vector‎. std::function. std::shared_ptr. أو غيرها، وذلك لتجنّب شغل أي مساحة تخزين إضافية من قبل عضو مُخصِّص allocator member إذا كان ذلك المُخصّص عديم الحالة stateless. ويمكن تحقيق ذلك عن طريق تخزين أحد أعضاء البيانات المطلوبة، مثل: مؤشّرات ‎begin‎ و ‎end‎، أو ‎capacity‎ الخاصّة بالمتجهات. التشخيص Profiling التشخيص باستخدام gcc وgprof يتيح لك برنامج التشخيص gprof الخاصّ بـ GNU gprof تشخيص شيفرتك. ولأجل لاستخدامه، عليك إنجاز الخطوات التالية: ابنِ التطبيق مع الإعدادات المناسبة لتوليد معلومات التشخيص. قم بتوليد معلومات التشخيص عن طريق تشغيل التطبيق المبني. اعرض معلومات التشخيص المُولّدة باستخدام أداة gprof. أضف راية ‎-pg‎ لأجل بناء التطبيق مع إعدادات توليد معلومات التشخيص المناسبة، انظر: $ gcc -pg *.cpp -o app أو: $ gcc -O2 -pg *.cpp -o app وهكذا دواليك. بمجرد إنشاء التطبيق (نفترض أنّ اسمه ‎app‎)، نفِّذه كالمعتاد: $ ./app من المفروض أن ينتج هذا الأمرُ ملفًّا يُسمّى ‎gmon.out‎. إن أردت رؤية نتائج التشخيص، نفّذ الأمر التالي: $ gprof app gmon.out (لاحظ أننا وقرنا كلًّا من التطبيق وكذلك الخرج الناتج). ويمكنك توجيه الخرج أو إعادة توجيهه: $ gprof app gmon.out | less يجب أن تكون نتيجة الأمر الأخير مطبوعة على شكل جدول، ستمثّل صفوفهُ الدوالَّ، أمّا أعمدته فتشير إلى عدد الاستدعاءات، وإجمالي الوقت المستغرق، والوقت الذاتي المستغرق - self time spent - (أي الوقت المستغرق داخل الدالة ما عدا استدعاءات الدوال الداخلية). إنشاء مخططات الاستدعاءات callgraph بواسطة gperf2dot بالنسبة للتطبيقات الكبيرة، فقد يصعب فهم تنسيق التشخيص المُستَوي flat execution profiles، لهذا تنتج العديد من أدوات التشخيص رسومًا تخطيطية لتسهيل قراءتها. تحوّل أداة gperf2dot خرْج النص الذي تنتجه العديد من المُشخِّصات، مثل: Linux perf، وcallgrind، وoprofile إلى رسم تخطيطي، يمكنك استخدام هذه الأداة عن طريق تشغيل المشخّص profiler الذي تعمل به مثل ‎gprof‎: # التصريف مع رايات التشخيص g++ *.cpp -pg # توليد بيانات التشخيص ./main # ترجمة بيانات التشخيص إلى نص، وإنشاء صورة gprof ./main | gprof2dot -s | dot -Tpng -o output.png تشخيص استخدام وحدة المعالجة المركزية بواسطة gcc وGoogle Perf توفّر أدوات Google Perf برنامج تشخيص لوحدة المعالجة المركزية CPU، مع واجهة استخدام سهلة نسبيًا. لاستخدامها عليك: تثبيت أدوات Google Perf . تصريف الشيفرة كالمعتاد. إضافة مكتبة التشخيص ‎libprofiler‎ إلى مسار تحميل المكتبات في وقت التشغيل. استخدم ‎pprof‎ لتوليد تشخيص نصّي، أو رسم تخطيطي. مثلّا: g++ -O3 -std=c++11 main.cpp -o main # تنفيذ المُشخِّص LD_PRELOAD=/usr/local/lib/libprofiler.so CPUPROFILE=main.prof CPUPROFILE_FREQUENCY=100000 ./main حيث: تشير ‎CPUPROFILE‎ إلى ملف بيانات التشخيص، و تشير ‎CPUPROFILE_FREQUENCY‎ إلى تردّد عيّنات المشخّص profiler sampling frequency. إن أردت إجراء المعالجة اللاحقة post-process لبيانات التشخيص، فاستخدم ‎pprof‎، ويمكنك إنشاء تشخيص استدعاءات flat call profile على هيئة نصّ: $ pprof --text ./main main.prof PROFILE: interrupts/evictions/bytes = 67/15/2016 pprof --text --lines ./main main.prof Using local file ./main. Using local file main.prof. Total: 67 samples 22 32.8% 32.8% 67 100.0% longRunningFoo ??:0 20 29.9% 62.7% 20 29.9% __memmove_ssse3_back /build/eglibc-3GlaMS/eglibc-2.19/string/../sysdeps/x86_64/multiarch/memcpy-ssse3-back.S:1627 4 6.0% 68.7% 4 6.0% __memmove_ssse3_back /build/eglibc-3GlaMS/eglibc-2.19/string/../sysdeps/x86_64/multiarch/memcpy-ssse3-back.S:1619 3 4.5% 73.1% 3 4.5% __random_r /build/eglibc-3GlaMS/eglibc-2.19/stdlib/random_r.c:388 3 4.5% 77.6% 3 4.5% __random_r /build/eglibc-3GlaMS/eglibc-2.19/stdlib/random_r.c:401 2 3.0% 80.6% 2 3.0% __munmap /build/eglibc-3GlaMS/eglibc-2.19/misc/../sysdeps/unix/syscall-template.S:81 2 3.0% 83.6% 12 17.9% __random /build/eglibc-3GlaMS/eglibc-2.19/stdlib/random.c:298 2 3.0% 86.6% 2 3.0% __random_r /build/eglibc-3GlaMS/eglibc-2.19/stdlib/random_r.c:385 2 3.0% 89.6% 2 3.0% rand /build/eglibc-3GlaMS/eglibc-2.19/stdlib/rand.c:26 1 1.5% 91.0% 1 1.5% __memmove_ssse3_back /build/eglibc-3GlaMS/eglibc-2.19/string/../sysdeps/x86_64/multiarch/memcpy-ssse3-back.S:1617 1 1.5% 92.5% 1 1.5% __memmove_ssse3_back /build/eglibc-3GlaMS/eglibc-2.19/string/../sysdeps/x86_64/multiarch/memcpy-ssse3-back.S:1623 1 1.5% 94.0% 1 1.5% __random /build/eglibc-3GlaMS/eglibc-2.19/stdlib/random.c:293 1 1.5% 95.5% 1 1.5% __random /build/eglibc-3GlaMS/eglibc-2.19/stdlib/random.c:296 1 1.5% 97.0% 1 1.5% __random_r /build/eglibc-3GlaMS/eglibc-2.19/stdlib/random_r.c:371 1 1.5% 98.5% 1 1.5% __random_r /build/eglibc-3GlaMS/eglibc-2.19/stdlib/random_r.c:381 1 1.5% 100.0% 1 1.5% rand /build/eglibc-3GlaMS/eglibc-2.19/stdlib/rand.c:28 0 0.0% 100.0% 67 100.0% __libc_start_main /build/eglibc-3GlaMS/eglibc-2.19/csu/libcstart. c:287 0 0.0% 100.0% 67 100.0% _start ??:0 0 0.0% 100.0% 67 100.0% main ??:0 0 0.0% 100.0% 14 20.9% rand /build/eglibc-3GlaMS/eglibc-2.19/stdlib/rand.c:27 0 0.0% 100.0% 27 40.3% std::vector::_M_emplace_back_aux ??:0 أو يمكنك إنشاء رسم تخطيطي في ملف pdf باستخدام الأمر: pprof --pdf ./main main.prof > out.pdf هذا الدرس جزء من سلسلة دروس عن C++‎. ترجمة -بتصرّف- للفصول: Chapter 143: Optimization in C++‎ Chapter 144: Optimization Chapter 145: Profiling من كتاب C++ Notes for Professionals
  3. يقضي مطوّرو C++‎ الكثير من وقتهم في تنقيح الأخطاء (debugging)، ويهدف هذا المقال إلى مساعدتك في هذه المهمة وإعطائك بعض التقنيات المفيدة، لكن لا تتوقع قائمة شاملة للمشاكل وحلولها التي تدعمها هذه الأدوات. اختبار الوحدات في C++‎ يسعى اختبار الوحدات (Unit testing) إلى التحقق من صحّة وسلامة وحدات الشيفرة (units of code)، ويشير مصطلح "وحدات الشيفرة" في C++‎ غالبًا إلى الأصناف أو الدوال أو مجموعات مؤلّفة منهما. ويُجرى اختبار الوحدات غالبًا باستخدام "أُطر اختبار" متخصصة أو "مكتبات اختبار"، والتي تستخدِم غالبًا صياغات أو أنماط غير اعتيادية، وسنراجع في هذا الموضوع بعض استراتيجيات ومكتبات وأطر اختبار الوحدات. اختبار جوجل اختبار جوجل (Google Test) هو إطار لاختبار C++‎ من Google، ويتطلّب استخدامُه بناء مكتبة ‎gtest‎ وربطها بإطار الاختبار الخاص بك. مثال بسيط، سنقسم المثال بداعي الشرح … // main.cpp #include <gtest/gtest.h> #include <iostream> أنشئت حالات اختبار جوجل بواسطة وحدات ماكرو خاصة بالمعالج الأولي لـ ++C، وسنوفر المعامِليْن test name و test suite، نتابع … TEST(module_name, test_name) { std::cout << "Hello world!" << std::endl; كذلك توفر Google Test وحدات ماكرو لأجل التوكيدات (assertions): ASSERT_EQ(1+1, 2); } يمكن تشغيل اختبار جوجل يدويًا من دالة ()main أو ربطها بمكتبة gtest_main لدالة ()main معدَّة سلفًا لقبول حالات اختبار جوجل، … int main(int argc, char** argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); } // أمر البناء: g++ main.cpp -lgtest catch Catch هي مكتبة ترويسة (header only library) تسمح لك باستخدام كل من نمطي الاختبار TDD و BDD. الشيفرة التالية مأخوذة من صفحة توثيق Catch: SCENARIO("vectors can be sized and resized", "[vector]") { GIVEN("A vector with some items") { std::vector v(5); REQUIRE(v.size() == 5); REQUIRE(v.capacity() >= 5); WHEN("the size is increased") { v.resize(10); THEN("the size and capacity change") { REQUIRE(v.size() == 10); REQUIRE(v.capacity() >= 10); } } WHEN("the size is reduced") { v.resize(0); THEN("the size changes but not capacity") { REQUIRE(v.size() == 0); REQUIRE(v.capacity() >= 5); } } WHEN("more capacity is reserved") { v.reserve(10); THEN("the capacity changes but not the size") { REQUIRE(v.size() == 5); REQUIRE(v.capacity() >= 10); } } WHEN("less capacity is reserved") { v.reserve(0); THEN("neither size nor capacity are changed") { REQUIRE(v.size() == 5); REQUIRE(v.capacity() >= 5); } } } } سيُبلَغ عن هذه الاختبارات على النحو التالي عند التشغيل: Scenario: vectors can be sized and resized Given: A vector with some items When: more capacity is reserved Then: the capacity changes but not the size التحليل الساكن Static analysis التحليل الساكن طريقة لتفحّص الشيفرة للبحث عن الأنماط التي ترتبط عادة بالأخطاء المشهورة وتدل عليها، هذه التقنية تستغرق وقتًا أقل من مراجعة الشيفرة، لكن تقتصر عمليات التحقق التي تقوم بها على الحالات المُبرمجة في الأداة. وتتضمّن عمليات مثل التحقّق استخدام الفاصلة المنقوطة بشكل غير صحيح بعد عبارة ‎(if (var);)‎ مثلًا، وخوارزميات الشُّعَب (graph algorithms) المتقدمة التي تحدد ما إذا كان المتغيّر مُهيَّأً أم لا. تحذيرات المُصرِّف Compiler warnings من السهل تمكين التحليل الساكن، فهناك إصدارات مُبسّطة مُضمّنة سلفًا في مصرّفك: clang++ -Wall -Weverything -Werror ...‎ g++ -Wall -Weverything -Werror ...‎ cl.exe /W4 /WX ...‎ إذا مكّنتَ هذه الخيارات، ستلاحظ أنّ كل مُصرِّف سيعثر على أخطاء لا تنتبه إليها المصرّفات الأخرى، وستحصل على أخطاء على تقنيات قد تكون صالحة في سياقات محدّدة. فمثلًا، قد تكون ‎while (staticAtomicBool);‎ مقبولة حتى لو لم تكن ‎while (localBool);‎ كذلك. لذلك، على عكس مراجعة الشيفرة، فأنت تستخدم أداة تفهم شيفرتك، وتنبّهك إلى الكثير من الأخطاء، وأحيانًا قد لا تتفق معك. قد تضطر إلى وقف التحذير محليًا في هذه الحالة نظرًا لأنّ الخيارات المذكورة أعلاه قد تُمكِّن جميع التحذيرات، بما في ذلك التحذيرات التي لا تريدها (مثلًا، لماذا يجب أن تكون شيفرتك متوافقة مع C++‎ 98؟). في هذه الحالة، يمكنك تعطيل تحذير بعينه: clang++ -Wall -Weverything -Werror -Wno-errortoaccept ...‎ ‎‎‎‎g++ -Wall -Weverything -Werror -Wno-errortoaccept ...‎‎ ‎‎‎‎cl.exe /W4 /WX /wd<no of warning>...‎‎ صحيح أنّ تحذيرات المصرّف ستساعدك أثناء التطوير، إلّا أنّها تُبطئ عملية التصريف، لهذا قد لا ترغب في تمكينها افتراضيًا في كل مرة، فإما أن تشغّل تحذيرات المصرّف افتراضيًا، أو يمكنك تمكين التكامل المستمر (continuous integration) باستخدام عمليات تحقق أكثر كلفة (أو كليهما). أدوات خارجية إذا قرّرت العمل بالتكامل المستمر (continuous integration)، فإنّ استخدام الأدوات الأخرى ليست ذات فائدة. وتحتوي أداة مثل clang-tidy على قائمة من عمليّات التحقق التي تغطي مجموعة واسعة من المشاكل، منها: الأخطاء الفعلية منع التشريح (Prevention of slicing) توكيدات (Asserts) مع آثار جانبية التحقّق من إمكانية القراءة إزاحات بادئة (indentation) غير صحيحة التحقق من تسمية المُعرِّف (Check identifier naming) التحقّق من التحديث (Modernization checks) استخدام make_unique()‎ استخدام nullptr *التحقّق من الأداء إيجاد النسخ غير الضرورية البحث عن خوارزميات الاستدعاء غير الفعّالة قد لا تكون القائمة كبيرة، إذ أنّ Clang تحتوي على الكثير من التحذيرات، إلّا أنها ستجعل شيفرتك أكثر أمانًا. أدوات أخرى توجد أدوات أخرى مماثلة بنفس الغرض، مثل: محلل visual studio الساكن كأداة خارجية clazy، إضافة خاصّة بمصرّف Clang للتحقق من شيفرات Qt الخلاصة توجد الكثير من أدوات التحليل الساكن لـ C++‎، سواء المضمّنة في المُصرِّف، أو الأدوات الخارجية، ولا تأخذ تجربتها الكثير من الوقت عند الاكتفاء بالعمل بالإعدادات السهلة، وستساعدك على رصد أخطاء قد لا تنتبه لها عند مراجعة الشيفرة. تحليل Segfault مع GDB سنستخدم نفس الشيفرة التي استخدمناها في المثال أعلاه. #include <iostream> void fail() { int *p1; int *p2(NULL); int *p3 = p1; if (p3) { std::cout << *p2 << std::endl; } } int main() { fail(); } أولًا، سنحاول تصريفها: g++ -g -o main main.cpp ثم نشغّلها بواسطة gdb: gdb ./main الآن، سنكون في صدفة gdb. اكتب الأمر run. (gdb) run The program being debugged has been started already. Start it from the beginning? (y or n) y Starting program: /home/opencog/code-snippets/stackoverflow/a.out Program received signal SIGSEGV, Segmentation fault. 0x0000000000400850 in fail () at debugging_with_gdb.cc:11 11 std::cout << *p2 << std::endl; لاحظ أنّ خطأ التجزئة (segmentation fault) قد حدث في السطر 11، لذا فالمتغيّر الوحيد المُستخدم في هذا السطر هو المؤشّر p2. يمكن التحقّق من محتواه بطباعة ما يلي: (gdb) print p2 $1 = (int *) 0x0 نلاحظ أنّ p2 هُيِّئ عند القيمة 0×0، والتي تكافئ NULL. في هذا السطر، حاولنا تحصيل (dereference) مؤشّر فارغ، لذا علينا إصلاح هذا الخلل. تنظيف الشيفرات يبدأ التنقيح بفهم الشيفرة التي تحاول تنقيحها. انظر المثال التالي عن شيفرة سيّئة: int main() { int value; std::vector < int > vectorToSort; vectorToSort.push_back(42); vectorToSort.push_back(13); for (int i = 52; i; i = i - 1) { vectorToSort.push_back(i *2); } /// مُحسّنة لاستخدامها في ترتيب المتجهات الصغيرة if (vectorToSort.size() == 1); else { if (vectorToSort.size() <= 2) std::sort(vectorToSort.begin(), std::end(vectorToSort)); } for (value: vectorToSort) std::cout << value << ' '; return 0; } هذه الشيفرة أفضل: std::vector < int > createSemiRandomData() { std::vector < int > data; data.push_back(42); data.push_back(13); for (int i = 52; i; --i) vectorToSort.push_back(i *2); return data; } /// مُحسّنة لاستخدامها في ترتيب المتجهات الصغيرة void sortVector(std::vector &v) { if (vectorToSort.size() == 1) return; if (vectorToSort.size() > 2) return; std::sort(vectorToSort.begin(), vectorToSort.end()); } void printVector(const std::vector<int> &v) { for (auto i: v) std::cout << i << ' '; } int main() { auto vectorToSort = createSemiRandomData(); sortVector(std::ref(vectorToSort)); printVector(vectorToSort); return 0; } بغض النظر عن نمط التشفير الذي تفضله، سيساعدك الثّبات على نفس النمط على فهم شيفرتك، ويمكن رصد فرصتين لتحسين قراءة الشيفرة بالنظر إلى الشيفرة أعلاه، وكذلك تسهيل تنقيحها: استخدام دوال منفصلة للإجراءات المنفصلة يتيح لك استخدام دوال منفصلة تخطّي بعض الدوال في المنقّح إذا لم تكن مهتمًّا بتفاصيلها. قد لا تكون مهتمًا بتفاصيل إنشاء البيانات أو طباعتها في هذه الحالة الخاصّة، وقد ينصبّ اهتمامك على عملية الترتيب وحَسب. هناك ميزة أخرى، وهي أنّ الشيفرة التي عليك قراءتها وحفظها ستكون أقصر عند تفحّص الشيفرة، إذ ستحتاج الآن إلى قراءة 3 أسطر فقط من الشيفرة في دالة ‎main()‎ لفهمها، بدلًا من قراءة الدالّة بأكملها. والميزة الثالثة هي أنّه نظرًا لكون الشيفرةً المُتحقَّق منها أقصر، فإن عينك ستتدرب الآن على رصد الأخطاء في غضون ثوانٍ. استخدام تنسيقات وإنشاءات متّسقة استخدامُ التنسيقات والإنشاءات المتّسقة ينظّم الشيفرةَ ويسهّل تحليلها، وهناك نقاشات كثيرة حول طريقة التنسيق "الأمثل". لكن بغضّ النظر فالمهمّ أن تلتزم بنمط برمجة واحد في شيفرتك، فذلك سيحسّن قراءتها ويسهّل التركيز على محتوى الشيفرة. ويوصى باستخدام أداة مخصّصة لتنسيق الشيفرة نظرًا لأنّ هذه المهمّة قد تستغرق وقتًا طويلاً، وتدعم معظم بيئات التطوير المتكاملة (IDEs) هذه الميزة بشكل أو بآخر. قد تلاحظ أنّ نمط البرمجة لا يقتصر على استخدام المسافات الفارغة والسطور الجديدة، فلم نعد نمزج بين النمط الحر (free-style) والدوال التابعة لبدء / إنهاء الحاويات (‎v.begin()‎ و ‎std::end(v)‎). تسليط الضوء على الأجزاء الهامّة من شيفرتك بغض النظر عن النمط الذي تستخدمه، فإنّ الشيفرة أعلاه تحتوي على عدد من الملاحظات التي قد تعطيك تلميحًا حول الأمور المهمّة في الشيفرة: تعليق يتضمّن الكلمة "مُحسَّن" ، في إشارة إلى استخدام بعض التقنيات الجذابة. بعض تعليمات return المبكّرة في ‎sortVector()‎ تشير إلى أنّنا نريد فعل شيء خاصّ. ‎std::ref()‎ - تشير إلى أنّ شيئًا ما يحدث في ‎sortVector()‎. الخلاصة الشيفرات النظيفة أسهل في الفهم وتقلل الوقت اللازم لتنقيحهَا، فقد يتمكّن مُراجِع الشيفرة من رصد الأخطاء منذ الوهلة الأولى في المثال الثاني، بينما قد تكون الأخطاء أخفَى في المثال الأوّل. (ملاحظة الخطأ موجود في: عملية الموازنة مع العدد ‎2‎.) هذا الدرس جزء من سلسلة دروس عن C++‎. ترجمة -بتصرّف- للفصل Chapter 142: C++ Debugging and Debugprevention Tools & Techniques والفصل Chapter 141: Unit Testing in C++‎ من كتاب C++ Notes for Professionals
  4. ينبغي تصريف البرامج المكتوبة بلغة C++‎ قبل أن تتمكن تلك البرامج من العمل، وستجد مجموعة كبيرة ومتنوّعة من برامج التصريف أو المصرِّفات (compilers) المتاحة والمناسبة لنظام التشغيل الذي تعمل به. التصريف بواسطة GCC التصريف دون تحسين مفيد في المراحل الأولى من التطوير والتنقيح، على الرغم من أنّ خيار ‎-Og‎ موصىً به في الإصدارات الحديثة من GCC، وبناء عليه فيمكن تصريف وربط ملف تنفيذي وغير محسَّن على افتراض أن هناك ملفًا مصدريًا واحدًا يسمى main.cpp، وذلك على النحو التالي: g++ - o app - Wall main.cpp - O0 ولإنتاج ملف مُحسّن قابل للتنفيذ لاستخدامه في الإنتاج، استخدم أحد خيارات ‎-O‎ (راجع: ‎،-O1‎، ‎-O2‎، ‎-O3‎، ‎-Os‎، ‎-‎‎Ofast‎): g++ - o app - Wall - O2 main.cpp في حال حذف الخيار ‎-O، سيُستخدَم ‎-O0 الذي يعني إلغاء التحسينات، كخيار افتراضي، واعلم أن تحديد ‎-O بدون رقم يجعله مساويًا لـ ‎-O1. وكخيار بديل، يمكن أن نستخدم رايات التحسين (optimization flags) من مجموعات ‎O‎ -أو تحسينات تجريبية أخرى- مباشرةً. انظر المثال التالي حيث يبني البرنامج مع التحسين ‎-O2‎، بالإضافة إلى راية واحدة من مستوى التحسين ‎-O3‎: g++ -o app -Wall -O2 -ftree-partial-pre main.cpp لإنتاج ملفّ تنفيذي مُحسّن لأجل منصة معيّنة -لاستخدامه على جهاز له نفس المعمارية-، استخدم: g++ -o app -Wall -O2 -march=native main.cpp سوف ينتُج عن الشيفرَتين أعلاه ملفٌّ ثنائي (binary file) يمكن تشغيله باستخدام ‎.\app.exe‎ على Windows، أو باستخدام ‎./app‎ على Linux و Mac OS، كما يمكن أيضًا تخطي الراية ‎-o‎، وفي هذه الحالة سينشئ GCC الملفّ التنفيذي الافتراضي ‎a.exe‎ على Windows و ‎a.out‎ على الأنظمة الشبيهة بيونكس (Unix-like). لأجل تصريف ملف دون ربطه، استخدم الخيار ‎-c‎: g++ -o file.o -Wall -c file.cpp سينتج عن هذا ملفٌّ يحمل الاسم ‎file.o‎، والذي يمكن ربطه لاحقًا بملفّات أخرى لإنتاج ملف ثنائي: g++ -o app file.o otherfile.o انظر gcc.gnu.org للمزيد من التفاصيل حول خيارات التحسين، خاصة ‎-Og‎، وهو التحسين مع التركيز على تجربة التنقيح (debugging)، الذي يوصى باستخدامه في دورات تحرير-تصريف-تنقيح القياسية - standard edit-compile-debug cycle)، وكذلك ‎-Ofast‎ الذي يشمل جميع التحسينات، بما في ذلك تلك التي تتجاهل الامتثال الصارم للمعايير. تُمكِّنك راية ‎-Wall‎ من إطلاق تحذيرات من بعض الأخطاء الشائعة، وينبغي أن تُستخدم دائما، ويوصى باستخدام ‎-Wextra‎ لتحسين جودة الشيفرة، وكذلك رايات التحذير الأخرى التي لا تُمكَّن تلقائيًا من قِبل ‎-Wall‎ و ‎-‎‎Wextra‎. إذا كانت الشيفرة تتوقع معيارًا محدّدًا للغة C++‎، فيمكنك تحديد المعيار الذي تريد استخدامه عن طريق تضمين راية ‎-std=‎. وتتوافق القيم المدعومة مع سنة الإصدار النهائي لمعيار ISO C++‎، واعتبارا من GCC 6.1.0 فإنّ القيم الصالحة لـ ‎std=‎ هي: ‎c++98‎ / ‎c++03‎، ‎c++11‎، ‎c++14‎، و‎c++17‎ / ‎c++1z‎. لاحظ أن القيم المفصولة بشرطةٍ مائلة / متكافئة. g++ -std=c++11 <file> يتضمّن GCC بعض الإضافات (Extensions) (extensions) الخاصة بالمُصرِّف، والتي تُعطَّل عندما تتعارض مع معيار قياسي محدّد من قبل راية ‎-std=‎، فإذا أردت التصريف مع كل الإضافات (Extensions) المُمَكَّنة، فاستخدام القيمة ‎gnu++XX‎، حيث تمثّل ‎XX‎ أيًّا من السنوات المذكورة أعلاه. ويُستخدَم المعيار الافتراضي في حالة عدم تعريف أيّ منها، وبالنسبة لإصدارات GCC التي تسبق 6.1.0 فإنّ الإعداد الافتراضي هو ‎-‎std‎ = ‎gnu‎++03، أما في GCC 6.1.0 والإصدارات الأحدث فإن الإعداد الافتراضي هو ‎-std=gnu++14‎. لاحظ أنّه نظرا لوجود بعض الأخطاء (bugs) في GCC، فيجب أن تكون راية pthread- حاضرة في التصريف والربط إن أردت أن يدعم GCC معيار الخيوط (threading) الذي أُدخل في C++‎11، مثل ‎std::thread‎ و ‎std::wait_for‎. قد لا ينتج عن حذفها عند استخدام الخيوط أيّ تحذيرات، ولكن قد تحصل على نتائج غير صحيحة في بعض المنصّات. الربط بالمكتبات Linking with libraries استخدم خيار ‎-l‎ لتمرير اسم المكتبة: g++ main.cpp -lpcre2-8 # 8bit code units (UTF-8) في وحدات الشيفرات ثمانية البتّات PCRE2 هي مكتبة pcre2-8 إذا لم تكن المكتبة موجودة في مسار المكتبة القياسية، فأضف المسار باستخدام ‎-L‎: g++ main.cpp -L/my/custom/path/ -lmylib يمكن ربط عدّة مكتبات معًا: g++ main.cpp -lmylib1 -lmylib2 -lmylib3 إذا كانت إحدى المكتبات تعتمد على مكتبة أخرى، فضع المكتبة المعتِمدة قبل المكتبة المستقلة: g++ main.cpp -lchild-lib -lbase-lib أو اترك الرابط (linker) يتكفّل بتحديد الترتيب عبر الخيارات ‎--start-group‎ و ‎--end-group‎ (ملاحظة هذا له تكلفة كبيرة على الأداء): g++ main.cpp -Wl,--start-group -lbase-lib -lchild-lib -Wl,--end-group التصريف باستخدام Visual Studio (واجهة رسومية) نزِّل Visual Studio Community 2015 وثبِّته. افتح Visual Studio Community . انقر على File، ثم NewK ثم Project. انقر على Templates ثم ++Visual C ثم Win32 Console Application، ثم ضع اسمًا للمشروع وليكن MyFirstProgram. انقر على Ok. انقر على "Next " في النافذة التالية. اختر الخانة ‎Empty project‎ تحت الخيارات الإضافية (Additional options) ثم انقر على "Finish": انقر بالزر الأيمن فوق مجلد ملف المصدر ثم ‎Add، ثم New Item: حدّد ملف C++‎ وقم بتسمية main.cpp، ثم انقر فوق Add: انسخ والصق الشيفرة التالية في الملف الجديد main.cpp: #include <iostream> int main() { std::cout << "Hello World!\n"; return 0; } ينبغي أن تبدو بيئة العمل لديك كما يلي: انقر على Debug ثم Start Without Debugging، (أو اضغط على ctrl + F5): يجب أن تحصل على الخرج التالي في سطر الأوامر: مُصرِّفات الشبكة Online Compilers توفّر العديد من المواقع مصرّفات C++‎ يمكن الوصول إليها عبر شبكة الإنترنت، وتختلف مزايا وإمكانيات مصرّفات الشبكة من موقع إلى آخر، ولكنها عادة ما تسمح بالقيام بما يلي: لصق الشيفرة في نموذج في المتصفّح. تحديد بعض خيارات المصرّف، وتصريف الشيفرة. الحصول على خرج المصرّف و/أو البرنامج. عادةً ما تكون المُصرِّفات الشبكية مقيّدة إذ أنّها تتيح لأيّ شخص تشغيل المصرّف وتنفيذ شيفرات عشوائية على الخادم، وهذا قد يكون مصدر خطر في حال تنفيذ شيفرات ضارة. قد تكون مصرّفات الشبكة مفيدة للأغراض التالية: تشغيل مقتطف صغير من شيفرة جهاز يفتقر إلى مصرّف C++‎ (مثل الهواتف الذكية والأجهزة اللوحية وما إلى ذلك). التحقّق من أنّ الشيفرة تُصرَّف بنجاح في مختلف المُصرِّفات، وتعمل بنفس الطريقة بغض النظر عن المُصرِّف الذي صُرِّفت فيه. تعلُّم أو تعليم أساسيات C++‎. تعلّم ميزات C++‎ الحديثة (C++‎ 14 و C++‎ 17 في المستقبل القريب) إذا لم يكن لديك مصرّف C++‎ حديث على جهازك. رصد الأخطاء التي يمكن أن تكون موجودة في المصرّف الذي تعمل به بمقارنته بمجموعة كبيرة من المصرّفات الأخرى. التحقق ممّا إذا كانت الإصدارات اللاحقة من المصرّف الذي تعمل قد صُحِّحت في حال لم تتوفر تلك الإصدارات على جهازك. حل المشاكل والتمارين عبر الشبكة. بالمقابل، لا ينبغي استخدام مُصرِّفات الشبكة لأجل: تطوير برامج كاملة (ولو كانت صغيرة) باستخدام C++‎، وفي العادةً فإن مصرّفات الشبكة لا تسمح بالارتباط مع مكتبات خارجية، أو تنزيل أدوات البناء. إجراء حسابات مكثفة. موارد الخادم الحسابية محدودة، لذلك سيوقَف أيّ برنامج يقدّمه المستخدم بعد بضع ثوانٍ من بدء تنفيذه، فوقت التنفيذ المسموح به يكفي عادة للاختبار والتعلم فقط. قرصنة ومهاجمة الخادم الذي يعمل عليه المُصرِّف، أو أيّ جهة خارجية مُستضافة على الشبكة. أمثلة على المصرفات الشبكية: codepad.org: مُصرِّف مع إمكانية مشاركة الشيفرة، وتحرير الشيفرة بعد التصريف، لكن التحذيرات و الأخطاء لا تعمل بشكل جيد. coliru.stacked-crooked.com: مصرّف يمكّنك من أن تحدّد سطر الأوامر، ويتيح لك اختيار أحد المُصرِّفين GCC أو Clang. cpp.sh: مُصرِّف يدعم C++‎ 14، لا يسمح لك بتحرير سطر أوامر المصرّف، ولكن يوفّر بعض الخيارات عبر عناصر التحكم في واجهة المستخدم الرسومية. gcc.godbolt.org: يوفر قائمة واسعة من إصدارات المُصرِّف، والمعماريات، وهو مفيد للغاية لمن يحتاج إلى التحقّق من عمل الشيفرة في عدّة مُصرِّفات مختلفة. والمصرفات التالية هي بعض المصرّفات المتاحة: GCC و Clang و MSVC ومصرّف Intel و ELLCC و Zapcc، مع توفّر واحد أو أكثر من هذه المصرّفات للمعماريّات ARM و ARMv8 (مثلARM64) و Atmel AVR و MIPS و MIPS64 و MSP430 و PowerPC و x86 و x64. أيضًا، يمكن تعديل وسائط سطر الأوامر الخاص بالمُصرِّف. ideone.com: يُستخدم على نطاق واسع لتوضيح سلوك الشيفرة، ويوفر كلًّا من GCC و Clang، لكنّه لا يسمح بتحرير سطر أوامر المصرّف. melpon.org/wandbox: يدعم العديد من إصدارات Clang و GNU / GCC. onlinegdb.com: بيئة تطوير متكاملة لكن محدودة، تتضمّن محررًا ومُصرِّفًا (gcc) ومنقّحًا (gdb). rextester.com: يوفر المُصرِّفات Clang و GCC و Visual Studio لكل من C و C++‎ (إضافة إلى مُصرِّفات خاصّة بلغات أخرى)، ومكتبة Boost. tutorialspoint.com/compile_cpp11_online.php: صدفة UNIX متكاملة، مع مصرّف GCC، ومستكشف مشاريع سهل الاستخدام. webcompiler.cloudapp.net: مُصرِّف لبرنامج Visual Studio 2015، مُقدّم من قبل Microsoft كجزء من RiSE4fun. التصريف باستخدام Visual C++‎ (سطر الأوامر) بالنسبة للمبرمجين الذين اعتادوا على العمل بالمصرّفَين GCC أو Clang، والذين يودّون الانتقال إلى Visual Studio، أو المبرمجين الذين يفضّلون العمل بسطر الأوامر بشكل عام، يمكن استخدام مصرّف Visual C++‎ من سطر الأوامر إلى جانب بيئة تطوير متكاملة (IDE). وإذا كنت ترغب في تصريف شيفرتك من سطر الأوامر في Visual Studio، فسيكون عليك إعداد بيئة سطر الأوامر، عن طريق فتح: Visual Studio Command Prompt/Developer Command Prompt/x86 Native Tools Command Prompt/x64 Native Tools Command Prompt أو شيئٍا من هذا القبيل (يختلف الأمر حسب إصدار Visual Studio الذي تعمل به)، أو في موجّه الأوامر، من خلال الانتقال إلى المجلّد الفرعي ‎VC‎ في مجلّد التثبيت الخاصّ بالمصرّف (يكون عادةً ‎\Program Files (x86)\Microsoft Visual Studio x\VC‎، حيث يمثّل ‎x‎ رقم الإصدار (مثل ‎10.0‎ لعام 2010، أو ‎14.0‎ لعام 2015)، ثمّ تنفيذ الملفّ ‎VCVARSALL‎ مع مُعامل سطر الأوامر المُحدّد هنا. لاحظ أنّه على عكس GCC، فإنّ Visual Studio لا يوفّر واجهة للرابط (‎link.exe‎) عبر المصرّف (‎cl.exe‎)، ولكنّها توفّر الرابط (linker) كبرنامج منفصل، ويستدعيه االمُصرِّف عند الإنهاء. ويمكن استخدام ‎cl.exe‎ و ‎link.exe‎ بشكل منفصل مع عدّة ملفّات وخيارات، أو إخبار ‎cl‎ بتمرير الملفّات والخيارات إلى ‎link‎ إذا كانت المهمّتان ستُنجزان معًا، وستُترجم خيارات الربط المُحدّدة لـ ‎cl‎ إلى خيارات خاصة بـ ‎link‎، وتُمرّر الملفات غير المُعالجة بواسطة ‎cl‎ مباشرة إلى الرابط ‎link‎. انظر هذه الصفحة للمزيد عن وسائط ‎link‎. لاحظ أنّ الوسائط التي تخصّ ‎cl‎ حسّاسة لحالة الأحرف، وذلك على خلاف وسائط ‎link‎، وأن بعض الأمثلة التالية تستخدم المتغيّر "current directory" الخاصّ بصدفة (shell) ويندوز، ‎%cd%‎، عند تحديد أسماء المسار المطلق (absolute path names)، ويُوسَّع هذا المتغيّر إلى مجلّد العمل الحالي (current working directory). وفي سطر الأوامر، سيكون هو المجلّد الذي كنت تستخدمه عند تشغيل ‎cl‎، وهو محدَّد في موجّه الأوامر - command prompt - افتراضيًا (مثلًا، في موجّه الأوامر ‎C:\src>‎، فإنّ ‎%cd%‎ سيكون ‎C:\src\‎). يمكنك تصريف وربط ملف تنفيذي غير مُحسَّن (unoptimised executable) بافتراض أنّ هناك ملفّا مصدريًا واحدًا يُسمّى ‎main.cpp‎ في المجلد الحالي، وهذا مفيد في مرحلة التطوير الأولي والتنقيح، عبر استخدام أيٍّ ممّا يلي: cl main.cpp // "main.obj" إنشاء ملف // "main.obj" إجراء الربط مع // "main.exe" إنشاء الملف القابل للتنفيذ cl /Od main.cpp في المثال السابق، سيتصرف cl /Od main.cpp كسابقه، لكن يكون "Od/" هو خيار "Optimisation: disabled"، ويكون الخيار الافتراضي عند عدم تحديد أي من خيارات "O/". بافتراض أنّ هناك ملفًّا مصدريًا إضافيًا " niam.cpp " في نفس المجلّد، فستنشئ الشيفرة التالية الملفين "main.obj" و "niam.obj"، ثم يجري الربط معهما، ومن ثم ينشئ الملف التنفيذي "main.exe": cl main.cpp niam.cpp يمكنك أيضًا استخدام محارف البدل (wildcards)، ستنشئ الشيفرة التالية ملف كائن "main.obj" إضافة إلى ملف كائن لكل ملف cpp. في المجلد التالي: "%cd%\src" ثم تُجري الربط مع "main.obj" وكل ملف كائن مُنشأ، وستكون كل ملفات الكائن في المجلد الحالي، ثم تولِّد ملف main.exe. cl main.cpp src\* .cpp لإعادة تسمية الملف القابل للتنفيذ أو نقله، استخدم أحد الخيارات التالية: cl /o name main.cpp // "name.exe" تولد ملف قابل للتنفيذ cl /o folder\ main.cpp // "%cd%\folder" في المجلد "main.exe" إنشاء ملف cl /o folder\name main.cpp // "%cd%\folder" في المجلد "main.exe" إنشاء ملف cl /Fename main.cpp // "/o name" مثل cl /Fename main.cpp // "/o folder\" مثل cl /Fefolder\name main.cpp // "/o folder\name" مثل يمرِّر كلٌّ من ‎/o‎ و ‎/Fe‎ معاملاتِهما (دعنا نسميها ‎o-param‎) إلى ‎link‎ على شكل ‎/OUT:o-param‎، مع إلحاق الامتداد المناسب (بشكل عام ‎.exe‎ أو ‎.dll‎) لتسمية ‎o-param‎ بما يُناسب، في حين أنّ لـ ‎/o‎ و ‎/Fe‎ نفس الوظيفية - على حدّ علمي - إلّا أنّ الأخير هو المفضل في Visual Studio. لقد أصبحت ‎/o‎ مهمَلة، ويبدو أنّها تًقدّم بشكل أساسي للمبرمجين الذين اعتادوا العمل بالمصرّفَين GCC و Clang. لاحظ أنّه على الرغم من أنّ المسافة الفارغة بين ‎/o‎ واسم المجلد المحدّد اختيارية، إلا أنه لا يمكن وضع مسافة بيضاء بين ‎/Fe‎ واسم المجلد المحدّد. بالمثل، يمكن إنشاء ملف تنفيذي مُحسّن (لاستخدامه في مرحلة الإنتاج) على النحو التالي: سنحسِّن حجم الملف التنفيذي في السطر الأول لكن قد يكون هذا على حساب سرعة التنفيذ، أما في السطر الثاني فيحسِّن سرعة البرنامج، لكن ربما يجعل هذا حجم الملف أكبر. cl /O1 main.cpp cl /O2 main.cpp هنا ننشئ ملفات خاصة لتحسين البرنامج، مما يسمح لسطر الأوامر أن يأخذ كل وحدات الترجمة في حسبانه خلال عملية التحسين، وسيمرر خيار توليد شيفرة الرابط-الوقت (Link-Time Code Generation) أو LTCG/، إلى الرابط LINK، ليخبره باستدعاء سطر الأوامر خلال مرحلة الربط لإجراء تحسينات إضافية. وينبغي ربط كائن الملف المُنشأ مع LTCG/ في حال عدم إجراء الربط في ذلك الوقت، كما يمكن استخدامها مع خيارات التحسين الأخرى في سطر الأوامر، … cl /GL main.cpp other.cpp أخيرًا، لإنتاج ملف تنفيذي مُحسَّن لأجل منصّة معيّنة (لاستخدامه في الإنتاج على جهاز ذي معمارية معيّنة)، فعليك اختيار موجّه الأوامر المناسب، أو مُعامل ‎VCVARSALL‎ المناسب للمنصّة المُستهدفة. ويجب أن يرصُد الرابط (‎link‎) المنصّة المطلوبة من ملفّات الكائن؛ وإلّا، فعليك استخدام الخيار ‎/MACHINE‎ لتعريف المنصّة المُستهدفة بشكل صريح. مثال: إذا أردت التصريف للتنفيذ على معمارية 64 بت، وتعذر على الرابط LINK أي يرى المنصة المستهدفة: cl main.cpp / link / machine: X64 سينتج أيًا مما سبق ملفًا تنفيذيًا يحمل الاسم المحدّد من قِبل ‎/o‎ أو ‎/Fe‎، وفي حالة عدم توفير أيّ منهما، سيحمل اسمًا مطابقًا للملف المصدري الأول أو ملفِّ الكائن المحدّد للمُصرِّف. cl a.cpp b.cpp c.cpp // "a.exe" إنشاء cl d.obj a.cpp q.cpp // "d.exe" إنشاء cl y.lib n.cpp o.obj // "n.exe" إنشاء cl / o yo zp.obj pz.cpp // "yo.exe" إنشاء لتصريف الملفّات دون ربط: cl / c main.cpp // "main.obj" إنشاء ملف تخبر الشيفرةُ السابقة سطرَ الأوامر ‎cl‎ بالخروج دون استدعاء ‎link‎، وتنتج ملفّ كائن يمكن ربطه لاحقًا بالملفّات الأخرى لإنتاج ملف ثنائي. في الشيفرة التالية، ينشئ السطر الأول ملف الكائن niam.obj، ويجري الربط مع niam.obj و main.obj، ثم ينشئ الملف التنفيذي main.exe. ويجري السطر الثاني ربطًا مع niam.obj و main.obj، ثم ينشئ الملف التنفيذي main.exe. cl main.obj niam.cpp link main.obj niam.obj هناك مُعاملات أخرى مهمّة لسطر الأوامر، ومن المفيد جدًا معرفتها، منها ما يلي: cl /EHsc main.cpp: يشير EHsc إلى أنه لن تُمسك إلا اعتراضات ++C القياسية، وأن دوال C الخارجية لن ترفع اعتراضات (exceptions). يوصى بهذا المعامل لمن يريد كتابة شيفرة محمولة ومتعددة المنصات. cl /clr main.cpp: يشير clr/ إلى أن الشيفرة يجب أن تُصرَّف لاستخدام اللغة المشتركة لوقت التشغيل، وهي آلة وهمية خاصة بإطار عمل NET.، وتسمح باستخدام لغة C++/CLI الخاصة بميكروسوفت إضافة إلى سطر أوامر ++C، وتنشئ ملفًا تنفيذيًا يتطلب NET. ليعمل. cl /Za main.cpp: يشير Za/ إلى وجوب تعطيل إضافات (Extensions) ميكروسوفت، وأن الشيفرة يجب أن تُصرَّف وفق مواصفات ISO للغة ++C حصرًا، وهذا ضروري لضمان محمولية البرنامج. cl /Zi main.cpp: يولد Zi/ ملف قاعدة بيانات PDB للبرنامج من أجل استخدامه عند تنقيح برنامج ما دون التأثير على مواصفات التحسين، ويُمرَّر خيار DEBUG/ إلى الرابط LINK. cl /LD main.cpp: يخبر LD/ سطرَ الأوامر أن يضبط LINK كي يولد ملف DLL بدلًا من ملف تنفيذي، وسينتج الرابط ملف DLL إضافة إلى ملفي EXP و LIB لاستخدامهما عند الربط. مرر ملف LIB المرتبط بملف DLL إلى سطر الأوامر أو الرابط عند تصريف تلك البرامج، وذلك لاستخدام الأخير في برامج أخرى. cl main.cpp /link /LINKER_OPTION: يمرر link/ كل ما بعده إلى الرابط مباشرة دون تحليل. استبدل LINKER_OPTION/ بخيارات الرابط التي تريد. بالنّسبة للذين لديهم خبرة في التعامل مع اليونكسات وأشباهها، و/أو GCC / Clang و ‎cl‎ و ‎link‎، وأدوات سطر أوامر Visual Studio الأخرى، يمكنهم أن يقبلوا المُعاملات المُحدّدة باستخدام الواصلة - (مثل ‎-c‎) بدلًا من الشرطة المائلة (مثل ‎/c‎). إضافة إلى ذلك، يتعرّف نظام ويندوز على الشرطة المائلة العكسية \ (backslash) أو الأمامية / (slash) ويعدّهما فواصل صالحة للمسارات، لذلك يمكن استخدام المسارات التي على نمط يونكس وما شابهه. يسهّل هذا تحويل أسطر أوامر المصرّف من ‎g++‎ أو ‎clang++‎ إلى ‎cl‎، أو العكس، بالحد الأدنى من التغييرات. g++ - o app src / main.cpp cl - o app src / main.cpp ستحتاج إلى البحث عن أوامر مكافئة في توثيق المصرّف الذي تستخدمه و/أو في مواقع توثيق أخرى، وذلك عند محاولة نقل سطور الأوامر التي تستخدم خيارات معقّدة لـ ‎g++‎ أو ‎clang++‎. وإن احتجت إلى استخدام ميزات لغة معيّنة في شيفرتك، فيلزم استخدام إصدار محدّد من MSVC. ومن الممكن في Visual C++‎ 2015 التحديث 3 اختيار إصدار المعيار الذي تريد اعتماده عند التصريف عبر الراية ‎/std‎، والقيمُ الممكنةُ هي ‎/std:c++14‎ و ‎/std:c++latest‎ (ستتبعُهما ‎/std:c++17‎ قريبًا). ملاحظة في الإصدارات القديمة من هذا المُصرِّف، كانت بعض رايات المزايا متوفرة، بيْد أنّها كانت في الغالب تُستخدم لمعاينة الميزات الجديدة. التصريف باستخدام Clang بما أن الواجهة الأمامية لمصرّف Clang مُصمّمة لتكون متوافقة مع GCC، فإنّ معظم البرامج التي يمكن تصريفها عبر GCC ستُصرَّف كذلك عند استبدال ‎clang++‎ بـ‎g++‎ في برامج البناء النصية (build scripts)، وفي حال عدم إعطاء ‎-std=version‎، فسيُستخدَم الإصدار gnu11. يمكن لمستخدمي ويندوز الذين اعتادوا على MSVC استبدال ‎clang-cl.exe‎ بـ ‎cl.exe‎. واعلم أن clang يحاول بشكل افتراضي أن يكون متوافقًا مع أحدث إصدار مُثبّت من MSVC. ويمكن استخدام clang-cl عبر تغيير ‎Platform toolset‎ في خاصّيات المشروع، وذلك عند التصريف باستخدام visual studio. وفي كلتا الحالتين، سيكون clang متوافقًا عبر واجهته الأمامية وحسب، لكنه سيحاول أيضًا بناء ملفات ثنائية متوافقة. كذلك يجب على مستخدمي clang-cl أن ينتبهوا إلى أنّ التوافق مع MSVC ليس كاملًا. لاستخدام clang أو clang-cl، يمكن استخدام التثبيت الافتراضي على توزيعات محدّدة من Linux، أو تلك المُجمَّعة في بيئات التطوير المتكاملة - IDEs - (مثل XCode على Mac). أما بالنسبة للإصدارات الأخرى من هذا المُصرِّف، أو في المنصّات التي لم يُثبّت عليها، فيمكن تنزيله من صفحة التنزيل الرسمية. إذا كنت تستخدم CMake لبناء شيفرتك، فيمكنك تبديل االمُصرِّف عادة عن طريق تعيين متغيّرَي البيئة ‎CC‎ و ‎CXX‎ على النحو التالي: mkdir build cd build CC=clang CXX=clang++ cmake .. cmake --build . عملية التصريف في C++‎ بعد تطوير برنامج بلغة C++‎، فإنّ الخطوة التالية هي تصريف ذلك البرنامج قبل تشغيله. والتصريف (compiling) هو العملية التي تحوّل البرنامج المكتوب بلغة قابلة للقراءة البشرية، مثل C و C++‎ وغيرهما، إلى شيفرة الآلة، وهي شيفرة يمكن أن تُفهمها وحدة المعالجة المركزية مباشرة. على سبيل المثال، إذا كان لديك ملفّ مصدَري مكتوب بلغة C++‎ باسم prog.cpp، وقمت بتنفيذ الأمر compile … g++ -Wall -ansi -o prog prog.cpp ستحدث 4 مراحل رئيسية عند إنشاء ملف تنفيذي من الملف المصدري. يأخذ معالج C++‎ الأولي ملفّ الشيفرة المصدرية ويتعامل مع الترويسات (include#)، ووحدات الماكرو (‎#define‎) وموجّهات المعالج الأخرى. تُصرّف شيفرة C++‎ المصدرية المُوسّعة التي تم إنتاجها من قبل معالج C++‎ الأوّلي إلى لغة التجميع (assembly) المناسبة للمنصّة. تُصرَّف الشيفرة المجمّعة التي تم إنتاجها بواسطة المصرّف إلى كائن شيفرة (object code) مناسب للمنصة. يُربط ملفّ كائن الشيفرة المُنتج من قبل المجمّع مع ملفّات كائنات الشيفرة الخاصة بمكتبات الدوال المُستخدمة، وذلك لإنتاج مكتبة أو ملف قابل للتنفيذ. المعالجة الأولية Preprocessing يُعالج المعالج الأولي تعليمات المعالج، مثل ‎#include و ‎#define، وهو غير واعٍ بصيغة C++‎، لذا يجب توخّي الحذر عند استخدامه. يعمل المعالج الأوّلي على ملفّ ++C مصدري واحد عن طريق استبدال محتوى الملفّات المناسبة (والتي تكون عادة مجرّد تصريحات) بتعليمات ‎#include، ويستبدل وحدات الماكرو (‎#define)، ويختار عدة أجزاء من النص بحسب تعليمات ‎#if و ‎#ifdef و ‎#fndef. يعمل المعالج الأوّلي على مجموعة من وحدات المعالجة الأوّلية (preprocessing tokens)، ويُعُرِّف استبدال الماكرو على أنّه استبدال مقاطع بمقاطع أخرى (يتيح المُعامل ## دمجَ شيفرَتين إذا كان ذلك مناسبًا). بعد كل هذا، يُنتج المعالج الأوّلي خرجًا واحدًا يمثّل مجرى من المقاطع الناتجة عن التحويلات الموضّحة أعلاه، ويضيف أيضًا بعض العلامات/الوسوم الخاصة التي تخبر المُصرِّف بالسطر الذي جاء منه كل مقطع، حتّى يتمكّن من صياغة رسائل الخطأ المناسبة. قد تنتج بعض الأخطاء في هذه المرحلة عند استخدام التعليمتين #if و ‎#error. ونستطيع إيقاف العملية في مرحلة المعالجة الأولية باستخدام راية المصرّف أدناه: g++ -E prog.cpp التصريف تُجرى خطوة التصريف على كل خرج من مخرجات المعالج الأوّلي (Preprocessor)، ويحلّل االمُصرِّف شيفرة C++‎ المصدرية الخالصة - دون أي تعليمات للمعالج الأولي الآن- ثمّ يحوّلها إلى شيفرة المجمّع، ثم يستدعي الواجهة الخلفية الأساسية (underlying backend)، وهي المجمّع في سلسلة الأدوات، التي تجمّع تلك الشيفرة وتحوّلها إلى شيفرة الآلة، وتنتج ملفًّا ثنائيًا وفق تنسيق معيّن (مثل ELF أو COFF أو a.out …). تحتوي ملفات الكائنات على الشيفرة المُصرَّفة (في شكل ثنائي) للرموز المُعرَّفة في المدخلات، ويشار إلى الرموز الموجودة في ملف الكائن بالاسم. وتستطيع ملفات الكائنات أن تشير إلى الرموز التي لم تُعرِّف بعد، كأن تستخدم تصريحًا بدون توفير تعريف له، فلا يمانع المُصرِّف ذلك، وسينتج ملفَّ الكائن بلا مشاكل طالما أنّ الشيفرة المصدرية مصاغة بشكل جيّد. تسمح لك المصرّفات عادةً بإيقاف عملية التصريف في هذه المرحلة، وهو أمر مفيد للغاية إذ يُمكِّنك من تصريف كل ملفّ مصدري بشكل منفصل، وعليه فلن تكون بحاجة إلى إعادة تصريف كل شيء إن لم تكن غيرت إلا ملفًّا واحدًا فقط. يمكن وضع ملفات الكائنات الناتجة في أرشيفات خاصة تسمّى المكتبات الساكنة (static libraries)، لتسهيل استخدامها لاحقًا، ويُبلَّغ في هذه المرحلة عن أخطاء المصرّف "المعتادة"، مثل أخطاء الصيغة، أو أخطاء فشل تحليل التحميل الزائد. لإيقاف العملية بعد خطوة التصريف، يمكن استخدام الخيار ‎-S: g++ -Wall -ansi -S prog.cpp التجميع Assembling ينشئ المجمّع شيفرة الكائن، وقد ترى على نظام UNIX ملفّات ذات لاحقة ‎.o (أو .OBJ على MSDOS) للدلالة إلى ملفات كائنات الشيفرَة (object code files). في هذه المرحلة، يحوّل المجمّع تلك الملفّات من شيفرة مُجمَّعة (assembly code) إلى تعليمات على مستوى الآلة، كما سيكون الملفُّ المُنشأ كائنَ شيفرة قابلة للنقل (relocatable object code)، ومن ثم ستنشئ مرحلةُ التصريف برنامجَ الكائن القابل للنقل، ويمكن استخدام هذا البرنامج في مواضع أخرى دون الحاجة إلى إعادة تصريفه مرّة أخرى. لإيقاف العملية بعد خطوة التجميع، استخدم الخيار‎-c: g++ -Wall -ansi -c prog.cpp الربط Linking الرابط (Linker) هو الذي ينتج مخرجات التصريف النهائية من كائنات الملفّات التي أنتجها المجمّع (Assembler)، وقد تكون المُخرجات إمّا مكتبة مشتركة (أو ديناميكية، رغم تشابه الأسماء، إلا أنّها تختلف عن المكتبات الساكنة المذكورة آنفًا)، أو يمكن أن تكون ملفّا تنفيذيًا. ويربط جميع الكائنات عن طريق استبدال العناوين الصحيحة بالمراجع التي تشير إلى رموز غير مُعرَّفة (undefined symbols). يمكن تعريف تلك الرموز في ملفّات كائنات أخرى أو في مكتبات أخرى، وإذا عُرِّفت في مكتبات غير المكتبة القياسية، فعليك إخبار الرابط بذلك. وتكون الأخطاء الأكثر شيوعًا في هذه المرحلة هي التعاريف المفقودة أو التعاريف المكرّرة، والأولى تعني أنّه إمّا أنّ التعاريف غير موجودة (أي أنّها غير مكتوبة)، أو أنّ ملفات الكائنات أو المكتبات التي عُرِّفت فيها لم تُعط للرابط، أمّا الأخطاء الأخيرة فهي واضحة: إذ أنّها تعني أنّ نفس الرمز قد عُرِّف في ملفّين أو مكتبتين مختلفتين. التصريف عبر Code::Blocks (واجهة رسومية) نزِّل Code::Blocks وثبِّته، وإذا كنت تستخدم ويندوز فاحرص على اختيار الملفّ الذي يحتوي اسمُه على ‎mingw‎، إذ لا تثبّت الملفّات الأخرى أي مُصرِّفات. افتح Code::Blocks وانقر على "Create a new project": اختر "Console application" وانقر على "Go": انقر على"Next"، واختر "C++‎"، انقر على "Next"، اختر اسمًا لمشروعك، واختر مجلدًا لحفظه، ثمّ انقر على "Next" ثمّ على "Finish". يمكنك الآن تعديل وتصريف شيفرتك، ستجد شيفرة افتراضية تطبع السلسلة النصية "Hello world!" في سطر الأوامر. اضغط على أحد الأزرار الثلاثة compile/run في شريط الأدواتل تصريف و/أو تشغيل البرنامج: للتصريف دون تشغيل، اضغط على ، وللتشغيل دون التصريف مرّة أخرى، اضغط على ، وللتصريف ثمّ التشغيل اضغط على . تصريف وتشغيل البرنامج الافتراضي "مرحبا العالم!" سيُنتِج الخرج التالي: مواصفات الربط Linkage specifications تخبر مواصفات الربط (linkage specification) المُصرِّفَ بأن يصرِّف التصريحات بطريقة تسمح بربطها بالتصريحات المكتوبة بلغة أخرى، مثل C. معالج الإشارات الخاصّ بأنظمة التشغيل المشابهة ليونيكس بما أن معالج الإشارة (signal handler) يُستدعى عن طريق النواة (kernel) باستخدام صيغة الاستدعاء في لغة C، فيجب أن نطلب من االمُصرِّف أن يستخدم تلك الصيغة عند تصريف الدالّة. volatile sig_atomic_t death_signal = 0; extern "C" void cleanup(int signum) { death_signal = signum; } int main() { bind(...); listen(...); signal(SIGTERM, cleanup); while (int fd = accept(...)) { if (fd == -1 && errno == EINTR && death_signal) { printf("Caught signal %d; shutting down\n", death_signal); break; } // ... } } إنشاء مكتبة بلغة C متوافقة مع C++‎ يمكن في العادةً تضمين ترويسات مكتبة C في برامج C++‎، لأنّ معظم التصريحات صالحة في كلا اللغتين. على سبيل المثال، انظر ملف ‎foo.h‎ التالي: typedef struct Foo { int bar; } Foo; Foo make_foo(int); سيصرَّف تعريف ‎make_foo‎ بشكل منفصل ويُوزّع مع الترويسة على هيئة كائن. يمكن لبرنامج C++‎ أن يُضمِّن ملف الترويسة ‎foo.h‎ عبر التعبير ‎#include <foo.h>‎، لكن لن يستطيع المُصرِّف أن يعرف أنّ الدالّة ‎make_foo‎ قد عُرِّفت وفق صيغة مكتوبة بلغة C، وسيحاول البحث عنه باسم آخر، وسيفشل في إيجاده. وحتى لو استطاع إيجاد تعريف ‎make_foo‎ في المكتبة، فليست كل المنصّات تستخدم نفس صيغات الاستدعاء في C و C++‎، وسوف يستخدم مصرّف C++‎ صيغة الاستدعاء الخاصة بـ C++‎ عند استدعاء ‎make_foo‎، وهذا من شأنه أن يتسبّب في حدوث خطأ تجزئة (segmentation fault) في حال كان يُتوقّع أن تُستدعى ‎make_foo‎ وفق صيغة الاستدعاء في C. يمكن حلّ هذه المشكلة عبر تغليف (wrap) كافة التصريحات في ترويسة في كتلة ‎extern "C"‎، انظر .. #ifdef __cplusplus extern "C" { #endif typedef struct Foo { int bar; } Foo; Foo make_foo(int); #ifdef __cplusplus } /* "extern C" نهاية الكتلة */ #endif والآن، عند تضمين ‎foo.h‎ من برنامج C، سيظهر كتصريح عادي، أمّا عند تضمين ‎foo.h‎ من برنامج مكتوب بلغة C++‎، فسيكون ‎make_foo‎ داخل كتلة ‎extern "C"‎، وسيعرف المصرّفُ كيف يبحث عن الأسماء، وسيستخدم صيغة الاستدعاء في C. هذا الدرس جزء من سلسلة دروس عن C++‎. ترجمة -بتصرّف- للفصل Chapter 138: Compiling and Building والفصل Chapter 134: Linkage specifications من كتاب C++ Notes for Professionals
  5. محدّدات أصناف التخزين هي كلمات مفتاحية يمكن استخدامها في التصريحات، ولا تؤثر على نوع التصريح لكنها تعدّل الطريقة التي تُخزّن بها الكيانات. extern محدّدُ صنف التخزين ‎extern‎ يستطيع التصريحَ بإحدى الطرق الثلاث التالية، وذلك وفقًا للسياق، فمثلًا: يمكن استخدامه للتصريح عن متغيّر دون تعريفه، يُستخدَم عادة في ملفات الترويسات الخاصة بالمتغيّرات التي تُعرّف في ملف تنفيذ منفصل. // نطاق عام int x; // بالقيمة الافتراضية x تعريف: سيهيأ extern int y; // معرَّف في مكان آخر، غالبًا في وحدة ترجمة أخرى y :تصريح. extern int z = 42; // أيّ تأثير هنا "extern" تعريف: ليس للكلمة المفتاحية إعطاء ارتباط خارجي (external linkage) لمتغيّر في نطاق فضاء الاسم، حتى لو كانت الكلمتان المفتاحيتان ‎const‎ و ‎constexpr‎ تتسبّبان في إنشاء ارتباط داخلي. // النطاق العام const int w = 42; // C وخارجي في ،C++ ارتباط داخلي في static const int x = 42; // C++ و C ارتباط داخلي في كل من extern const int y = 42; // C++ و C ارتباط خارجي في كل من namespace { extern const int z = 42; // ارتباط داخلي لأنّه في فضاء اسم غير مُسمّى } إعادة التصريح (redeclaring) عن متغيّر في نطاق الكتلة إن صُرِّح عنه مسبقًا عبر الارتباط (linkage). أما خلاف ذلك، سيُصرّح عن متغيّر جديد عبر الارتباط، وسيكون عضوًا في أقرب فضاء اسم محيط. // النطاق العام namespace { int x = 1; struct C { int x = 2; void f() { extern int x; // x إعادة التصريح عن std::cout << x << '\n'; // طباعة 1 وليس 2 } }; } void g() { extern int y; // العامّة المُعرّفة في موضع آخر y ارتباط خارجي، ويشير إلى قيمة y لدى } يمكن أيضًا التصريح عن الدالّة على أنها ‎extern‎، لكنّ هذا ليس له أيّ تأثير وإنّما يُستخدم في العادةً كتلميح للقارئ بأنّ الدالّة المُصرَّح عنها هنا ستعُرَّف في وحدة ترجمة أخرى. ففي المثال التالي، يكون ()void f تصريحًا لاحقًا يعني أن f ستُعرَّف في وحدة الترجمة هذه، أما ()extern void g ليس تصريحًا لاحقًا، مما يعني أن g تُعرَّف في وحدة ترجمة أخرى، انظر: void f(); extern void g(); في الشيفرة أعلاه، في حال التصريح عن ‎f‎ مع ‎extern‎ والتصريح عن ‎g‎ بدون ‎extern‎، لن يؤثر ذلك على صحة أو دلالات البرنامج، ولكن من المحتمل أن يربك القارئ. register الإصدار < C++‎ 17 ++‎> register هو محدّد صنف تخزين يخبر االمُصرِّف أنّ المتغيّر سيُستخدَم بكثرة، وتأتي الكلمة "register" من حقيقة أن المُصرِّف قد يختار تخزين هذا المتغيّر في سجل وحدة المعالجة المركزية (CPU register) لتسريع الوصول إليه. وقد أُهمِلت بدءًا من الإصدار C++‎ 11. register int i = 0; while (i < 100) { f(i); int g = i * i; i += h(i, g); } يُمكن تعريف كلٍّ من المتغيّرات المحلية ومُعامِلات الدوالّ بالكلمة المفتاحية ‎register‎، ولا تضع C++‎ قيودًا على ما يمكن فعله بالمتغيّرات المعرّفة بـ ‎register‎، على عكس لغة C. فكثلًا، يجوز أخذ عنوان متغيّر مصرّح عنه بـ ‎register‎، لكنّ هذا قد يمنع المُصرِّف من تخزين مثل هذا المتغيّر في السجل (register). الإصدار ≥ C++‎ 17 الكلمة المفتاحية ‎register‎ غير مستخدمة ومحفوظة، ولا ينبغي استخدامها. static لدى محدّد التخزين ‎static‎ ثلاث معانٍ مختلفة. يعطي ارتباطًا داخليًا لمتغيّر أو دالّة مُصرَّح عنها في نطاق فضاء الاسم. // دالة داخلية، لا يمكن الارتباط بها static double semiperimeter(double a, double b, double c) { return (a + b + c) / 2.0; } // التصدير إلى العميل double area(double a, double b, double c) { const double s = semiperimeter(a, b, c); return sqrt(s * (s - a) * (s - b) * (s - c)); } تصرّح بأنّ المتغيّر له مدة تخزين ساكنة - static storage duration - (ما لم تكن خيطًا محليًا ‎thread_local‎)، ومتغيرات نطاق فضاء الاسم تكون ساكنة ضمنيًا، وتُهيّأ المتغيّرات المحلية الساكنة مرّة واحدة فقط، إذ يمرّ التحكّم في المرّة الأولى عبر تعريفها، ولا تُدمَّر بعد الخروج من نطاقها. void f() { static int count = 0; std::cout << "f has been called " << ++count << " times so far\n"; } عند تطبيقها على تصريح عن عضو صنف (class member)، فإنّها تجعل ذلك العضو ساكنًا. struct S { static S* create() { return new S; } }; int main() { S* s = S::create(); } لاحظ أنّه إن كان العضو ساكنًا، ستَنطبق النقطتان 2 و 3 في نفس الوقت، إذ تضع الكلمة المفتاحية ‎static‎ عضو الصنف في عضو بيانات ساكن (static data member)، كما تجعله أيضًا في متغيّر ذي مدة تخزين ساكنة (static storage duration). auto الإصدار ≤ C++‎ 03 يصرّح هذا المحدّد بأنّ المتغيّر لديه مدة تخزين آلية (automatic storage duration)، وهو غير ضروري نظرًا لأنّ مدة التخزين التلقائي هي الإعداد الافتراضي في نطاق الكتلة، كما لا يُسمح باستخدام المحدّد auto في نطاق فضاء الاسم. void f() { auto int x; auto y; auto int z; في المثال السابق، auto int x تكافئ int x، ولا تجوز auto y في ++C لكنها جائزة في C89، كذلك لا تجوز auto int z، إذ لا يمكن أن تكون متغيرات نطاق فضاء الاسم آلية. تغيّر معنى ‎auto‎ في الإصدار C++‎ 11 بشكل كامل، ولم تعد محدّدًا للتخزين، وإنّما صارت تُستخدم في استنتاج الأنواع. mutable mutable هو محدّدٌ يمكن تطبيقه على تصريح عضو بيانات من صنف غير ساكن (non-static) وغير مرجعي (non-reference)، والعضو القابل للتغيير في صنفٍ لا يكون غيرَ ثابتٍ حتى لو كان الكائن ثابتًا. class C { int x; mutable int times_accessed; public: C(): x(0), times_accessed(0) {} int get_x() const { ++times_accessed; // لا بأس: يمكن للدوال التابعة الثابتة أن تعدّل أعضاء البيانات غير الثابتة return x; } void set_x(int x) { ++times_accessed; this -> x = x; } }; الإصدار ≥ C++‎ 11 أضيف معنى ثانٍ للكلمة المفتاحية ‎mutable‎ في C++‎ 11، فعندما تتبع قائمة المُعاملات الخاصة بتعبير لامدا، فإنها تقمع المؤهّل الثباتي ‎const‎ الضمني الموجود في مُعامل استدعاء لامدا (lambda's function call operator). ولذلك يمكن لتعابير لامدا القابلة للتغيير (mutable) أنّ تعدّل قيم الكيانات التي حصلت عليها عن طريق النسخ. std::vector < int > my_iota(int start, int count) { std::vector < int > result(count); std::generate(result.begin(), result.end(), [start]() mutable { return start++; }); return result; } لاحظ أنّ ‎mutable‎ لا تُعدُّ محدّدَ صنف تخزين إذا استخدِمَت بهذه الطريقة لتشكيل تعابير لامدا تقبل للتغيير (أي mutable). هذا الدرس جزء من سلسلة دروس عن C++‎. ترجمة -بتصرّف- للفصل Chapter 133: Storage class specifiers من كتاب C++ Notes for Professionals
  6. إدارة الموارد هي إحدى أصعب الأشياء في C و C++‎، لكن C++‎ توفّر لنا العديد من الطرق التي نصمم بها إدارة الموارد في برامجنا، وسنحاول في هذا المقال أن نشرح بعض هذه الطرق. تقنية RAII: تخصيص الموارد يكافئ التهيئة تقنية RAII وتدعى اكتساب أو تخصيص الموارد هي تهيئة (Resource Acquisition Is Initialization) هو مصطلح شائع في إدارة الموارد، ويستخدم المؤشّرات الذكية (smart pointer) لإدارة الموارد في حالة الذاكرة الديناميكية، وتُمنح الموارد المكتسبة ملكية مؤشر ذكي أو مدير موارد مكافئ مباشرة عند استخدام أسلوب RAII. ولا يمكن الوصول إلى المورد إلا من خلال ذلك المدير، مما يمكِّن المدير من تتبّع مختلف العمليات الجارية على المورد. على سبيل المثال، تحرِّر الكائنات من النوع ‎std::auto_ptr‎ مواردها تلقائيًا عندما تخرج عن النطاق، أو عندما تُحذف بطريقة أخرى. #include <memory> #include <iostream> using namespace std; int main() { { auto_ptr ap(new int(5)); cout << *ap << endl; // تطبع 5 } } في المثال السابق، كان المورد هو الذاكرة الديناميكية، ودُمِّر auto-ptr ثم حُرِّر مورده تلقائيًا. الإصدار ≥ C++‎ 11 كانت المشكلة الرئيسية في كائنات std::auto_ptr أنّها لا يمكن أن تُنسخ دون نقل الملكية: #include <memory> #include <iostream> using namespace std; int main() { auto_ptr ap1(new int(5)); cout << *ap1 << endl; // تطبع 5 auto_ptr ap2(ap1); cout << *ap2 << endl; // تطبع 5 cout << ap1 == nullptr << endl; } تفسير الشيفرة السابقة: (auto_ptr ap2(ap1: تنسخ ap2 من ap1 وتنتقل الملكية إلى ap2. cout << ap1 == nullptr << endl: تطبع القيمة 1، ويخسر ap1 ملكيته للمورد. وبسبب دلالات النسخ الغريبة تلك فإنّ هناك قيودًا على استخدام ‎std::auto_ptr‎، مثل أنها لا يمكن أن تُستخدم في الحاويات، وذلك لمنع حذف الذاكرة مرتين: فإذا كان لدينا كائنين من النوع ‎auto_ptrs‎ يملكان نفس المورد، سيحاول كلا الكائنين تحريره عند تدميرهما. ومن المهم منع تحرير المورد الذي سبق تحريره من قبل، إذ سيؤدي ذلك التحرير الثاني إلى حدوث مشاكل، لكن بأي حال فإن std::shared_ptr لديه طريقة لتجنب ذلك، مع عدم نقل الملكية أثناء النسخ. #include <memory> #include <iostream> using namespace std; int main() { shared_ptr sp2; { shared_ptr sp1(new int(5)); // sp1 إعطاء الملكيّة إلى cout << *sp1 << endl // تطبع 5 sp2 = sp1; // يملك كلاهما المورد ،sp1 من sp2 نسخ cout << *sp1 << endl; // تطبع 5 cout << *sp2 << endl; // تطبع 5 } // الملكية الحصرية للمورد sp2 عن النطاق وتدميره، أصبح لـ sp1 خروج cout << *sp2 << endl; } // عن النطاق، وتحرير المورد sp2 خروج القفل Locking هذا مثال عن قفل سيّء: std::mutex mtx; void bad_lock_example() { mtx.lock(); try { foo(); bar(); if (baz()) { mtx.unlock(); // ينبغي فتح القفل عند كل نقطة خروج return; } quux(); mtx.unlock(); // يحدث فتح القفل العادي هنا } catch (...) { mtx.unlock(); // ينبغي فرض فتح القفل في حال طرح اعتراض throw; // والسماح للاعتراض بالاستمرار } } تلك طريقة خاطئة لتنفيذ عمليتي القفل والفتح لكائنات المزامنة (mutex)، ولا بدّ أن يتحقّق المُبرمِجُ من أنّ جميع التدفّقات (flows) الناتجة عن إنهاء الدالّة تؤدّي إلَى استدعاء ‎unlock()‎، وذلك للتأكد أنّ فتح القفل باستخدام ‎unlock()‎ سيحرّر الكائن المزامنة الصحيح. وهذه عمليات هشة كما وضحنا أعلاه، لأنّها تتطّلب من المطوّرين متابعة النمط يدويًا. ويمكن حلّ هذه المشكلة باستخدام صنف مُصمّم خصّيصًا لتنفيذ تقنية RAII: std::mutex mtx; void good_lock_example() { std::lock_guard<std::mutex > lk(mtx); // المنشئ يقفل. // المدمِّر يفتح! // تضمن اللغة استدعاء المدمر. foo(); bar(); if (baz()) { return; } quux(); } ‎lock_guard‎ هو قالب صنف بسيط للغاية يستدعي ‎lock()‎ ويمرّر إليه الوسيط المُمرّر إلى مُنشئه ويحتفظ بمرجع إلى ذلك الوسيط، ثمّ يستدعي ‎unlock()‎ على الوسيط في مدمّره، ممّا يعني ضمان فتح قفل mutex عند خروج ‎lock_guard‎ عن النطاق. ولا يهم سبب الخروج عن النطاق سواءً كان ذلك بسبب اعتراض أو عودة مبكّرة، ذلك أن جميع الحالات سيتمّ التعامل معها، وسيفتح القفل بشكل صحيح بغض النظر عن سير البرنامج. ScopeSuccess الإصدار ≥ C++‎ 17 نستطيع باستخدام ‎int std::uncaught_exceptions()‎ أن ننفذ إجراءً لم يكن لينفَّذ إلّا في حالة النجاح (عدم رفع اعتراض في النطاق). وقد كانت ‎bool std::uncaught_exception()‎ فيما سبق تسمح برصد العمليّات الجارية لفكّ المكدّس (stack unwinding). انظر: #include <exception> #include <iostream> template < typename F> class ScopeSuccess { private: F f; int uncaughtExceptionCount = std::uncaught_exceptions(); public: explicit ScopeSuccess(const F &f): f(f) {} ScopeSuccess(const ScopeSuccess &) = delete; ScopeSuccess &operator=(const ScopeSuccess &) = delete; // f() might throw, as it can be caught normally. ~ScopeSuccess() noexcept(noexcept(f())) { if (uncaughtExceptionCount == std::uncaught_exceptions()) { f(); } } }; struct Foo { ~Foo() { try { ScopeSuccess logSuccess { []() { std::cout << "Success 1\n"; } }; // نجاح النطاق // أثناء فكّ المكدّس Foo حتى في حال تدمير // 0 < std::uncaught_exceptions() // std::uncaught_exception() == true } catch (...) {} try { ScopeSuccess logSuccess { []() { std::cout << "Success 2\n"; } }; تزيد القيمة المعادة من std::uncaught_exceptions، … throw std::runtime_error("Failed"); } وتنقص القيمة المعادة من std::uncaught_exceptions … catch (...) { } } }; int main() { try { Foo foo; throw std::runtime_error("Failed"); // std::uncaught_exceptions() == 1 } catch (...) { // std::uncaught_exceptions() == 0 } } سيكون الخرج: Success 1 ScopeFail ‏(C++‎ 17) الإصدار ≥ C++‎ 17 يمكننا تنفيذ إجراء معيّن لا يُنفَّذ إلّا عند الفشل (رفع اعتراض في النطاق) بفضل ‎int std::uncaught_exceptions()‎، وكانت ‎bool std::uncaught_exception()‎ تسمح سابقًا برصد إن كانت هناك عملية جارية لفك المكدّس. #include <exception> #include <iostream> template < typename F> class ScopeFail { private: F f; int uncaughtExceptionCount = std::uncaught_exceptions(); public: explicit ScopeFail(const F &f): f(f) {} ScopeFail(const ScopeFail &) = delete; ScopeFail &operator=(const ScopeFail &) = delete; يجب ألا ترفع ()f وإلا فستستدعى std::terminate، نتابع المثال … ~ScopeFail() { if (uncaughtExceptionCount != std::uncaught_exceptions()) { f(); } } }; struct Foo { ~Foo() { try { ScopeFail logFailure { []() { std::cout << "Fail 1\n"; } }; // نجاح النطاق // أثناء فكّ المكدّس Foo حتى في حال تدمير // 0 < std::uncaught_exceptions() في حال // std::uncaught_exception() == true أو سابقا } catch (...) {} try { ScopeFail logFailure { []() { std::cout << "Failure 2\n"; } }; تزيد القيمة المعادة من std::uncaught_exceptions …. throw std::runtime_error("Failed"); } تقل القيمة المعادة من std::uncaught_exceptions …. catch (...) { } } }; int main() { try { Foo foo; throw std::runtime_error("Failed"); // std::uncaught_exceptions() == 1 } catch (...) { // std::uncaught_exceptions() == 0 } } سيكون الخرج: Failure 2 Finally/ScopeExit إذا لم ترد كتابة أصناف خاصّة للتعامل مع بعض الموارد، فاكتب صنفًا عامًا على النحو التالي: template < typename Function> class Finally final { public: explicit Finally(Function f): f(std::move(f)) {} ~Finally() { f(); } // (1) انظر أدناه Finally(const Finally &) = delete; Finally(Finally &&) = default; Finally &operator=(const Finally &) = delete; Finally &operator=(Finally &&) = delete; private: Function f; }; // عندما يخرج الكائن المُعاد عن النطاق f تنفيذ الدالة template < typename Function> auto onExit(Function && f) { return Finally<std::decay_t < Function>> { std::forward<Function> (f) }; } وهذا مثال على استخدام ذلك الصنف: void foo(std::vector<int> &v, int i) { // ... v[i] += 42; auto autoRollBackChange = onExit([ &]() { v[i] -= 42; }); // ... `foo(v, i + 1)` شيفرة تكرارية } ملاحظة (1): يجب أخذ الملاحظات التالية حول تعريف المدمّر في الاعتبار عند التعامل مع الاعتراضات: Finally() noexcept { f(); }: std::terminate // تُستدعى في حال رفع اعتراض Finally() noexcept(noexcept(f())) { f(); } // إلّا في حال رفع اعتراض أثناء فك المكدّس terminate() لا تُستدعى Finally() noexcept { try { f(); } catch (...) { /* ignore exception (might log it) */} } ) // لا تُستدعى std::terminate، لكن لا نستطيع معالجة الخطأ (حتى في حالة عدم فك المكدّس كائنات المزامنة وأمان الخيوط Mutexes & Thread Safety قد تحدث مشكلة إن حاولت عدّة خيوط الوصول إلى نفس المورد، فلنفترض مثلًا أنّ لدينا خيطًا يضيف العدد 1 إلى متغيّر. فأوّل شيء يفعله هو أنّه يقرأ المتغيّر، ثم يضيف واحدًا إليه، ثم يعيد تخزينه مرّة أخرى. لنفترض الآن أنّنا هيّأنا ذلك المتغيّر عند القيمة 1، ثم أنشأنا نُسختين من ذلك الخيط، فبعد أن ينتهي كلا الخيطين، نتوقّع أن تكون قيمة ذلك المتغيّر هي 3، لكن هناك بعض المشاكل التي قد تحدث هنا، سنفصلها في الجدول التالي: الخيط 1 الخيط 2 المرحلة 1 قراءة 1 من المتغير المرحلة 2 قراءة 1 من المتغير المرحلة 3 إضافة 1 إلى 1 للحصول على 2 المرحلة 4 إضافة 1 إلى 1 للحصول على 2 المرحلة 5 تخزين 2 في المتغير المرحلة 6 تخزين 2 في المتغير table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; } في نهاية العملية، تُخزّن القيمة 2 في المتغيّر بدلًا من 3، ذلك أن الخيط 2 يقرأ المتغيّر قبل أن يُحدِّث الخيط 1 ذلك المتغيّر. ما الحلّ إذن؟ يكون الحل في كائنات المزامنة … . كائن المزامنة (mutex) هو كائن لإدارة الموارد، ومُصمّم لحل هذا النوع من المشاكل. فعندما يحاول خيط ما الوصول إلى مورد، فإنّه يستحوذ على كائن المزامنة لذلك المورد (resource's mutex). ويحرّر ذلك (releases) الخيطُ كائن المزامنة بمجرّد الانتهاء من العمل على المورد. وعند استحواذ خيط على كائن مزامنةٍ فإنّ كلّ الاستدعاءات للاستحواذ على ذلك الكائن لن تعود إلى أن يُحرَّر. لفهم ذلك بشكل أفضل، فكّر في كائن المزامنة كطابور انتظار في متجر كبير، إذ تنتقل الخيوط في الطابور لمحاولة الاستحواذ على كائن المزامنة، ثم تنتظر الخيوط التي تسبقُها حتى تنتهي، ثم تستخدم المورد، ثم تخرج عن الطابور عن طريق تحرير كائن المزامنة. فلو حَاول الجميع الوصول إلى المورد في نفس الوقت ستحدث فوضى كبيرة. الإصدار ≥ C++‎ 11 المكتبة std::mutexهي تطبيق كائن المزامنة في C++‎ 11. #include <thread> #include <mutex> #include <iostream> using namespace std; void add_1(int& i, const mutex& m) { // الدالة التي ستُنفّذ في الخيط m.lock(); i += 1; m.unlock(); } int main() { int var = 1; mutex m; cout << var << endl; // تطبع 1 thread t1(add_1, var, m); // إنشاء خيط مع وسائط thread t2(add_1, var, m); // إنشاء خيط آخر t1.join(); t2.join(); // انتظار انتهاء الخيطين cout << var << endl; // تطبع 3 } هذا الدرس جزء من سلسلة دروس عن C++‎. ترجمة -بتصرّف- للفصل Chapter 83: RAII: Resource Acquisition Is Initialization والفصل Chapter 132: Resource Management من كتاب C++ Notes for Professionals
  7. لغة C++‎، على غرار C، لها تاريخ طويل ومتنوّع بخصوص سير العمل التصريفي وعمليات البناء. واليوم، لدى لغة C++‎ العديد من أنظمة البناء التي تُستخدَم لتصريف البرامج لعدة منصات أحيانًا داخل نظام بناء واحد. وسننظر في هذا الدرس في بعض أنظمة البناء ونحلّلها. إنشاء بيئة بناء باستخدام CMake ينشئ CMake بيئات البناء للمُصرِّفات أو بيئات التطوير المتكاملة (IDE) انطلاقًا من تعريف مشروع واحد، وسوف توضّح الأمثلة التالية كيفية إضافة ملف CMake إلى برنامج "Hello World" متعدّد المنصات. تأخذ ملفات CMake دائمًا الاسم "CMakeLists.txt"، ويجب أن تكون موجودة في مجلّد الجذر لكل مشروع، وربما في المجلدات الفرعية أيضا. هذا مثال على ملف CMakeLists.txt: cmake_minimum_required(VERSION 2.4) project(HelloWorld) add_executable(HelloWorld main.cpp) يمكنك رؤية البرنامج يعمل مباشرة من هنا. هذا الملفُّ يخبر أداةَ CMake باسم المشروع، وإصدار الملف المتوقع، وبعض التعليمات الضرورية لتوليد ملف تنفيذي (executable) يُسمّى "HelloWorld"، والذي يتطلب ملف ‎main.cpp‎. في المثال التالي: أنشئ بيئة بناء للمُصرِّف أو بيئة تطوير متكاملة من سطر الأوامر: > cmake . ابن التطبيق بما يلي: > cmake --build . تولّد هذه الشيفرة بيئة البناء الافتراضية للنظام وفق نظام التشغيل والأدوات المُثبّتة، حافظ على الشيفرة المصدرية نظيفة من آثار البناء (build artifacts) باستخدام نمط تصميم "خارج المصدر" (out-of-source): > mkdir build > cd build > cmake .. > cmake --build . يمكن لـ CMake أيضًا تجريد (abstract) الأوامر الأساسية للصدفة (shell) الخاصة بالمنصة من المثال السابق: > cmake -E make_directory build > cmake -E chdir build cmake .. > cmake --build build يتضمّن CMake مولدّات لعدد من أدوات البناء الشائعة وبيئات التطوير المتكاملة، ولإنشاء ملفات makefiles لأجل nmake الخاصة بـ Visual Studio: > cmake -G "NMake Makefiles" .. > nmake التصريف باستخدام GNU GNU Make هو برنامج مخصّص لأتمتة تنفيذ أوامر الصدفة، وينتمي لعائلة برامج Make، ويتمتع Make بشعبية كبيرة لدى مستخدمي أنظمة التشغيل الشبيهة بيونكس وتلك الموافقة لمعايير POSIX، بما في ذلك الأنظمة المشتقة من نواة لينُكس ونظام Mac OSX ونظام BSD، ويتميّز GNU Make بأنّه مرتبط بمشروع جنو الذي يرتبط بدوره بأنظمة التشغيل المبنية على جنو\لينكس الشهير. هناك إصدارات أخرى من GNU Make متوافقة مع أنظمة تشغيل أخرى، مثل ويندوز و Mac OS X، وهذا البرنامج مستقر للغاية وله أثر تاريخي يبقيه مشهورًا، لهذا يُدرَّس GNU Make إلى جانب لغتي C و C++‎. القواعد الأساسية يجب أن تنشئ ملف Makefile في مجلّد المشروع لأجل التصريف بواسطة make. هذا مثال بسيط على ملف Makefile: سنقسم الملف أثناء سرد المثال للشرح …. Makefile # إعداد بعض المتغيرات لاستخدامها مع الأمر # g++ أولا، سنحدد المصرف CXX=g++ # وغيرها g++ ثم نصرّف مع التنبيهات الموصى بها في CXXFLAGS=-Wall -Wextra -pedantic # هذا هو ملف الخرج EXE=app SRCS=main.cpp يُستدَعى هذا الهدف عند استدعاء make في سطر الأوامر، وتقول (EXE)$ الموجودة على اليمين أن الهدف all يعتمد على الهدف (EXE)$. لاحظ أنه بما أن هذا هو الهدف الأول، فسيكون الهدف الافتراضي إن استُدعيَت make بدون هدف، نتابع المثال … all: $(EXE) # هذا يكافئ # app: $(SRCS) (SRCS)$ يمكن فصلها، مما يعني أن هذا الهدف سيعتمد على كل الملفات. لاحظ أن هذا الهدف له متن تابع (method body)، وهو الجزء المزاح بمسافة جدول واحدة (tab) وليس أربع مسافات فارغة. وسينفذ make الأمر التالي عند بناء هذا الهدف: g++ -Wall -Wextra -pedantic -o app main.cpp والذي يعني تصريف main.cpp مع التحذيرات، والإخراج إلى الملف app/.، نتابع … $(EXE): $(SRCS) @$(CXX) $(CXXFLAGS) -o $@ $(SRCS) هذا الهدف ينبغي له أن يعكس الهدف all، وإن استدعيْتَ make مع وسيط مثل make clean فسيُستَدعى الهدف gets الموافق له، … clean: @rm -f $(EXE) ملاحظة: تأكد من أن الإزاحات البادئة قد تمت بزر الجدول (tab)، ولا تستخدم 4 مسافات بيضاء من زر Space. وإلا فسيُطلق الخطأ ‎Makefile:10: *** missing separator. Stop.‎. لتنفيذ هذه الشيفرة من سطر الأوامر، عليك كتابة ما يلي: $ cd ~/Path/to/project $ make $ ls app main.cpp Makefile $ ./app Hello World! $ make clean $ ls main.cpp Makefile عمليات البناء التزايدية التزايدي Incremental builds تظهر فائدة make عندما تكثر الملفات، فماذا لو عدّلت الملف a.cpp دون الملف b.cpp؟ لن يكون من الحكمة إذًا أن تعيد تصريف b.cpp، لأنّ ذلك سيُهدر الوقت. في المثال أدناه، لنفرض أن لدينا الهيكل التالي لمجلد: . +-- src | +-- a.cpp | +-- a.hpp | +-- b.cpp | +-- b.hpp +-- Makefile فسيكون هذا ملف Makefile جيد: CXX=g++ CXXFLAGS=-Wall -Wextra -pedantic EXE=app SRCS_GLOB=src/*.cpp SRCS=$(wildcard $(SRCS_GLOB)) OBJS=$(SRCS:.cpp=.o) all: $(EXE) $(EXE): $(OBJS) @$(CXX) -o $@ $(OBJS) depend: .depend .depend: $(SRCS) @-rm -f ./.depend @$(CXX) $(CXXFLAGS) -MM $^>>./.depend clean: -rm -f $(EXE) -rm $(OBJS) -rm *~ -rm .depend include .depend مرّة أخرى، لاحظ إزاحات الجدول البادئة. سيضمن ملفّ Makefile الجديد ألّا تصرّف إلّا الملفات التي عُدِّلت، وهذا سيقلّل وقت التصريف. التوثيق للحصول على المزيد من المعلومات عن make، راجع التوثيق الرسمي -بالإنجليزية- من مؤسسة البرمجيات الحرة وإجابة dmckee التفصيلية على موقع stackoverflow. البناء بواسطة SCons يمكنك بناء شيفرة "Hello World" متعدّدة المنصات باستخدام أداة Scons، وهي أداة بناء برامج تستخدم لغة بايثون. أولاً، عليك إنشاء ملف يُسمّى ‎SConstruct‎ -سيبحث SCons عن هذا الملف افتراضيًا-، وينبغي أن يكون الملف -للآن- في مجلّد مع الملف ‎hello.cpp‎. اكتب السطر التالي في الملف الجديد: Program('hello.cpp') الآن، من سطر الأوامر، نفّذ الأمر ‎scons‎، ينبغي أن ترى شيئًا من هذا القبيل: $ scons scons: Reading SConscript files ... scons: done reading SConscript files. scons: Building targets ... g++ -o hello.o -c hello.cpp g++ -o hello hello.o scons: done building targets. رغم أن التفاصيل ستختلف وفقًا لنظام التشغيل والمُصرِّف المُستخدم. سيساعدك الصنفان ‎Environment‎ و ‎Glob‎ على إعداد ما يجب بناؤه. على سبيل المثال، سيبني الملف ‎SConstruct‎ الملف التنفيذي hello باستخدام جميع ملفات cpp في مجلد src، كما أن CPPPATH الخاص به سيكون ‎/usr/include/boost‎، كما يحدّد معيار C++‎ 11، انظر: env=Environment(CPPPATH='/usr/include/boost/', CPPDEFINES=[], LIBS=[], SCONS_CXX_STANDARD="c++11" ) env.Program('hello', Glob('src/*.cpp')) Autotools هي مجموعة من البرامج التي تُستخدَم لإنشاء نظام بناء جنو (GNU Build System) لأجل حزمة برامج معيّنة، وهي حزمة من الأدوات تعمل معًا لإنتاج العديد من موارد البناء، مثل ملفات Makefile -لتُستخدَم مع برنامج GNU Make، لهذا تُعدُّ هذه الحزمة المولّد المعياري لأنظمة البناء. فيما يلي بعض برامج Autotools الأساسية: Autoconf. Automake (لا تخلط بينها وبين ‎make‎). والهدف من حزمة Autotools بشكل عام، هو توليد برنامج نصّي متوافق مع يونيكس (Unix-compatible script)، إضافة إلى ملفّ Makefile للسماح للأمر التالي بإنجاز عملية البناء -وكذلك تثبيت- معظم الحُزم: ./configure && make && make install وترتبط حزمة Autotools على هذا النحو أيضًا ببعض مدراء الحزم (package managers)، خاصة تلك المرتبطة بأنظمة التشغيل التي تتوافق مع POSIX. Ninja يوصف نظام بناء Ninja في موقعه بأنه "نظام بناء صغير يركّز على السرعة"، وصُمِّم هذا النظام لكي تُبنى ملفاته بواسطة مولدات ملفات بناء النظام (build system file generators)، ويعتمد أسلوبًا منخفض المستوى (low-level) لبناء الأنظمة، وذلك على عكس مدراء أنظمة البناء عالية المستوى مثل CMake أو Meson. كُتِب نظام Ninja أساسًا بلغة C++‎ و Python، وقد أُنشِئ كبديل لنظام البناء SCons في مشروع Chromium. NMAKE (أداة صيانة برامج Microsoft) NMAKE هي أداة سطر أوامر طوّرتها ميكروسوفت لاستخدامها مع Visual Studio و / أو أدوات سطر أوامر Visual C++‎. ويُنظر إليها على أنها نظام بناء يندرج ضمن مجموعة عائلة Make لأنظمة البناء، ولكنّه يتميّز ببعض الميزات الخاصّة التي تميّزه عن برامج Make الشبيهة بيونكس الأخرى، مثل دعم صياغة مسارات ملفات Windows (والتي تختلف عن مسارات الملفات في يونيكس). أخطاء المصرف الشائعة (GCC) مرجعّ غير معرَّف إلى "***" تحدث أخطاء الرابط (linker) عندما يعجز عن العثور عن رمز مُستخدم، ويحدث هذا غالبًا عند عدم ربط إحدى المكتبات المستخدمة. qmake: LIBS += nameOfLib cmake: TARGET_LINK_LIBRARIES(target nameOfLib) استدعاء g++‎: g++ -o main main.cpp -Llibrary/dir -lnameOfLib قد ينسى المرء أيضًا تصريف وربط جميع ملفّات ‎.cpp‎ المستخدمة (تحدّد functionModule.cpp الدالّة المطلوبة): g++ -o binName main.o functionsModule.o error: '***' was not declared in this scope يحدث هذا الخطأ في حال استخدام كائن غير معروف. أخطاء متعلقة بالمتغيّرات لن تُصرّف الشيفرة التالية، لأن المتغير i غير موجود في نطاق الدالة main: #include <iostream> int main(int argc, char *argv[]) { { int i = 2; } std::cout << i << std::endl; return 0; } الحل: #include <iostream> int main(int argc, char *argv[]) { { int i = 2; std::cout << i << std::endl; } return 0; } أخطاء متعلقة بالدوال يحدث هذا الخطأ غالبًا في حال عدم تضمين الترويسة المطلوبة (على سبيل المثال استخدام ‎std::cout‎ دون ‎#include <iostream>‎). ففي المثال التالي، لن تُصرّف الشيفرة التالية: #include <iostream> int main(int argc, char *argv[]) { doCompile(); return 0; } void doCompile() { std::cout << "No!" << std::endl; } الحلّ: #include <iostream> void doCompile(); // تصريح لاحق للدالة int main(int argc, char *argv[]) { doCompile(); return 0; } void doCompile() { std::cout << "No!" << std::endl; } أو: #include <iostream> void doCompile() // تعريف الدالة قبل استخدامها { std::cout << "No!" << std::endl; } int main(int argc, char *argv[]) { doCompile(); return 0; } ملاحظة: يفسر المُصرِّف الشيفرة من الأعلى إلى الأسفل (للتبسيط فقط)، لذا يجب التصريح عن كل شيء (أو تعريفه) قبل استخدامه. fatal error: ***: No such file or directory يحدث هذا الخطأ عندما يعجز المُصرِّف عن العثور عن ملفّ معيّن (ملف مصدري يستخدم ‎#include "someFile.hpp"‎). qmake: INCLUDEPATH += dir / Of / File cmake: include_directories(dir/Of/File) استدعاء g++‎: g++ -o main main.cpp -Idir/Of/File عدم التوافقية مع لغة C سنتحدّث في هذا القسم عن شيفرات C غير المتوافقة مع لغة C++‎ والتي ستعطل مصرّفاتها‎. الكلمات المفتاحية المحجوزة هناك كلمات مفتاحية لها غرض خاص في C++‎، فالشيفرة التالية مثلًا جائزة في C، لكن غير جائزة في C++‎. int class = 5 وإصلاح هذه الأخطاء سهل، فكل ما عليك هو إعادة تسمية المتغيّر. المؤشّرات ذات النوعية الضعيفة Weakly Typed Pointers يمكن تحويل المؤشّرات في C إلى النوع ‎void*‎، أما في C++‎ فستحتاج إلى تحويل صريح explicit cast. انظر المثال التالي حيث تكون الشيفرة غير جائزة في C++‎، ولكن جائزة في C: void* ptr; int* intptr = ptr; إضافة تحويل صريح (explicit cast) ستحل المشكلة لكن قد يسبّب مشاكل أخرى. goto أو switch لا يجوز في ++C أن تتخطي التهيئة باستخدام ‎goto‎ أو ‎switch‎. انظر المثال التالي حيث تكون الشيفرة صحيحة في C، وغير صحيحة في C++‎: goto foo; int skipped = 1; foo; قد يتطلب إصلاح هذه الأخطاء إعادة تصميم البرنامج. هذا الدرس جزء من سلسلة دروس عن C++‎. ترجمة -بتصرّف- للفصول Chapter 130: Build Systems Chapter 139: Common compile/linker errors (GCC)‎ Chapter 136: C incompatibilities من كتاب C++ Notes for Professionals
  8. الصحة الثباتية (Const Correctness) أسلوب لتصميم الشيفرات، يعتمد على فكرة أنّه لا ينبغي أن تُتاح إمكانية تعديل نسخة معيّنة -على سبيل المثال الحق في الكتابة- إلّا للشيفرَات التي تحتاج إلى تعديل تلك النسخة، وبالمقابل، فإنّ أيّ شيفرة لا تحتاج إلى تعديل نُسخة ما لن تملك القدرة على تعديلها، أي سيكون لها حق القراءة فقط. تصون هذه المقاربة النُسخة من أن تُعدَّل عن غير قصد، لأنّ ذلك قد يجعل الشيفرة غير موثوقة، كما يوضّح للمبرمجين الذي يقرؤون الشيفرة ما إذا كانت الشيفرة تهدف إلى تغيير حالة النُسخة أم لا. وتسمح أيضًا بالتعامل مع النسخ ككائنات ثابتة (‎const‎) عندما لا تكون هناك حاجة إلى تعديلها، أو تعريفها ككائنات ثابتة إذا لم تكن بحاجة إلى تغييرها بعد التهيئة، وذلك دون التأثير على وظيفتها. يتم ذلك عن طريق إعطاء التوابعِ مؤهلات (const CV-qualifiers)، وعن طريق وسم معاملات المؤشّرات / المراجع بالكلمة المفتاحية ‎const‎. class ConstCorrectClass { int x; public: int getX() const { return x; } // الدالة ثابتة: أي أنها لن تعدل النسخة void setX(int i) { x = i; } // غير ثابتة: أي أنها ستعدل النسخة }; // المعامل ثابت، أي أنّه لن يُعدّل int const_correct_reader(const ConstCorrectClass& c) { return c.getX(); } // المعامل غير ثابت: سيُعدَّل void const_correct_writer(ConstCorrectClass& c) { c.setX(42); } const ConstCorrectClass invariant; // النسخة ثابتة: لن تُعدّل ConstCorrectClass variant; // النسخة غير ثابتة: يمكن أن تُعدَّل // … // صحيح: استدعاء دالة ثابتة غير مُعدِّلة على نسخة ثابتة const_correct_reader(invariant); // صحيح: استدعاء دالة ثابتة غير مُعدِّلة على نسخة قابة للتعديل const_correct_reader(variant); // صحيح: استدعاء دالة ثابتة مُعدِّلة على نسخة قابلة للتعديل const_correct_writer(variant); // خطأ: استدعاء دالة ثابتة مُعدِّلة على نسخة ثابتة const_correct_writer(invariant); نظرًا لطبيعة الصحة الثباتية، فإنّها تبدأ بتوابع الصنف، ثمّ تستمر إلى الخارج، وسيطلق المصرِّف خطأً إذا حاولت استدعاء تابع غير ثابت من نُسخة ثابتة، أو من نُسخة غير ثابتة تُعامل كنسخة ثابتة. الصحة الثباتية في تصميم الأصناف جميع الدوال التابعة في صنف صحيح ثباتيًا (const-correct class)، التي لا تُغيّر الحالة المنطقية، يكون مؤشّر ‎this‎ الخاص بها ثابتًا، للدلالة على أنّها لن تُعدّل الكائن، لكن هذا لا يمنع أن تكون هناك حقول متغيرة - ‎mutable‎ - والتي يمكن تعديلها حتى لو كانت الكائنات التي تنتمي إليها ثابتة. عندما تعيد دالةٌ تأهيلها ثابت (‎const‎) مرجعًا، فينبغي أن يكون ذلك المرجع ثابتًا. يتيح هذا لها إمكانية أن تُستدعى على كلٍّ من النُسخ الثابتة، وكذلك النسخ غير المؤهّلة (non-cv-qualified)، إذ ستكون ‎const T*‎ قادرة على الارتباط بـ ‎T*‎ أو ‎const T*‎. هذا يسمح بدوره للدوالّ بأن تصرّح عن المُعاملات المُمرّة بالمرجع (passed-by-reference) على أنّها ثابتة عندما لا تحتاج إلى تعديل، وذلك دون التأثير على وظيفتها. علاوة على ذلك، في الأصناف الصحيحة ثباتيًا، تكون جميع مُعاملات الدالة المُمرّة بالمرجع صحيحة ثباتيًا، إذ لن يمكن تعديلها إلّا عندما تحتاج الدالّة إلى تعديلها صراحة. أولاً، لننظر إلى مؤهِّلات للمؤشّر ‎this‎ : لنفرض أن لدينا صنف Field، له الدالة التابعة ;(void insert_value(int class ConstIncorrect { Field fld; public: ConstIncorrect(Field& f); // يعدِّل Field& getField(); // قد يعدِّل، كما تُكشَف الأعضاء كمراجع غير ثابتة وذلك لإتاحة التعديل غير // المباشر void setField(Field& f); // يعدِّل void doSomething(int i); // قد يعدِّل void doNothing(); // قد يعدِّل }; ConstIncorrect::ConstIncorrect(Field& f): fld(f) {} // يعدِّل Field& ConstIncorrect::getField() { return fld; } // لا يعدِّل void ConstIncorrect::setField(Field& f) { fld = f; } // يعدِّل void ConstIncorrect::doSomething(int i) { // يعدِّل fld.insert_value(i); } void ConstIncorrect::doNothing() {} // لا يعدِّل class ConstCorrectCVQ { Field fld; public: ConstCorrectCVQ(Field& f); // يعدِّل const Field& getField() const; // لا يعدِّل، تُكشف الأعضاء كمراجع ثابتة لمنع التعديل غير المباشر void setField(Field& f); // يعدِّل void doSomething(int i); // يعدِّل void doNothing() const; // لا يعدِّل }; ConstCorrectCVQ::ConstCorrectCVQ(Field& f): fld(f) {} Field& ConstCorrectCVQ::getField() const { return fld; } void ConstCorrectCVQ::setField(Field& f) { fld = f; } void ConstCorrectCVQ::doSomething(int i) { fld.insert_value(i); } void ConstCorrectCVQ::doNothing() const {} هذا لن يعمل إذا لا يمكن استدعاء الدوال التابعة على نُسخ ConstIncorrect، نتابع … void const_correct_func(const ConstIncorrect& c) { Field f = c.getField(); c.do_nothing(); } أما هذا فيعمل إذ يمكن استدعاء ()doNothing و ()getField على نسخ ConstCorrectCVQ … void const_correct_func(const ConstCorrectCVQ& c) { Field f = c.getField(); c.do_nothing(); } يمكننا بعد ذلك دمج هذا مع معاملات الدوال الصحيحة ثباتيا (‎Const Correct Function Parameters‎)، وسيجعل هذا الصنفَ صحيحًا ثباتيًا بشكل كامل. class ConstCorrect { Field fld; public: ConstCorrect(const Field& f); // تعدّل النسخة، ولكن لا تعدّل المعامل const Field& getField() const; // لا تعدِّل، وتكشف العضو كمرجع ثابت لمنع التعديل غير المباشر void setField(const Field& f); // تعدّل النسخة، ولكن لا تعدّل المعامل void doSomething(int i); // passed by value تعدّل، لكن لا تعدّل المعامل - ممرر بالقيمة void doNothing() const; // لا تعدّل }; ConstCorrect::ConstCorrect(const Field& f) : fld(f) {} Field& ConstCorrect::getField() const { return fld; } void ConstCorrect::setField(const Field& f) { fld = f; } void ConstCorrect::doSomething(int i) { fld.insert_value(i); } void ConstCorrect::doNothing() const {} يمكن أيضًا دمج هذا مع التحميل الزائد على أساس الصحة الثباتية إذا أردنا سلوك ما عندما تكون النُسخة ثابتة، وسلوكًا آخر مختلفًا في الحالات الأخرى. وإحدى الاستخدامات الشائعة لهذا هي الحاويات (constainers)، إذ توفّر دوال وصول (accessors) لا تسمح بالتعديل إلّا إن كانت الحاوية نفسها غير ثابتة. class ConstCorrectContainer { int arr[5]; public: // معامل تسجيل يوفر إمكانية القراءة إن كانت النسخة ثابتة، أو إمكانية القراءة/الكتابة خلاف ذلك int& operator[](size_t index) { return arr[index]; } const int& operator[](size_t index) const { return arr[index]; } // ... }; يُستخدم هذا كثيرًا في المكتبة القياسية، إذ توفّر معظم الحاويات تحميلات زائدة لأخذ الثباتية في الحسبان. معاملات الدوال الصحيحة ثباتيًا إذا كانت دالّة ما صحيحة ثباتيًا، فإنّ جميع المُعاملات المُمرَّرة إليها بالمرجع ستكون ثابتة، ما لم تعدّلها الدالّة بشكل مباشر أو غير مباشر. يفيد هذا في منع المبرمج من إجراء تعديل غير مقصود، ويسمح للدالّة بأن تقبل النُسخ الثابتة، وكذلك النسخ غير المؤهّلة، وتجعل مؤشّر النسخة ‎this‎ من النوع ‎const T*‎ عندما تُستدعى دالة تابعة ما، حيث ‎T‎ يمثّل هنا نوع الصنف. struct Example { void func() { std::cout << 3 << std::endl; } void func() const { std::cout << 5 << std::endl; } }; void const_incorrect_function(Example& one, Example* two) { one.func(); two->func(); } void const_correct_function(const Example& one, const Example* two) { one.func(); two->func(); } int main() { Example a, b; const_incorrect_function(a, &b); const_correct_function(a, &b); } هذا هو خرج البرنامج 3 3 5 5 تمسك الدوال الصحيحة ثباتيًا الكثير من الأخطاء التي قد لا تمسكها الدوال غير الصحيحة ثباتيًا كما هو مبيّن أدناه، وذلك رغم أنّ تأثيرات هذا السلوك أقل وضوحًا من التأثيرات الناجمة عن تصميم الأصناف الصحيحة ثباتيًا التي رأيناها من قبل، في أنّ الدوال الصحيحة ثباتيا والأصناف غير الصحيحة ثباتيًا تتسبّب في حدوث أخطاء تصريفية، في حين أنّ الأصناف الصحيحة ثباتيًا والدوال غير الصحيحة ثباتيًا تُصرَّف بلا مشاكل. لاحظ أنّ الدوالّ غير الصحيحة ثباتيًا تتسبب في حدوث أخطاء تصريفية في حالة تمرير نُسخة ثابتة إليها. // قراءة القيمة من المتجهة ثم حساب وإعادة قيمة // إمساك القيمة المعادة لأجل تسريع البرنامج template<typename T> const T& bad_func(std::vector<T>& v, Helper<T>& h) { // إمساك القيم لإستخدامها مستقبلا // بعد حساب القيمة المعادة، تُخزّن ويُسجل فهرسها static std::vector<T> vals = {}; int v_ind = h.get_index(); // v الفهرس الحالي في سيكون السطر التالي مساويًا لـ 1- إن كان فهرس المخزن المؤقت غير مسجلًا … int vals_ind = h.get_cache_index(v_ind); if (vals.size() && (vals_ind != -1) && (vals_ind < vals.size()) && !(h.needs_recalc())) { return vals[h.get_cache_index(v_ind)]; } T temp = v[v_ind]; temp -= h.poll_device(); temp *= h.obtain_random(); temp += h.do_tedious_calculation(temp, v[h.get_last_handled_index()]); if (vals_ind != -1) { vals[vals_ind] = temp; } else { v.push_back(temp); // لا ينبغي الدخول إلى القيم vals_ind = vals.size() - 1; h.register_index(v_ind, vals_ind); } return vals[vals_ind]; } // النسخة الصحيحة ثباتيا تماثل النسخة أعلاه، لذا سنتجاوز معظمها template<typename T> const T& good_func(const std::vector<T>& v, Helper<T>& h) { // ... if (vals_ind != -1) { vals[vals_ind] = temp; } else { v.push_back(temp); // Error: discards qualifiers. vals_ind = vals.size() - 1; h.register_index(v_ind, vals_ind); } return vals[vals_ind]; } الصحة الثباتية كأداة للتوثيق من فوائد مفهوم الصحة الثباتية (const correctness) هي أنّه يمكن استخدامها لتوثيق الشيفرات، عبر توفير ضمانات وتوجيهات للمبرمجين والمستخدمين الآخرين. هذه الضمانات يفرضها المُصرِّف بسبب الثباتية، مع غيابٍ للثباتية يوحي أن الشيفرة لا توفرها. التوابع المؤهّلة ثباتيًا يمكن أن نفترض أنّ المبرمج الذي كتب الشيفرة يقصد أنّ الدوال التابعة الثابتة ستقرأ النسخة، و: لن تعدّل الحالة المنطقية للنُسخة التي استُدعيت عليها. نتيجة لذلك، فلن تعدّل أيّ حقل من حقول النُسخة التي استُدعيت عليها، باستثناء المتغيّرات القابلة للتغيير ‎mutable‎. لن تستدعي دوال أخرى من شأنّها تعديل أيّ حقل من حقول النُسخة، باستثناء المتغيّرات القابلة للتغيير ‎mutable‎. بالمقابل، يمكن افتراض أنّ أيّ دالة تابعة غير ثابتة سوف تسعى إلى تعديل النُسخة، و: قد تعدّل الحالة المنطقية أو لا. قد تستدعي دوال أخرى يمكن أن تعدّل الحالة المنطقية أو لا. يمكن استخدام هذا لأجل بناء افتراضات حول حالة الكائن بعد استدعاء دالة تابعة معيّنة عليه دون الحاجة إلى رؤية تعريف تلك الدالّة: // ConstMemberFunctions.h class ConstMemberFunctions { int val; mutable int cache; mutable bool state_changed; public: // يعدّل المنشئ الحالة المنطقية، لذا لا ضرورة لأيّ افتراضات ConstMemberFunctions(int v = 0); يمكن أن نفترض أن الدالة التالية لن تعدل الحالة المنطقية، ولن تستدعي ()set_val، ويمكن كذلك أن تستدعي ()bad_func أو ()squared_calc … int calc() const; يمكن أن نفترض أن الدالة التالية لن تعدل الحالة المنطقية، ولن تستدعي ()set_val، ويمكن كذلك أن تستدعي ()bad_func أو ()calc … int squared_calc() const; يمكن أن نفترض أن الدالة التالية لن تعدل الحالة المنطقية، ولن تستدعي ()set_val، ويمكن كذلك أن تستدعي ()calc أو ()squared_calc … void bad_func() const; يمكن أن نفترض أن الدالة التالية لن تعدل الحالة المنطقية، ولن تستدعي ()set_val، ويمكن كذلك أن تستدعي ()bad_func أو ()squared_calc أو ()calc … void set_val(int v); }; وفقًا لقواعد الصحة الثباتية، ستُفرض هذه الافتراضات في الواقع بواسطة المُصرِّف. // ConstMemberFunctions.cpp ConstMemberFunctions::ConstMemberFunctions(int v /* = 0*/) : cache(0), val(v), state_changed(true) {} // لقد كان افتراضنا صحيحا int ConstMemberFunctions::calc() const { if (state_changed) { cache = 3 * val; state_changed = false; } return cache; } // لقد كان افتراضنا صحيحا int ConstMemberFunctions::squared_calc() const { return calc() * calc(); } // لقد كان افتراضنا غير صحيح // للمؤهّلات `this` فشل تصريف الدالة بسبب فقدان void ConstMemberFunctions::bad_func() const { set_val(863); } // لقد كان افتراضنا صحيحا void ConstMemberFunctions::set_val(int v) { if (v != val) { val = v; state_changed = true; } } مُعاملات الدالة الثابتة يمكن أن نفترض أنّ الدوالّ التي تقبل مُعاملًا واحدًا ثابتًا أو أكثر تهدف إلى قراءة تلك المُعاملات، و: أنّها لن تعدّل تلك المُعاملات، أو تستدعي عليها تابعًا يمكن أن يُعدّلها. لن تمرّر تلك المُعاملات إلى أيّ دالّة أخرى من شأنها تعديلها و/أو استدعاء أي توابع أخرى من شأنها تعديلها. بالمقابل، يمكن افتراض أنّ أي دالّة تقبل مُعاملًا واحدًا غير ثابت أو أكثر ستسعى لتعديل تلك المُعاملات، و: قد تعدِّل أو لا تعدّل تلك المُعاملات، أو قد تستدعي عليها توابع يمكن أن تعدّلها. قد تمرّر أو قد لا تمرّر تلك المُعاملات إلى دوال أخرى يمكن أن تعدّلها و / أو تستدعي عليها توابع قد تعدّلها. يمكن استخدام هذا الأمر لأجل بناء افتراضات حول الحالة التي ستكون عليها المُعاملات بعد تمريرها إلى دالّة معيّنة، حتى دون النظر في تعريف تلك الدالّة. فيما يلي، يمكن أن نفترض أن c لم تُعدَّل وأن ()c.set_val لم تُستدعى ولم تمرَّر إلى ()non_qualified_function_parameter، وإذا مُرِّرَت إلى ()one_const_one_not فستكون أول المعامِلات … // function_parameter.h void const_function_parameter(const ConstMemberFunctions& c); يمكن أن نفترض أن c عُدِّلَت و/أو أن ()c.set_val استدعيَت، وقد تكون مرِّرَت إلى أي من تلك الدوال، وإن مُرِّرَت إلى ()one_const_one_not فقد تكون أيًا من المعامِلات … void non_qualified_function_parameter(ConstMemberFunctions& c); نستطيع افتراض أن: l لم تُعدَّل، وأن ()l.set_val لن تُستدعى. l قد تُمرَّر أو لا إلى ()const_function_parameter. تم تعديل r و/أو قد تستدعى ()r.set_val. قد تُمرَّر r أو لا إلى أي من الدوال السابقة. نتابع … void one_const_one_not(const ConstMemberFunctions& l, ConstMemberFunctions& r); يمكن أن نفترض أن c لم تُعدَّل وأن ()c.set_val لم تُستدعى، وأنما لم تُمرَّر إلى ()non_qualified_function_parameter، وإن مُرِّرَت إلى ()one_const_one_not فقد تكون أيًا من المعامِلات … void bad_parameter(const ConstMemberFunctions& c); وفقًا لقواعد الصحة الثباتية، ستُفرض هذه الافتراضات من قِبل المُصرِّف. // function_parameter.cpp // افتراضنا كان صحيحا void const_function_parameter(const ConstMemberFunctions& c) { std::cout << "With the current value, the output is: " << c.calc() << '\n' << "If squared, it's: " << c.squared_calc() << std::endl; } // افتراضنا كان صحيحا void non_qualified_function_parameter(ConstMemberFunctions& c) { c.set_val(42); std::cout << "For the value 42, the output is: " << c.calc() << '\n' << "If squared, it's: " << c.squared_calc() << std::endl; } // افتراضنا كان صحيحا // لاحظ أنّ الصحة الثباتية لا تحصِّن التغليف من أن يُكسر، وإنما تمنع الحق في الكتابة // إلا عند الحاجة إليها void one_const_one_not(const ConstMemberFunctions& l, ConstMemberFunctions& r) { struct Machiavelli { int val; int unimportant; bool state_changed; }; reinterpret_cast<Machiavelli&>(r).val = l.calc(); reinterpret_cast<Machiavelli&>(r).state_changed = true; const_function_parameter(l); const_function_parameter(r); } افتراضنا فيما يلي كان خطأ، وتفشل الدالة في التصريف لأن this فقد مؤهلاتٍ في ()c.set_val، … void bad_parameter(const ConstMemberFunctions& c) { c.set_val(18); } رغم أنّه يمكن التحايل على الصحة الثباتية وكسر الضمانات التي تقدّمها، إلا أنّه ينبغي توخّي الحذر عند فعل ذلك، ويجب أن يكون المبرمج على علم بما يفعل، فمن المحتمل أن يتسبّب في حدوث سلوك غير معرَّف. class DealBreaker : public ConstMemberFunctions { public: DealBreaker(int v = 0); // اسم غير مسموح به، لكنه ثابت void no_guarantees() const; } DealBreaker::DealBreaker(int v /* = 0 */) : ConstMemberFunctions(v) {} // افتراضنا كان خاطئا // الصحة الثباتية، وتجعل المصرف يعتقد أننا على علم بتبِعات ما نفعل const_cast تحذف void DealBreaker::no_guarantees() const { const_cast<DealBreaker*>(this)->set_val(823); } // ... const DealBreaker d(50); d.no_guarantees(); // ثابتة، وقد تُعدّل d سلوك غير معرَّف، إذ أنّ على أي حال، ولأن هذا يتطلب من المبرمج أن يخبر المصرِّف أنه ينوي تجاهل الثباتية (Constness)، ولعدم التماثل (Consistency) بين المصرِّفات، فمن الآمن افتراض أن الشيفرة الصحيحة ثباتيًا لن تتجاهل الصحة الثباتية أو التماثل بين المصرِّفات إلا إن حُدِّد خلاف ذلك. هذا الدرس جزء من سلسلة دروس عن C++‎. ترجمة -بتصرّف- للفصل Chapter 128: Const Correctness من كتاب C++ Notes for Professionals
  9. سوف نستعرض في هذا الدرس بعض الأمثلة على كيفية التعامل مع خادم العميل (Client server). مثال Hello TCP Client هذا البرنامج مكمّل لبرنامج Hello TCP Server، ويمكنك تشغيل أي منهما للتحقق من صلاحيتهما. انظر شيفرة المثال فيما يلي: #include <cstring> #include <iostream> #include <string> #include <arpa/inet.h> #include <netdb.h> #include <sys/socket.h> #include <sys/types.h> #include <unistd.h> int main(int argc, char *argv[]) { سنأخذ هنا عنوان IP ورقم بوابة كوسائط لبرنامجنا، نتابع … if (argc != 3) { std::cerr << "Run program as 'program <ipaddress> <port>'\n"; return -1; } auto &ipAddress = argv[1]; auto &portNum = argv[2]; addrinfo hints, *p; memset(&hints, 0, sizeof(hints)); hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; hints.ai_flags = AI_PASSIVE; int gAddRes = getaddrinfo(ipAddress, portNum, &hints, &p); if (gAddRes != 0) { std::cerr << gai_strerror(gAddRes) << "\n"; return -2; } if (p == NULL) { std::cerr << "No addresses found\n"; return -3; } ينشئ استدعاء ()socket قناة socket جديدة ويعيد الواصف الخاص بها … int sockFD = socket(p->ai_family, p->ai_socktype, p->ai_protocol); if (sockFD == -1) { std::cerr << "Error while creating socket\n"; return -4; } لاحظ عدم وجود استدعاء ()bind كما في برنامج Hello TCP Server، فذلك غير ضروري رغم أنك تستطيع استدعاءها، لأنه لا يلزم العميل أن يكون له رقم منفذ (port number) ثابت، لذا فإن الاستدعاء التالي سيربطه برقم عشوائي. سيحاول استدعاء ()connect أن ينشئ اتصال TCP بالخادم المحدد … int connectR = connect(sockFD, p -> ai_addr, p -> ai_addrlen); if (connectR == -1) { close(sockFD); std::cerr << "Error while connecting socket\n"; return -5; } std::string reply(15, ' '); هنا يحاول استدعاء ()recv الحصول على إجابة من الخادم، لكن الإجابة قد تحتاج إلى عدة استدعاءات لـ ()recv قبل أن تُستقبَل بالكامل، سنحاول حل هذا لاحقًا … auto bytes_recv = recv(sockFD, &reply.front(), reply.size(), 0); if (bytes_recv == -1) { std::cerr << "Error while receiving bytes\n"; return -6; } std::cout << "\nClient recieved: " << reply << std::endl; close(sockFD); freeaddrinfo(p); return 0; } مثال Hello TCP Server نقترح أن تلقي نظرة سريعة على كتاب Beej's Guide to Network Programming، والذي سيفسر لك معظم المفاهيم المُستخدَمة هنا. سننشئ خادم TCP بسيطًا هنا يعرض الجملة "Hello World" لكل الاستدعاءات الواردة، ثم يُغلقها. كما أنّ الخادم سيتواصل مع العملاء تكراريًا (iteratively)، أي عميلًا واحدًا في كل مرّة. سنشغّل الخادم عبر منفذ محدّد، لذلك سنأخذ رقم المنفذ كوسيط. انظر: #include <cstring> // sizeof() #include <iostream> #include <string> الترويسات التالية من أجل ()getaddrinfo و ()socket والدوال الصديقة … #include <arpa/inet.h> #include <netdb.h> #include <sys/socket.h> #include <sys/types.h> #include <unistd.h> // close() int main(int argc, char *argv[]) { هنا نتحقق مما إذا كان رقم المنفذ قد أُعطي أم لا … if (argc != 2) { std::cerr << "Run program as 'program <port>'\n"; return -1; } auto &portNum = argv[1]; في السطر التالي، عدد الاتصالات المسموح بها في طابور الوارد … const unsigned int backLog = 8; نحتاج إلى مؤشرين، res سيأخذ القيمة وp سيكرر عليها … addrinfo hints, *res, *p; memset( &hints, 0, sizeof(hints)); // راجع الكتاب لمزيد من التوضيحات hints.ai_family = AF_UNSPEC; // التي ستستخدم IP لا تحدّد بعدُ نسخة في السطر التالي، يشير SOCK_STREAM إلى TCP … hints.ai_socktype = SOCK_STREAM; hints.ai_flags = AI_PASSIVE; int gAddRes = getaddrinfo(NULL, portNum, &hints, &res); if (gAddRes != 0) { std::cerr << gai_strerror(gAddRes) << "\n"; return -2; } std::cout << "Detecting addresses" << std::endl; unsigned int numOfAddr = 0; يحرص طول ipv6 على إمكانية تخزين عنواني ipv4/6 في هذا المتغير، وبما أن ()getaddrinfo قد أعطتنا قائمة من العناوين، فسنكرر عليها ونسأل المستخدم أن يختار أحدها لربطها بالبرنامج … char ipStr[INET6_ADDRSTRLEN]; for (p = res; p != NULL; p = p -> ai_next) { void *addr; std::string ipVer; // ipv4 إن كان العنوان من النوع if (p -> ai_family == AF_INET) { ipVer = "IPv4"; sockaddr_in *ipv4 = reinterpret_cast < sockaddr_in *> (p -> ai_addr); addr = &(ipv4 -> sin_addr); ++numOfAddr; } // ipv6 إن كان العنوان من النوع else { ipVer = "IPv6"; sockaddr_in6 *ipv6 = reinterpret_cast < sockaddr_in6 * > (p -> ai_addr); addr = &(ipv6 -> sin6_addr); ++numOfAddr; } // من الشكل الثنائي إلى الشكل النصي IPv4/6 تحويل عناوين inet_ntop(p -> ai_family, addr, ipStr, sizeof(ipStr)); std::cout << "(" << numOfAddr << ") " << ipVer << " : " << ipStr << std::endl; } // في حال عدم العثور على أيّ عنوان if (!numOfAddr) { std::cerr << "Found no host address to use\n"; return -3; } // اسأل المستخدم أن يختار عنوانا std::cout << "Enter the number of host address to bind with: "; unsigned int choice = 0; bool madeChoice = false; do { std::cin >> choice; if (choice > (numOfAddr + 1) || choice < 1) { madeChoice = false; std::cout << "Wrong choice, try again!" << std::endl; } else madeChoice = true; } while (!madeChoice); p = res; // كواصف socketFD لننشئ قناة جديدة، ستُعاد // تعيد هذه الاستدعاءات في العادة القيمة -1 في حال وقوع خطأ ما int sockFD = socket(p->ai_family, p->ai_socktype, p->ai_protocol); if (sockFD == -1) { std::cerr << "Error while creating socket\n"; freeaddrinfo(res); return -4; } // لنربط العنوان بالقناة التي أنشأناها للتو int bindR = bind(sockFD, p->ai_addr, p->ai_addrlen); if (bindR == -1) { std::cerr << "Error while binding socket\n"; // في حال وقوع خطأ، احرص على إغلاق القناة وتحرير الموارد close(sockFD); freeaddrinfo(res); return -5; } // وأخيرا، ابدأ بالإنصات إلى الاتصالات الواردة عبر قناتنا int listenR = listen(sockFD, backLog); if (listenR == -1) { std::cerr << "Error while Listening on socket\n"; // في حال وقوع خطأ، احرص على إغلاق القناة وتحرير الموارد close(sockFD); freeaddrinfo(res); return -6; } // البنية كبيرة كفاية لتحمل عنوان العميل sockaddr_storage client_addr; socklen_t client_addr_size = sizeof(client_addr); const std::string response = "Hello World"; فيما يلي حلقة لا نهائية للتواصل مع الاتصالات الواردة، وستتعامل معها بالتتابع أي واحدة في كل مرة. كذلك فيما يلي من الأمثلة سنستدعي ()fork لكل اتصال مع العملاء … while (1) { // يعيد واصف قناة جديد accept استدعاء int newFD = accept(sockFD, (sockaddr *) &client_addr, &client_addr_size); if (newFD == -1) { std::cerr << "Error while Accepting on socket\n"; continue; } فيما يلي، يعيد استدعاء send البيانات التي مرَّرتها أنت كمعامِل ثاني، وطولها كمعامل ثالث، ويعيد عدد البِتَّات المرسلة فعليًا … auto bytes_sent = send(newFD, response.data(), response.length(), 0); close(newFD); } close(sockFD); freeaddrinfo(res); return 0; } سيعمل البرنامج على النحو التالي: Detecting addresses (1) IPv4 : 0.0.0.0 (2) IPv6 : :: Enter the number of host address to bind with: 1 هذا الدرس جزء من سلسلة دروس عن C++‎. ترجمة -بتصرّف- للفصل Chapter 127: Client server examples من كتاب C++ Notes for Professionals
  10. تطبيقات على التعاود يمكن استعمال التعاود في الكثير من التطبيقات المفيدة إذ يساعدنا على تبسيط الشيفرة ويعطيها قوة وفعالية على عكس لو لم نعتمد على مفهوم التعاود وإليك بعض هذه التطبيقات مع شيفراتها. حساب تسلسلات فيبوناتشي Fibonnaci sequence هذه هي الطريقة الأبسط لاستخدام التكرارية للحصول على العنصر رقم N من سلسلة Fibonnaci: int get_term_fib(int n) { if (n == 0) return 0; if (n == 1) return 1; return get_term_fib(n - 1) + get_term_fib(n - 2); } لكن المشكلة في هذه الطريقة أنّها غير مناسبة للأعداد الكبيرة، فكلما زادت قيمة ‎n‎، زاد عدد استدعاءات الدالة بشكل أسي (exponentially). يمكن الاستعاضة عن هذه الطريقة بالتكرارية المُذيّلة (tail recursion) على النحو التالي: int get_term_fib(int n, int prev = 0, int curr = 1) { if (n == 0) return prev; if (n == 1) return curr; return get_term_fib(n - 1, curr, prev + curr); } سيحسُب كل استدعاء للدالّة الحدّ التالي في تسلسل Fibonnaci، وعليه فإنّ عدد استدعاءات الدالّة يزيد خطيًا مع ‎n‎. التكرارية مع التذكّر Recursion with memoization قد تصبح الدوال التكرارية مكلفة للغاية، ويمكن جعلها أسرع بكثير عن طريق تخزين القيم التي سبق حسابها، إذا كانت دوالًّا خالصة -أي دوالًّا تُعيد دائمًا نفس القيمة عند استدعائها مع نفس الوسائط، ولا تعتمد على الحالة الخارجية ولا تعدّلها-، لكن هذا سيكون على حساب استخدام مزيد من الذاكرة. يوضّح المثال التالي كيفية حساب تسلسل فيبوناتشي باستخدام التكرارية التذكيرية: #include <map> int fibonacci(int n) { static std::map < int, int > values; if (n == 0 || n == 1) return n; std::map<int,int>::iterator iter = values.find(n); if (iter == values.end()) { return values[n] = fibonacci(n - 1) + fibonacci(n - 2); } else { return iter->second; } } لاحظ أنّه على الرغم من استخدام صيغة التكرارية البسيطة، إلّا أنّ تعقيد الدالّة يساوي O(n)‎ في الاستدعاء الأول، وسيكون التعقيد ثابتًا في الاستدعاءات اللاحقة مع نفس القيمة، أي O(1)‎. واعلم أنّ هذا التنفيذ ليس متعدّد المداخل (reentrant)، كما لا يسمح بالتخلص من القيم المُخزّنة. هناك تقديم بديل يعتمد على السماح بتمرير قاموس كوسيط إضافي: #include <map> int fibonacci(int n, std::map<int, int> values) { if (n==0 || n==1) return n; std::map<int,int>::iterator iter = values.find(n); if (iter == values.end()) { return values[n] = fibonacci(n-1) + fibonacci(n-2); } else { return iter->second; } } في هذا المثال، يُطلَب من المُستدعي أن يحفظ القاموس الذي يحتوي القيم المُخزّنة. ميزة هذا أنّ الدالّة أصبحت الآن متعدّدة المداخل (reentrant)، وأنّه المستدعي يستطيع إزالة القيم التي لم تعد مطلوبة، وهذا يحسّن استخدام الذاكرة. بالمقابل، يعيب هذه الطريقة أنّها تكسر التغليف (breaks encapsulation). إذ يمكن للمُستدعي تغيير الخرج عن طريق ملء القاموس بقيم غير صحيحة. الكائنات القابلة للاستدعاء Callable Objects الكائنات القابلة للاستدعاء هي هياكل C++‎ يمكن استخدامها كدوال، وتدخل فيها كل الأشياء التي يمكنك تمريرها إلى دالة المكتبة القياسية في C++‎ 17‏ invoke()‎ أو التي يمكن استخدامها في مُنشئ std::function، وهذا يشمل: مؤشّرات الدوال، والأصناف التي تحتوي على operator()‎، والأصناف ذات التحويلات الضمنية، ومراجع الدوال، ومؤشّرات الدوال التابعة ومؤشّرات الحقول (Pointers to member data)، وتعابير لامبدا. تُستخدَم الكائنات القابلة للاستدعاء في العديد من خوارزميات مكتبة القوالب القياسية STL كدوال شرطية (predicate). مؤشّرات الدوال Function Pointers تعدّ مؤشّرات الدوال الطريقة الأبسط لتمرير الدوال، ويمكن استخدامها أيضًا في لغة C، وإن أردت استخدامها ككائنات قابلة للاستدعاء، فيمكنك تعريف مؤشّر الدالّة على النحو التالي: typedef returnType(*name)(arguments); // All using name = returnType(*)(arguments); // <= C++11 using name = std::add_pointer<returnType(arguments)>::type; // <= C++11 using name = std::add_pointer_t<returnType(arguments)>; // <= C++14 يستخدم المثال التالي مؤشّر دالة لكتابة دالة مخصّصة لترتيب المتجهات: using LessThanFunctionPtr = std::add_pointer_t<bool(int, int)>; void sortVectorInt(std::vector<int>&v, LessThanFunctionPtr lessThan) { if (v.size() < 2) return; if (v.size() == 2) { if (!lessThan(v.front(), v.back())) // استدعاء مؤشر الدالة std::swap(v.front(), v.back()); return; } std::sort(v, lessThan); } bool lessThanInt(int lhs, int rhs) { return lhs < rhs; } sortVectorInt(vectorOfInt, lessThanInt); // تمرير المؤشر إلى دالة حرّة struct GreaterThanInt { static bool cmp(int lhs, int rhs) { return lhs > rhs; } }; sortVectorInt(vectorOfInt, &GreaterThanInt::cmp); // تمرير المؤشر إلى دالة تابعة ساكن كان بإمكاننا استدعاء مؤشّر الدالّة بدلًا من ذلك، بإحدى الطرق التالية : (*lessThan)(v.front(), v.back()) std::invoke(lessThan, v.front(), v.back()) // <= C++17 الأصناف ذات التابع operator()‎‏ (الكائنات الداليّة Functors) يمكن استخدام كل الأصناف التي تحمّل ‎operator()‎ تحميلًا زائدًا ككائنات دوال، ويمكن كتابة هذه الأصناف يدويًا (يشار إليها غالبًا باسم الكائنات الدالية - functors)، أو إنشاؤها تلقائيًا بواسطة المُصرِّف عن طريق كتابة تعابير لامدا (منذ C++‎ 11 وما بعده). struct Person { std::string name; unsigned int age; }; // كائن دالي، يعثر على الشخص باسمه struct FindPersonByName { FindPersonByName(const std::string &name) : _name(name) {} // التابع المزاد تحميله والذي سيُستدعى bool operator()(const Person &person) const { return person.name == _name; } private: std::string _name; }; std::vector<Person> v; // نفترض أنّه يحتوي البيانات std::vector<Person>::iterator iFind = std::find_if(v.begin(), v.end(), FindPersonByName("Foobar")); // ... بحكم أنّ للكائنات الدالية هويّتها الخاصة، فلا يمكن وضعها في التعريفات النوعية typedef، ويجب قبولها عبر وسيط القالب. يمكن أن يكون تعريف ‎std::find_if‎ على الشكل التالي: template < typename Iterator, typename Predicate > Iterator find_if(Iterator begin, Iterator end, Predicate &predicate) { for (Iterator i = begin, i != end, ++i) if (predicate( *i)) return i; return end; } يمكن استدعاء الدالة الشرطية عبر استدعاء: ‎std::invoke(predicate, *i)‎، وذلك منذ الإصدار C++ 17. هذا الدرس جزء من سلسلة دروس عن C++‎. ترجمة -بتصرّف- للفصل Chapter 124: Recursion in C++‎ والفصل Chapter 126: Callable Objects من كتاب C++ Notes for Professionals
  11. السمة [[fallthrough]] الإصدار ≥ C++‎ 17 عند إنهاء تعليمة case بكلمة switch ستنفَّذ الشيفرة التي تليها، وإن أردت منع هذا السلوك، فاستخدم تعليمة ‎‎break‎‎. يُسمّى هذا السلوك بـ "السقطة" (fallthrough)، وقد ينجم عنه أخطاء غير مقصودة، لذا تطلق العديد من المُصرِّفات والمحلّلات الساكنة (static analyzers) تحذيرًا منه. وقد أُدخلت سمة قياسية (standard attribute) في C++ 17 من أجل الإشارة إلى أنّ التحذير ليس ضرويًا إذا كانت السقطة مقصودة في الشيفرة، ويمكن للمصرّفات إعطاء تحذيرات بأمان إذا أنهيت case بالكلمةُ المفتاحيةُ ‎break‎ أو ‎[[fallthrough]]‎، وكانت مؤلفة من تعليمة واحد على الأقل. switch (input) { case 2011: case 2014: case 2017: std::cout << "Using modern C++" << std::endl; [[fallthrough]]; // > لا يوجد تحذير case 1998: case 2003: standard = input; } راجع هذا الاقتراح لمزيد من الأمثلة حول كيفية استخدام ‎[[fallthrough]]‎. السمة [[nodiscard]] الإصدار ≥ C++‎ 17 يمكن استخدام السمة ‎[[nodiscard]]‎ للإشارة إلى أنّه لا ينبغي تجاهل القيمة المُعادة من دالة عند استدعاء دالة، ويجب على االمُصرِّف في حال تجاهلها إعطاء تحذير بهذا الشأن. يمكن إضافة هذه السمة إلى: تعريف دالّة نوع ينتج عن إضافة السمة إلى نوع ما نفسُ السلوكِ الناتج عن إضافة السمة إلى كل الدوالّ التي تُعيد ذلك النوع. template<typename Function> [[nodiscard]] Finally<std::decay_t<Function>> onExit(Function &&f); void f(int &i) { assert(i == 0); // لجعل التعليقات واضحة ++i; // i == 1 auto exit1 = onExit([&i]{ --i; }); // f() التخفيض بـ 1 عند الخروج من ++i; // i == 2 onExit([&i]{ --i; }); // خطأ: محاولة التخفيض بـ 1 مباشرة // يُتوقع أن يصدر المصرف تنبيها std::cout << i << std::end; // القيمة المتوقعة 2، لكن القيمة الحقيقية هي 1 } راجع هذا الاقتراح لمزيد من الأمثلة حول كيفية استخدام ‎[[nodiscard]]‎. السمة [[deprecated]] والسمة [[deprecated("reason")‎]] الإصدار ≥ C++‎ 14 قدَّمت C++‎ 14 طريقة قياسية لإهمال (deprecating) الدوال عبر السمات. يمكن استخدام ‎[[deprecated]]‎ للإشارة إلى إهمال الدالّة. ويسمح التعبير ‎[[deprecated("reason")]]‎بتحديد سبب للإهمال يمكن أن نجعل المُصرِّف يظهِره. void function(std::unique_ptr<A> &&a); // توفير رسالة محدّدة تساعد المبرمجين الآخرين على تصحيح شيفراتهم [[deprecated("Use the variant with unique_ptr instead, this function will be removed in the next release")]] void function(std::auto_ptr<A> a); // في حال عدم وجود رسالة، سيُطلق تنبيه عام عند الاستدعاء [[deprecated]] void function(A *a); يُمكن تطبيق هذه السمة على: تصريح صنف. اسم typedef. متغيّر. حقل غير ساكن (non-static data member). دالة. تعداد (enum). تخصيص قالب (template specialization). (المرجع: c++14 standard draft: 7.6.5 Deprecated attribute) السمة [[maybe_unused]] تُستعمل السمة ‎[[maybe_unused]]‎ للدلالة على احتمال عدم استخدام منطق بعينه داخل الشيفرة، وترتبط غالبًا بشروط المعالج الأَولي (preprocessor). ويمكن استخدام هذه السمة لمنع هذا السلوك من خلال الإشارة إلى أنّ هذا الأمر مقصود، نظرًا لأنّ المُصرِّفات قد تبعث تحذيرات بشأن المتغيّرات غير المستخدمة. وتُعد القيم المعادة التي تشير إلى نجاح عملية التنقيح من الأمثلة التطبيقية للمتغيّرات التي تُستخدم أثناء عمليات تنقيح بنيات الشيفرة (debug builds)، ولكن لا تبقى إليها حاجة في مرحلة الإنتاج. ويجب التحقق من شرط النجاح أثناء تنقيح أخطاء البُنيات، رغم أن تلك التحققات ستُحذف في مرحلة الإنتاج لعدم الحاجة إليها. [[maybe_unused]] auto mapInsertResult = configuration.emplace("LicenseInfo", stringifiedLicenseInfo); assert(mapInsertResult.second); // لا تُستدعى إلا خلال الإطلاق، لذا لا ينبغي أن تكون في القاموس هناك مثال أكثر تعقيدًا، وهي الدوال المساعدة التي توضع في فضاء اسم غير مُسمّى (unnamed namespace)، وتلك الدوال إن لم تُستخدَم أثناء التصريف، فقد يطلق االمُصرِّف تحذيرًا بهذا الشأن، لذا قد ترغب في حمايتها باستخدام نفس وسوم المعالج الأولي التي تُستخدَم مع المُستدعي (caller)، لكن قد يكون هذا معقدًا، لهذا يُعدُّ استخدام السمة ‎[[maybe_unused]]‎ بديلاً أسهل في الصيانة. namespace { [[maybe_unused]] std::string createWindowsConfigFilePath(const std::string &relativePath); // TODO: … BSD, MAC أعد استخدام هذا على [[maybe_unused]] std::string createLinuxConfigFilePath(const std::string &relativePath); } std::string createConfigFilePath(const std::string &relativePath) { #if OS == "WINDOWS" return createWindowsConfigFilePath(relativePath); #elif OS == "LINUX" return createLinuxConfigFilePath(relativePath); #else #error "OS is not yet supported" #endif } راجع هذا الاقتراح لمزيد من الأمثلة حول كيفية استخدام ‎[[maybe_unused]]‎. السمة [[noreturn]] الإصدار ≥ C++‎ 11 قدّمت C++‎ 11 سمةَ ‎[[noreturn]]‎ التي يمكن استخدامها مع دالّة للإشارة إلى أنّ تلك الدالّة لا تُعاد إلى المُستدعي، سواء عبر تعليمة return، أو عند الوصول إلى نهاية متنها -من المهم الإشارة إلى أن هذا لا ينطبق على الدوال الفارغة ‎void‎، نظرًا لأنّها تعود إلى المُستدعي، ولكن لا تعيد أيّ قيمة-. قد تنتهي مثل هذه الدوالّ عبر استدعاء ‎std::terminate‎ أو ‎std::exit‎، أو عبر رفع اعتراض (throwing an exception). تجدر الإشارة أيضًا إلى أنّ مثل هذه الدوالّ يمكن أن تُعاد عبر تنفيذ ‎longjmp‎. على سبيل المثال، الدالّة أدناه إمّا أن ترفع اعتراضًا أو تستدعي ‎std::terminate‎، لذلك فهي مرشح جيد لاستخدام ‎[[noreturn]]‎: [[noreturn]] void ownAssertFailureHandler(std::string message) { std::cerr << message << std::endl; if (THROW_EXCEPTION_ON_ASSERT) throw AssertException(std::move(message)); std::terminate(); } يسمح هذا السلوك للمُصرِّف بإنهاء الدوال التي لا تحتوي على تعليمة return إذا كان يعلم أنّ الشيفرة لن تُنفَّذ. هنا، لن يحتاج المُصرِّف إلى إضافة الشيفرة الموجودة أسفل ذلك الاستدعاء نظرًا لأنّ استدعاء ‎ownAssertFailureHandler‎ في الشيفرة أدناه لن يعود أبدًا: std::vector < int > createSequence(int end) { if (end > 0) { std::vector < int > sequence; sequence.reserve(end + 1); for (int i = 0; i <= end; ++i) sequence.push_back(i); return sequence; } ownAssertFailureHandler("Negative number passed to createSequence()"s); // return std::vector<int>{}; //< Not needed because of [[noreturn]] } سيكون السلوك غير معرَّف إذا كانت الدالّة ستعاد، ونتيجة لذلك لا يُسمح بما يلي: [[noreturn]] void assertPositive(int number) { if (number >= 0) return; else ownAssertFailureHandler("Positive number expected"s); //< [[noreturn]] } لاحظ أنّ ‎[[noreturn]]‎ تُستخدم في الغالب في الدوال الفارغة، لكنها ليست ضرورية، وهذا يسمح باستخدام الدوال في البرمجة العامة (generic programming): template<class InconsistencyHandler> double fortyTwoDivideBy(int i) { if (i == 0) i = InconsistencyHandler::correct(i); return 42. / i; } struct InconsistencyThrower { static [[noreturn]] int correct(int i) { ownAssertFailureHandler("Unknown inconsistency"s); } } struct InconsistencyChangeToOne { static int correct(int i) { return 1; } } double fortyTwo = fortyTwoDivideBy<InconsistencyChangeToOne>(0); double unreachable = fortyTwoDivideBy<InconsistencyThrower>(0); تحتوي دوال المكتبة القياسية التالية على هذه السمات: std::abort std::exit std::quick_exit std::unexpected std::terminate std::rethrow_exception std::throw_with_nested std::nested_exception::rethrow_nested هذا الدرس جزء من سلسلة دروس عن C++‎. ترجمة -بتصرّف- للفصل Chapter 123: Attributes من كتاب C++ Notes for Professionals
  12. ‎constexpr‎ هي كلمة مفتاحية يمكن استخدامها مع متغيّر لجعل قيمته تعبيرًا ثابتًا (constant expression)، أو دالةً لأجل استخدامها في التعبيرات الثابتة، أو (منذ C++‎ 17) تعليمة if حتى يُصرَّف فرع واحد فقط من فروعها. المصادقة عبر الدالة static_assert تقتضي المصادقات (Assertations) وجوب التحقق من شرط معيّن، وإطلاق خطأ إن كانت خطأً (false)، ويحدث هذا في وقت التصريف بالنسبة لـ ‎static_assert()‎. template<typename T> T mul10(const T t) { static_assert( std::is_integral<T>::value, "mul10() only works for integral types" ); return (t << 3) + (t << 1); } المعاملات التي تقبلها الدالة ‎static_assert()‎: المعامِل التفاصيل bool_constexpr التعبير المراد التحقق منه message الرسالة المُراد طباعتها عندما تساوي bool_constexpr القيمة false table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; } المعامل الأوّل إلزامي ويمثّل الشرط، وهو تعبير منطقي ثابت constexpr. كذلك يمكن أن تقبل الدالة أيضًا مُعاملًا ثانيًا، والذي يمثّل الرسالة، وهي سلسلة نصية مجردة. صار المعامِل الثاني اختياريًا ابتداءً من C++‎ 17، أما قبل ذلك كان إلزاميًا. الإصدار ≥ C++‎ 17 template < typename T > T mul10(const T t) { static_assert(std::is_integral < T > ::value); return (t << 3) + (t << 1); } تُستخدم المصادقات في حال: لزِم التحقق في وقت التصريف من نوع معيّن في تعبير ثابت constexpr. احتاجت دالّة القالب إلى التحقق من خاصّيات معيّنة من النوع المُمرّر إليها. إذا أردت كتابة حالات اختبارية لما يلي: الدوال الوصفية للقوالب template metafunctions دوال التعبيرات الثابتة constexpr functions شيفرة وصفية جامعة macro metaprogramming إن كانت بعض التعريفات مطلوبة (على سبيل المثال، إصدار C++‎) نقل الشيفرات القديمة (Porting legacy code)، والمصادقة (assertation) على ‎sizeof(T)‎ (على سبيل المثال، ‎32-bit int) إن كانت بعض ميزات المصرّف مطلوبة لعمل البرنامج (التحزيم - packing - أو تحسين الأصناف الأساسية الفارغة، وما إلى ذلك) لاحظ أنّ ‎static_assert()‎ لا تشارك في قاعدة SFINAE: إذا كانت التحميلات الزائدة/ التخصيصات الإضافية ممكنة، فلا ينبغي استخدامها بدلًا من تقنيات قوالب البرمجة الوصفية - template metaprogramming - (مثل ‎std::enable_if<>‎)، وقد تُستخدَم -مع لزوم التحقق منها- في شيفرة القالب في حال إيجاد ([التحميل الزائد](رابط الفصل 35) / التخصيص) المتوقع، وفي مثل هذه الحالات قد تُوفِّر رسالة خطأ أو أكثر تكون أوضح مما لو كنا اعتمدنا على قاعدة SFINAE. متغيّرات التعبيرات الثابتة (constexpr variables) إن صُرِّخ عن متغيّر بالكلمة المفتاحية ‎constexpr‎، فسيكون ثابتًا (‎const‎) ضمنيًا، وسيكون من الممكن استخدام قيمته كتعبير ثابت. المقارنة مع define يمكن استخدام ‎constexpr‎ كبديل آمن نوعيًا (type-safe) للتعبيرات التي تعتمد على ‎#define‎ في وقت التصريف، وعند استخدام ‎constexpr‎، سيُستبدَل التعبير المُقيَّم في وقت التصريف بنتيجته. انظر المثال التالي: الإصدار ≥ C++‎ 11 int main() { constexpr int N = 10 + 2; cout << N; } سينتج عن المثال أعلاه الشيفرة التالية: cout << 12; ستختلف الشيفرة الجامعة التي تعتمد على المعالج الأولى في وقت التصريف (pre-processor based compile-time macro)، انظر المثال التالي: #define N 10 + 2 int main() { cout << N; } هذا سيُنتِج الشيفرة التالية: cout << 10 + 2; والذي سيُحوَّل إلى cout << 10 + 2 لكن سيكون على المُصرِّف أن يقوم بالمزيد من العمل، كما قد تحدث مشكلة في حال لم تستخدم بشكل صحيح. على سبيل المثال (مع ‎#define‎): cout << N * 2; سينتج: cout << 10 + 2 * 2; // 14 سيعيد التقييم الأولي (pre-evaluated) القيمة ‎24‎ للتعبير الثابت ‎constexpr‎، كما هو مُتوقّع. مقارنة مع const تحتاج المتغيرات الثابتة (‎const‎) إلى ذاكرة لتخزينها، وذلك على خلاف التعبيرات الثابتة ‎constexpr‎، وتنتج التعبيرات الثابتة ‎constexpr‎ قيمًا ثابتة في وقت التصريف وغير قابلة للتغيير. قد يقال أيضًا أنّ القيمة الثابتة (‎const‎) هي أيضًا غير قابلة للتغيير، لكن انظر المثال التالي لتوضيح الفرق بينهما: int main() { const int size1 = 10; const int size2 = abs(10); int arr_one[size1]; int arr_two[size2]; } ستفشل التعليمة الثانية في معظم المُصرِّفات -رغم أنها قد تعمل في GCC-، إذ يجب أن يكون حجم أيّ مصفوفة تعبيرًا ثابتًا (أي ينتُج عنه قيمة في وقت التصريف). وكما ترى في الشيفرة أعلاه، فقد أُسنِد إلى المتغيّر الثاني ‎size2‎ قيمة ستُحدَّد في وقت التشغيل (runtime) رغم أنّها تساوي ‎10‎، إلا أنّ المُصرِّف لا يعدُّها قيمة تصريفية (تصريفية، من وقت التصريف، compile-time). هذا يعني أنّ ‎const‎ قد تكون أو لا تكون ثابتة تصريفية حقيقية، ولا تستطيع أن تضمن لقيمة ثابتة ‎const‎ معيّنة أن تكون تصريفيةً، ولك أن تستخدم ‎#define‎ رغم أنها لا تخلو من بعض المشاكل. وعليه، يمكنك استخدام الحلّ التالي: الإصدار ≥ C++‎ 11 int main() { constexpr int size = 10; int arr[size]; } يجب تقييم التعابير الثابتة ‎constexpr‎ إلى قيم تصريفية، لذا لا يمكن استخدام ما يلي … الإصدار ≥ C++‎ 11 constexpr int size = abs(10); … ما لم تكن الدالة (‎abs‎) نفسها تعيد تعبيرًا ثابتًا. يجوز تهيئة جميع الأنواع الأساسية باستخدام ‎constexpr‎. الإصدار ≥ C++‎ 11 constexpr bool FailFatal = true; constexpr float PI = 3.14 f; constexpr char* site = "StackOverflow"; يمكنك أيضًا استخدام ‎auto‎ كما في المثال التالي: الإصدار ≥ C++‎ 11 constexpr auto domain = ".COM"; // const char * const domain = ".COM" constexpr auto PI = 3.14; // constexpr double تعليمة if الساكنة الإصدار ≥ C++‎ 17 يمكن استخدام عبارة ‎if constexpr‎ للتحكّم في تصريف الشيفرة، لكن يجب أن يكون الشرط تعبيرًا ثابتًا. ستُتجَاهل الفروع غير المُختارة، ولن تُستنسَخ التعليمات التي تمّ تجاهلها داخل القالب. مثلًا: template<class T, class ... Rest> void g(T &&p, Rest &&...rs) { // ... p معالجة if constexpr (sizeof...(rs) > 0) g(rs...); // لا تقم بالتهيئة باستخدام قائمة وسائط فارغة } لا يلزم تعريف المتغيّرات والدوال التي استُخدَمت قيمتها (odr-used) حصرًا داخل العبارات المُتجاهلَة (discarded statements)، كما لا تُستخدَم عبارات ‎return‎ المُتجاهلة في استنتاج النوع المعاد من الدالّة. وتختلف العبارة ‎if‎ ‎constexpr‎ عن شيفرات التصريف الشرطية ‎#ifdef. #ifdef، إذ تعتمد حصرًا على الشروط التي يمكن تقييمها في وقت المعالجة الأولية. فلا يمكن استخدام ‎#ifdef‎ للتحكم في تصريف الشيفرة بناءً على قيمة مُعامل القالب، لكن من ناحية أخرى، لا يمكن استخدام ‎if constexpr‎ لتجاهل الشيفرات ذات الصياغة غير الصحيحة، وذلك على خلاف ‎#ifdef‎. if constexpr(false) { foobar; // error; foobar has not been declared std::vector < int > v("hello, world"); // error; no matching constructor } دوال التعبيرات الثابتة (constexpr functions) ستكون الدوالّ المُصرَّح عنها بالكلمة المفتاحية ‎constexpr‎ مُضمّنة (inline) ضمنيًا، وسينتج عن استدعائها تعابير ثابتة، فسيعاد تعبير ثابت إذا كانت الوسائط المُمرّرة إلى الدالة التالية تعابير ثابتة أيضًا: الإصدار ≥ C++‎ 11 constexpr int Sum(int a, int b) { return a + b; } يمكن استخدام نتيجة استدعاء الدالّة كمصفوفة مربوطة (array bound) أو وسيط قالب، كما يمكن استخدامها لتهيئة متغيّر تعبير ثابت (constexpr variable): الإصدار ≥ C++‎ 11 int main() { constexpr int S = Sum(10, 20); int Array[S]; int Array2[Sum(20, 30)]; // مصفوفة مؤلفة من 50 عنصرا في وقت التصريف } لاحظ أنّك إذا أزلت الكلمة المفتاحية ‎constexpr‎ من تعريف النوع المُعاد الخاص بالدالّة، فلن يعمل الإسناد إلى ‎S‎، لأنّ ‎S‎ متغيّر تعبير ثابت ويجب أن تُسند إليه قيمة تصريفية. وبالمثل، لن يكون حجم المصفوفة تعبيرًا ثابتًا إذا لم تكن ‎Sum‎ دالةَ تعبير ثابت. كذلك تستطيع استخدام دوال التعبيرات الثابتة (‎‎constexpr‎ functions) كما لو كانت دوال عادية: الإصدار ≥ C++‎ 11 int a = 20; auto sum = Sum(a, abs(-20)); لن تكون ‎Sum‎ دالّة تعبير ثابت الآن، وستُصرَّف كدالة عادية، وستأخذ وسائط متغيّرة (غير ثابتة)، وتعيد قيمة غير ثابتة، لذا لا تحتاج إلى كتابة دالّتين. هذا يعني أيضًا أنّه إذا حاولت إسناد هذا الاستدعاء إلى متغيّر غير ثابت، فلن ينجح التصريف: الإصدار ≥ C++‎ 11 int a = 20; constexpr auto sum = Sum(a, abs(-20)); وذلك لأنه لا ينبغي أن يُسنَد إلى تعبير ثابت إلّا ثابتة تصريفية (compile-time constant). بالمقابل فإن استدعاء الدالة أعلاه يجعل ‎Sum‎ تعبيرًا غير ثابت (القيمة اليمينية غير ثابتة، على خلاف القيمة اليسارية التي صُرَّح عنها كتعبير ثابت). يجب أيضًا أن تُعيد دالة التعبير الثابت ثابتةً تصريفية (compile-time constant). في المثال التالي، لن تُصرَّف الشيفرة: الإصدار ≥ C++‎ 11 constexpr int Sum(int a, int b) { int a1 = a; // خطأ return a + b; } لأنّ ‎a1‎ متغيّر غير ثابت، ويمنع الدالّة من أن تكون دالة تعبير ثابت ‎constexpr‎ حقيقية، ولن تنجح محاولة جعلها تعبيرًا ثابتًا وإسناد قيمة a لها - نظرًا لأنّ قيمة a (المُعامل الوارد - incoming parameter) ما تزال غير معروفة بعد: الإصدار ≥ C++‎ 11 constexpr int Sum(int a, int b) { constexpr int a1 = a; // خطأ .. وكذلك لن تُصرَّف الشيفرة التالية: الإصدار ≥ C++‎ 11 constexpr int Sum(int a, int b) { return abs(a) + b; // abs(a) + abs(b) أو } وبما أن ‎abs(a)‎ ليست تعبيرًا ثابتًا -ولن تعمل ‎abs(10)‎، إذ لن تعيد ‎abs‎ قيمة من النوع ‎constexpr int‎- فماذا عن الشيفرة التالية؟: الإصدار ≥ C++‎ 11 constexpr int Abs(int v) { return v >= 0 ? v : -v; } constexpr int Sum(int a, int b) { return Abs(a) + b; } لقد صمّمنا الدالّة ‎Abs‎ وجعلناها دالة تعبير ثابت، كما أنّ جسم ‎Abs‎ لن يخرق أيّ قاعدة. كذلك تعطي نتيجة تقييم التعبير هي تعبير ثابت ‎constexpr‎، وذلك في موضع الاستدعاء (داخل ‎Sum‎). ومن ثم يكون استدعاء Sum(-10, 20)‎‎ تعبيرًا ثابتًا في وقت التصريف (compile-time constexpr) ينتج عنه القيمة ‎30‎. هذا الدرس جزء من سلسلة دروس عن C++‎. ترجمة -بتصرّف- للفصلين Chapter 118: static_assert و Chapter 119: constexpr من كتاب C++ Notes for Professionals
  13. نماذج الذاكرة إن حاوَلَت عدّة خيوط الوصول إلى نفس الموضع من الذاكرة، فستدخل في تسابق على البيانات (data race) إذا تسبب عملية واحدة على الأقل من العمليات المُنفّذة في تعديل البيانات -تُعرف باسم عمليات التخزين store operation-، وتتسبّب سباقات البيانات تلك في سلوك غير معرَّف. ولكي نتجنبها، امنع الخيوط من تنفيذ عمليات متضاربة (Conflicting) بشكل متزامن. يمكن استخدام أساسيات التزامن (مثل كائنات التزامن - mutex - وما شابه) لتأمين عمليات الوصول المتضاربة، وقد قدّم الإصدار C++‎ 11 نموذج ذاكرة (Memory Model) جديد، هذا النموذج قدّم طريقتين محمولتين جديدتين لمزامنة الوصول إلى الذاكرة في بيئة متعددة الخيوط، وهما: العمليات الذرية (atomic operations) والأسوار (fences). العمليات الذرية أصبح من الممكن الآن القراءة من موضع معيّن من الذاكرة أو الكتابة فيها باستخدام التحميل الذري (atomic load) وعمليات التخزين الذرية (atomic store)، والتي تُغلَّف في صنف القالب ‎std::atomic<t>‎ من باب التيسير، ويغلّف هذا الصنف قيمةً من النوع ‎t‎، لكن تحميلها وتخزينها إلى الكائن يكون ذريًّا. وهذا القالب ليس متاحًا لجميع الأنواع بل هو متعلق بالتنفيذ، لكن في العادةً يكون متاحًا لمعظم (أو جميع) الأنواع العددية الصحيحة وأنواع المؤشّرات بحيث تكون الأنواع ‎std::atomic<unsigned>‎‎ و std::atomic<std::vector<foo> *>‎ متاحة، على خلاف ‎‎std::atomic<std::pair<bool,char>‎>‎‎‎. تتميّز العمليات الذرية بالخاصّيات التالية: يمكن إجراء جميع العمليات الذرية بشكل متزامن في عدّة خيوط دون الخوف من التسبب في سلوك غير معرَّف. سيرى التحميل الذري (atomic load) القيمة الأولية التي بُنِي الكائن الذري عليها، أو القيمة المكتوبة فيه عبر عملية التخزين الذرية. تُرتَّب عمليات التخزين الذرية (Atomic stores) في كائن ذري معيّن بنفس الطريقة في جميع الخيوط، وإذا رأى خيطٌ قيمةَ عملية تخزين ذرية ما من قبل، فإنّ عمليات التحميل الذري اللاحقة سترى إما القيمة نفسها أو القيمة المُخزّنة بواسطة عملية التخزين الذرية اللاحقة. تسمح عمليات القراءة-التعديل-الكتابة (read-modify-write) الذرية بالتحميل الذري والتخزين الذري دون حدوث أي تخزين ذرّي آخر بينهما. على سبيل المثال، يمكن للمرء أن يزيد العدّادَ ذريًّا (atomically increment) عبر عدة خيوط تلقائيًا، ولن تُفقد أيّ زيادة حتى لو كان هناك تنافر (contention) بين الخيوط. تتلقى العمليات الذرية مُعاملًا اختياريًا من نوع ‎std::memory_order‎، يُعرِّف الخاصّيات الإضافية للعملية بخصوص مواضع الذاكرة الأخرى. std::memory_order الشرح std::memory_order_relaxed لا قيود إضافية std::memory_order_release-std::memory_order_acquire‎ إذا رأت ‎load-acquire‎ القيمة المُخزّنة بواسطة ‎store-release‎ فإنّ التخزينات المتسلسلة (stores sequenced) قبل ‎store-release‎ ستحدث قبل التحميلات المتسلسلة (loads sequenced) بعد اكتساب التحميل (load acquire). std::memory_order_consume مثل ‎memory_order_acquire‎ ولكن تعمل مع الأحمال غير المستقلة (dependent loads) وحسب std::memory_order_acq_rel تجمع ‎load-acquire‎ و ‎store-release‎ std::memory_order_seq_cst تناسق تسلسلي (sequential consistency) table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; } تسمح وسوم ترتيب الذاكرة أعلاه بثلاث نماذج من ترتيب الذاكرة: الاتساق المتسلسل sequential consistency. الترتيب المتراخي Relaxed Ordering. ترتيب اكتساب-تحرير release-acquire واكتساب-استهلاك release-consume. الاتساق المتسلسل (Sequential Consistency) إذا لم يعُرِّف ترتيب الذاكرة الخاص بعملية ذرية معيّنة فسيتمّ اللجوء إلى الترتيب الافتراضي، والذي هو الاتساق المتسلسل. يمكن أيضًا اختيار هذا الوضع صراحة عبر الوسْم ‎std::memory_order_seq_cst‎. وفي هذا الترتيب، لا يمكن لأيّ عملية على الذاكرة أن تتجاوز العملية الذرية، وستحدث جميع عمليات الذاكرة التسلسلية السابقة للعملية الذرية قبل العملية الذرية، كما ستحدث العملية الذرية قبل جميع عمليات الذاكرة المتسلسلة اللاحقة. هذا هو الوضع الأسهل والأوضح، لكنه قد يؤدّي إلى إضعاف الأداء ويمنع كل تحسينات المصرّف التي قد تحاول إعادة ترتيب العمليات التي تأتي بعد العملية الذرية. الترتيب المتراخي (Relaxed Ordering) الترتيب المتراخي للذاكرة هو نقيض ترتيب الاتساق المتسلسل، ويمكن تعريفه باستخدام الوسم std::memory_order_relaxed. لا تفرض العمليات الذرية المتراخية أيّ قيود على عمليات الذاكرة الأخرى، ولا يبقى تأثير سوى أنّ العملية بحد ذاتها ستبقى ذرية. ترتيب التحرير-الاكتساب (Release-Acquire Ordering) يمكن وسم عملية تخزين ذرية باستخدام ‎std::memory_order_release‎، كما يمكن وسم عملية تحميل ذري باستخدام ‎std::memory_order_acquire‎. وتُسمّى العملية الأولى تخزين-تحرير (ذري) (‎(atomic) store-release) في حين تسمّى العملية الثانية تحميل-اكتساب (ذري) (‎(atomic) load-acquire‎). وعندما ترى عمليةُ "التحميل-الاكتساب" القيمةَ المكتوبة من قبل عملية (التخزين-التحرير) فسيحدث ما يلي: ستصبح جميع عمليات التخزين المُسلسلة التي تسبق عملية (التخزين-التحرير) مرئيّة لعمليات التحميل المُسلسلة بعد عملية (التحميل-الاكتساب). يمكن أن تحصل عمليات (القراءة-التعديل-الكتابة) الذرية أيضًا على الوسم التراكمي ‎std::memory_order_acq_rel‎. هذا يجعل الجزء المتعلّق بالتحميل الذري من العملية عبارة عن عملية تحميل-اكتساب ذرّية، بينما يصبح الجزء المتعلق بالتخزين الذري عبارة عن عملية تخزين-تحرير ذرّية. لا يُسمح للمُصرِّف بنقل عمليات التخزين الموجودة بعد عملية تخزين-تحرير ذرّية ما، كما لا يُسمح له أيضًا بنقل عمليات التحميل التي تسبق عملية تحميل-اكتساب ذرية (أو تحميل-استهلاك). لاحظ أيضًا أنّه لا توجد عملية تحميل-تحرير ذرّية (atomic load-release)، ولا عملية تخزين-اكتساب ذرّية (atomic store-acquire)، وأيّ محاولة لإنشاء مثل هذه العمليات سينجم عنها عمليات متراخية (relaxed operations). ترتيب تحرير-استهلاك (Release-Consume Ordering) عمليات تحرير-استهلاك تشبه عمليات تحرير-اكتساب، لكن هذه المرّة يوسَم الحمل الذري باستخدام std::memory_order_consume لكي يصبح عملية تحميل-استهلاك (ذرية) - ‎(atomic) load-consume operation. هذا الوضع يشبه عملية تحرير-اكتساب، مع اختلاف وحيد هو أنّه من بين عمليات التحميل المُسلسلة الموجودة بعد عملية تحميل-استهلاك، فلن تُرتَّب إلا تلك التي تعتمد على القيمة التي حُمِّلت بواسطة عملية تحميل-استهلاك. الأسوار (Fences) تسمح الأسوار بترتيب عمليات الذاكرة بين الخيوط، وقد يكون السور إمّا سور تحرير (release fence)، أو سور اكتساب (acquire fence). وإذا حدث سور التحرير قبل سور الاكتساب، فستكون المخازن المتسلسلة قبل سور التحرير مرئية للأحمال المتسلسلة بعد سور الاكتساب، وإن أردت ضمان تقديم سور التحرير قبل سور الاكتساب فاستخدام بدائل التزامن الأخرى، بما في ذلك العمليات الذرية المتراخية. فائدة نموذج الذاكرة انظر المثال التالي: int x, y; bool ready = false; void init() { x = 2; y = 3; ready = true; } void use() { if (ready) std::cout << x + y; } يستدعي أحد الخيطين دالة ‎init()‎، بينما يستدعي الخيط الآخر (أو معالج الإشارة) الدالةَ ‎use()‎. قد يتوقع المرء أنّ الدالّة ‎use()‎ إمّا ستطبع القيمة ‎5‎، أو لن تفعل أيّ شيء، لكن هذه الحالة قد لا تحدث كل مرة، لعدّة أسباب: قد تعيد وحدة المعالجة المركزية ترتيب عمليات الكتابة التي تحدث في ‎init()‎ بحيث تبدو الشيفرة التي ستنفذ على الشكل التالي: void init() { ready = true; x = 2; y = 3; } قد تعيد وحدة المعالجة المركزية ترتيب عمليّات القراءات التي تحدث في ‎use()‎ بحيث تصبح الشيفرة التي ستُنفّذ على الشكل التالي: void use() { int local_x = x; int local_y = y; if (ready) std::cout << local_x + local_y; } قد يقرّر مصرّف C++‎ إعادة ترتيب البرنامج بطريقة مماثلة لتحسين الأداء. لو كان البرنامج يُنفّذ في خيط واحد فلا يمكن أن يتغيّر سلوك البرنامج نتيجة لإعادة الترتيب، ذلك لأنّ الخيط لا يمكن أن يخلط بين استدعائي ‎init()‎ و ‎use()‎. أمّا في حالة الخيوط المتعددة، فقد يرى أحد الخيوط جزءًا من عمليات الكتابة التي يؤدّيها الخيط الآخر، حيث أنّ ‎use()‎ قد ترى ‎ready==true‎ وكذلك المُهملات (garbage) في ‎x‎ أو ‎y‎ أو كليهما. يسمح نموذج الذاكرة في C++‎ للمبرمج بأن يعيد تعريف عمليات إعادة الترتيب المسموح بها أو غير المسموح بها، بحيث يمكن توقّع سلوك البرامج متعددة الخيوط. يمكن إعادة كتابة المثال أعلاه بطريقة ملائمة لتعدّد الخيوط على النحو التالي: int x, y; std::atomic < bool > ready { false }; void init() { x = 2; y = 3; ready.store(true, std::memory_order_release); } void use() { if (ready.load(std::memory_order_acquire)) std::cout << x + y; } في المثال أعلاه، تُجري دالة ‎init()‎ عملية تخزين-تحرير ذرية، هذا لن يخزّن قيمة ‎true‎ في ‎ready‎ وحسب، ولكن سيُخطِر أيضًا المُصرِّف بأنّه لا يمكن نقل هذه العملية قبل عمليات الكتابة المتسلسلة التي تسبقها. كذلك تُجري الدالّة ‎use()‎ عملية تحميل-اكتساب ذرية. إذ تقرأ القيمة الحالية لـ ‎ready‎، وتمنع المُصرِّف من تقديم عمليات القراءة المتسلسلة اللاحقة قبل عملية تحميل-اكتساب الذرية. هذه العمليات الذرية تجعل المصرّف يضع الإرشادات الضرورية المتعلقة بالجهاز لإبلاغ وحدة المعالجة المركزية بالامتناع عن تقديم عمليات إعادة الترتيب غير المرغوب فيها. ونظرًا لأنّ عملية تخزين-تحرير الذرية تقع في نفس موقع الذاكرة الخاص بعملية تحميل-اكتساب، ينصّ نموذج الذاكرة على أنّه إذا كانت عملية تحميل-اكتساب ترى القيمة المكتوبة بواسطة عملية تخزين-تحرير، فإنّ جميع عمليات الكتابة التي تُنفّذ بواسطة دالة الخيط ‎init()‎ التي تسبق عملية التخزين-التحرير (store-release) ستكون مرئية للتحميلات التي تنفّذها دالة الخيط ‎use()‎ بعد عملية تحميل-اكتساب. أي أنّه في حال رأت الدالةُ ‎use()‎ تعليمة ‎ready==true‎، فسترى كذلك بالضرورة ‎x==2‎ و ‎y==3‎. لاحظ أنّ المُصرِّف ووحدة المعالجة المركزية لا يزالان يستطيعان الكتابة في ‎y‎ قبل الكتابة في ‎x‎، وبالمثل يمكن أن تحدث عمليات القراءة من المتغيّرات في ‎use()‎ وفق أيّ ترتيب. مثال على الأسوار (Fences) يمكن أيضًا تقديم المثال أعلاه باستخدام الأسوار والعمليات الذرية المتراخية: int x, y; std::atomic < bool > ready { false }; void init() { x = 2; y = 3; atomic_thread_fence(std::memory_order_release); ready.store(true, std::memory_order_relaxed); } void use() { if (ready.load(std::memory_order_relaxed)) { atomic_thread_fence(std::memory_order_acquire); std::cout << x + y; } } إذا رأت عملية التحميل الذرية القيمةَ المكتوبة بواسطة عملية التخزين الذري، فسيحدث التخزين قبل التحميل، وكذلك الحال مع الأسوار: يحدُث تحرير السور قبل اكتساب السور، ما يجعل عملية الكتابة في ‎x‎ و ‎y‎ التي تسبق سور التحرير مرئية للعبارة ‎std::cout‎ التي تلي اكتساب السور. قد يكون استخدام السور مفيدًا في حال كان يقلّل العدد الإجمالي لعمليات الاكتساب أو التحرير أو عمليات المزامنة الأخرى. مثلّا: void block_and_use() { while (!ready.load(std::memory_order_relaxed)) ; atomic_thread_fence(std::memory_order_acquire); std::cout << x + y; } ويستمرّ تنفيذ الدالّة ‎block_and_use()‎ إلى أن تُضبَط قيمة راية ‎ready‎ بمساعدة التحميل الذري المتراخي، ثم يُستخدَم سور اكتساب واحد لتوفير ترتيب الذاكرة المطلوب. إدارة الذاكرة (Memory management) التخزين الحرّ (Free Storage) مصطلح "الكومة" (heap) هو مصطلح عام في الحوسبة يشير إلى مساحة من الذاكرة يمكن تخصيص أجزاء منها أو تحريرها بشكل مستقل عن الذاكرة التي يوفرها المكدّس (stack). ويسمي المعيار في ++C هذه المساحة بالتخزين الحر، وهو أكثر دقة من الكومة. وقد تبقى مناطق الذاكرة المخصصة للتخزين الحرّ حتّى بعد الخروج من النطاق الأصلي الذي خُصِّصت فيه، ويمكن تخصيص ذاكرة للبيانات في التخزين الحر إن كانت مساحة البيانات أكبر من أن تُخزَّن في المكدّس. تُستخدم الكلمتان المفتاحيتان new و delete لتخصيص الذاكرة الخام (Raw memory) وتحريرها. float *foo = nullptr; { *foo = new float; // تخصيص ذاكرة لعدد عشري float bar; // تخصيص المكدّس } // ما تزال باقية foo لكنّ bar نهاية delete foo; // وهذا يؤدي إلى جعل المؤشّر غير صالح ،pF حذف ذاكرة العدد العشري الموجودة عند foo = nullptr; // من الممارسات السيئة `nullptr` يُعد ضبط المؤشر عند القيمة. من الممكن أيضًا تخصيص ذاكرة للمصفوفات ذات الحجم الثابت بكلمتيْ new و delete، لكن مع صيغة مختلفة قليلاً، ذلك أن تخصيص ذاكرة المصفوفات يختلف عن تخصيص الذاكرة للكائنات الأخرى، وسيؤدّي خلط الاثنتين إلى عطب في الكومة (heap corruption). ويؤدي تخصيص ذاكرة المصفوفات أيضًا إلى تخصيص ذاكرة مخصّصة لتعقّب حجم المصفوفة، وذلك لأجل استخدامها عند حذف المصفوفة لاحقًا (تتعلق بالتنفيذ). // تخصيص ذاكرة مؤلّفة من 256 عددًا صحيحًا int *foo = new int[256]; // حذف المصفوفة delete[] foo; سيُنفَّذ المنشئ والمدمّر -كما هو حال الكائنات في المكدّس (Stack based objects)- عند استخدام new و delete بدلًا من malloc و free، لهذا فإنّ خيار new و delete خير من malloc و free. انظر المثال التالي حيث نخصص ذاكرة لنوعٍ ComplexType، ونستدعي منشئه، ثم نستدعي مدمر ()ComplexType ونحذف ذاكرة Complextype عند الموضع pC. struct ComplexType { int a = 0; ComplexType() { std::cout << "Ctor" << std::endl; } ~ComplexType() { std::cout << "Dtor" << std::endl; } }; ComplexType *foo = new ComplexType(); delete foo; الإصدار ≥ C++‎ 11 يوصى باستخدام المؤشّرات الذكية منذ الإصدار C++‎ 11 للإشارة إلى المِلكِيّة. الإصدار ≥ C++‎ 14 C++‎ 14 أضافت ‎std::make_unique‎ إلى مكتبة القوالب القياسية STL، مغيرة بذلك الإرشادات لتفضيل ‎std::make_unique‎ أو std::make_shared على استخدام new و delete. new قد لا ترغب في بعض الحالات في الاعتماد على التخزين الحرّ (Free Store) لتخصيص الذاكرة، وتريد تخصيص ذاكرة مخصصة باستخدام ‎new‎. عندئذ يمكنك استخدام ‎Placement New‎، بحيث تخبر المعامل "new" بأن يخصّص الذاكرة من موضع مُخصص مسبقًا. انظر: int a4byteInteger; char *a4byteChar = new (&a4byteInteger) char[4]; في المثال السابق ، الذاكرة المشار إليها عبر ‎a4byteChar‎ هي 4 بايت، مخصصة "للمكدّس" عبر المتغيّر الصحيح ‎a4byteInteger‎. وفائدة هذا النوع من تخصيص الذاكرة أنه سيكون للمبرمجين تحكّم كامل في التخصيص، فبما أن ذاكرة ‎a4byteInteger‎ في المثال أعلاه مُخصّصة في المكدّس فلن تحتاج إلى استدعاء صريح لـ a4byteChar delete. يمكن تحقيق نفس السلوك في حالة تخصيص ذاكرة ديناميكية أيضًا. مثلّا: int *a8byteDynamicInteger = new int[2]; char *a8byteChar = new (a8byteDynamicInteger) char[8]; يشير مؤشّر الذاكرة ‎a8byteChar‎ في هذه الحالة إلى الذاكرة الديناميكية المخصصة عبر ‎a8byteDynamicInteger‎. لكن مع ذلك، سنحتاج في هذه الحالة إلى استدعاء ‎a8byteDynamicInteger‎ صراحةً لتحرير الذاكرة. هذا مثال آخر: #include <complex> #include <iostream> struct ComplexType { int a; ComplexType(): a(0) {} ~ComplexType() {} }; int main() { char* dynArray = new char[256]; نستدعي منشئ ComplexType لتهيئة الذاكرة كـ ComplexType، نتابع … new((void* ) dynArray) ComplexType(); // تنظيف الذاكرة بعد الانتهاء reinterpret_cast<ComplexType*>(dynArray)->~ComplexType(); delete[] dynArray; // placement new يمكن أيضا استخدام ذاكرة المكدّس مع alignas(ComplexType) char localArray[256]; //alignas() available since C++11 new((void* ) localArray) ComplexType(); // لا تحتاج إلى استدعاء المدمّر إلا لذاكرة المكدّس reinterpret_cast<ComplexType*>(localArray)->~ComplexType(); return 0; } المكدّس المكدّس هو منطقة صغيرة من الذاكرة توضع فيها القيم المؤقتة أثناء التنفيذ، ويعدّ تخصيص البيانات فيه سريعًا جدًا مقارنة بتخصيصها في الكومة، إذ أنّ الذاكرة كلها مسنَدة سلفًا لهذا الغرض. int main() { int a = 0; // مُخزّنة على المكدّس return a; } وقد سُمِّي مكدّسًا لأنّ الاستدعاءات المتسلسلة للدوال ستكون لها ذاكرة مؤقتة "مُكدّسة" فوق بعضها البعض، وكل واحدة ستستخدم قسمًا صغيرًا منفصلًا من الذاكرة. في المثال التالي، ستوضع f على المكدس في النهاية بعد كل وضع كل شيء (انظر 1)، وتوضع d كذلك بعد كل شيء في نطاق ()main (انظر 2): float bar() { // (1) float f = 2; return f; } double foo() { // (2) double d = bar(); return d; } int main() { // foo() لا تُخزّن في المكدّس أيّ متغيرات خاصّة بالمستخدم إلى حين استدعاء return (int) foo(); } ستبقى البيانات المخزّنة على المكدّس صالحة ما دام النطاق الذي خَصص المتغيّر نشطًًا. int* pA = nullptr; void foo() { int b = *pA; pA = &b; } int main() { int a = 5; pA = &a; foo(); // خارج النطاق pA سلوك غير معرَّف، أصبحت القيمة التي يشير إليها a = *pA; } هذا الدرس جزء من سلسلة دروس عن C++‎. ترجمة -بتصرّف- للفصل Chapter 115: Memory management و Chapter 116: C++11 Memory Model من كتاب C++ Notes for Professionals
  14. القيم الثنائية مصنفة النوع المُعرّفة من المستخدم (Self-made user-defined literal for binary) رغم إمكانية كتابة عدد ثنائي في C++‎ 14 على النحو التالي: int number =0b0001'0101; // ==21 إلا أننا في الأسطر التالية سنستعرض مثالًا مشهورًا يوفّر طريقة أخرى ذاتية التنفيذ للأعداد الثنائية. لاحظ أن برنامج توسيع القالب التالي يعمل في وقت التصريف. template < char FIRST, char...REST > struct binary { static_assert(FIRST == '0' || FIRST == '1', "invalid binary digit"); enum { value = ((FIRST - '0') << sizeof...(REST)) + binary < REST... > ::value }; }; template < > struct binary < '0' > { enum { value = 0 }; }; template < > struct binary < '1' > { enum { value = 1 }; }; // عامل قيمة خام مصنفة النوع template < char...LITERAL > inline constexpr unsigned int operator "" _b() { return binary < LITERAL... > ::value; } // عاملُ قيمةٍ خامٍ مصنَّفةِ النوع template < char...LITERAL > inline constexpr unsigned int operator "" _B() { return binary < LITERAL... > ::value; } #include <iostream> int main() { std::cout << 10101_B << ", " << 011011000111_b << '\n'; // تطبع 21, 1735 } القيم مصنفة النوع المعيارية المُعرّفة من المستخدم (Standard user-defined literals for duration) الإصدار ≥ C++‎ 14 فيما يلي قيمُ مدةٍ مصنَّفةِ النوع، ومعرَّفة من قِبل المستخدم (duration user literals)، مصرح عنها في فضاء الاسم namespace std::literals::chrono_literals، حيث ‎literals‎ و ‎chrono_literals‎ هما فضاءا اسم ضمنيّان (inline namespaces). يمكن الوصول إلى هذه العوامل باستخدام using namespace std::literals و using namespace std::chrono_literals و using namespace std::literals::chrono_literals. #include <chrono> #include <iostream> int main() { using namespace std::literals::chrono_literals; std::chrono::nanoseconds t1 = 600ns; std::chrono::microseconds t2 = 42us; std::chrono::milliseconds t3 = 51ms; std::chrono::seconds t4 = 61s; std::chrono::minutes t5 = 88min; auto t6 = 2 * 0.5h; auto total = t1 + t2 + t3 + t4 + t5 + t6; std::cout.precision(13); std::cout << total.count() << " nanoseconds" << std::endl; // 8941051042600 nanoseconds std::cout << std::chrono::duration_cast < std::chrono::hours > (total).count() << " hours" << std::endl; // ساعتان } القيم مصنَّفة النوع المُعرّفة من المستخدم، ذات قيَم long double يوضّح المثال التالي كيفية استخدام قيم مصنَّفة النوع، مُعرّفة من المستخدم وذات قيَم long double: #include <iostream> long double operator "" _km(long double val) { return val * 1000.0; } long double operator "" _mi(long double val) { return val * 1609.344; } int main() { std::cout << "3 km = " << 3.0_km << " m\n"; std::cout << "3 mi = " << 3.0_mi << " m\n"; return 0; } خرج هذا البرنامج هو: 3 km = 3000 m 3 mi = 4828.03 m السلاسل النصية المجردة القياسية والمعرّفة من المستخدم (Standard user-defined literals for strings) الإصدار ≥ C++‎ 14 فيما يلي سلاسل نصية مجردةٌ ومُعرّفة من المستخدم (string user literals)، مُصرَّح عنها في namespace std::literals::string_literals، حيث ‎literals‎ و ‎string_literals‎ هما فضاءا اسم مُضمّنان. ويمكن الوصول إلى هذه العوامل باستخدام using namespace std::literals و using namespace std::string_literals و using namespace std::literals::string_literals. #include <codecvt> #include <iostream> #include <locale> #include <string> int main() { using namespace std::literals::string_literals; std::string s = "hello world"s; std::u16string s16 = u"hello world"s; std::u32string s32 = U"hello world"s; std::wstring ws = L"hello world"s; std::cout << s << std::endl; std::wstring_convert < std::codecvt_utf8_utf16 < char16_t > , char16_t > utf16conv; std::cout << utf16conv.to_bytes(s16) << std::endl; std::wstring_convert < std::codecvt_utf8_utf16 < char32_t > , char32_t > utf32conv; std::cout << utf32conv.to_bytes(s32) << std::endl; std::wcout << ws << std::endl; } ملاحظة: قد تحتوي السلاسل النصية المجردة على المحرف ‎\0‎، انظر المثال التالي: // "foo"s النصية سينتج عنها C منشئات سلاسل std::string s1 = "foo\0\0bar"; // '\0' تحتوي هذه السلسلة النصية في وسطها على محرفين std::string s2 = "foo\0\0bar"s; القيم مصنفة النوع المركّبة المعرّفة من المستخدم (Standard user-defined literals for complex) الإصدار ≥ C++‎ 14 فيما يلي، قيم مركّبة مصنفة النوع ومعرّفة من المستخدم، مُصرًّح عنها فيnamespace std::literals::complex_literals، حيث ‎literals‎ و ‎complex_literals‎ فضاءا اسم ضمنيان. يمكن الوصول إلى هذه العوامل باستخدام using namespace std::literals و using namespace std::complex_literals و using namespace std::literals::complex_literals. #include <complex> #include <iostream> int main() { using namespace std::literals::complex_literals; std::complex < double > c = 2.0 + 1i; // {2.0, 1.} std::complex < float > cf = 2.0f + 1if; // {2.0f, 1.f} std::complex < long double > cl = 2.0L + 1il; // {2.0L, 1.L} std::cout << "abs" << c << " = " << abs(c) << std::endl; // abs(2,1) = 2.23607 std::cout << "abs" << cf << " = " << abs(cf) << std::endl; // abs(2,1) = 2.23607 std::cout << "abs" << cl << " = " << abs(cl) << std::endl; // abs(2,1) = 2.23607 } هذا الدرس جزء من سلسلة دروس عن C++‎. ترجمة -بتصرّف- للفصل Chapter 114: User-Defined Literals من كتاب C++ Notes for Professionals
  15. سنستعرض في هذا الدرس بعض أنماط التصميم الشهيرة في C++‎ ثم سنتطرق سريعًا إلى مفهوم إعادة التصميم (Refactoring) والنمط Goto Cleanup المتَّبع. نمط المحوِّل (Adapter Pattern) يتيح نمط المحوِّل للأصناف غير المتوافقة أن تعمل معًا، والسبب الأساسي في استخدامه تكمن في أنّه يمكّن المطوّرين من إعادة استخدام البرامج الموجودة عبر تعديل الواجهة فقط. يعتمد نمط المحول على تركيب الكائنات (object composition). العميل يستدعي العملية على المحوِّل. يستدعي المحوِّل الكائن المحوَّل Adaptee لتنفيذ العملية. تُبنى المكدّسات (stacks) في مكتبة القوالب القياسية STL على المتجهات، فمثلًا: عندما ينفّذ المُكدّس الدّالةَ push()‎، فإنّ المتجه الأساسي (underlying vector) سينفذ التابع vector::push_back()‎. انظر المثال التالي: #include <iostream> // الواجهة المقصودة class Rectangle { public: virtual void draw() = 0; }; // المركّب القديم - المحوَّل class LegacyRectangle { public: LegacyRectangle(int x1, int y1, int x2, int y2) { x1_ = x1; y1_ = y1; x2_ = x2; y2_ = y2; std::cout << "LegacyRectangle(x1,y1,x2,y2)\n"; } void oldDraw() { std::cout << "LegacyRectangle: oldDraw(). \n"; } private: int x1_; int y1_; int x2_; int y2_; }; // Adapter wrapper مغلِّف المحوَّل class RectangleAdapter: public Rectangle, private LegacyRectangle { public: RectangleAdapter(int x, int y, int w, int h): LegacyRectangle(x, y, x + w, y + h) { std::cout << "RectangleAdapter(x,y,x+w,x+h)\n"; } void draw() { std::cout << "RectangleAdapter: draw().\n"; oldDraw(); } }; int main() { int x = 20, y = 50, w = 300, h = 200; Rectangle * r = new RectangleAdapter(x, y, w, h); r -> draw(); } // :الخرج //LegacyRectangle(x1,y1,x2,y2) //RectangleAdapter(x,y,x+w,x+h) شرح الشيفرة أعلاه: يعتقد العميل أنّه يتحدث إلى ‎Rectangle‎ الهدف هو الصنف ‎Rectangle‎، وهو الذي سيستدعي العميلُ التابعَ عليه. Rectangle * r = new RectangleAdapter(x, y, w, h); r -> draw(); لاحظ أنّ صنف المحوِّل (adapter class) يستخدم الوراثة المتعدّدة. class RectangleAdapter: public Rectangle, private LegacyRectangle { ... } يتيح المحوِّل‎RectangleAdapter‎ للصنف ‎LegacyRectangle‎ الاستجابة للطلب (استدعاء ‎draw()‎ على ‎Rectangle‎) عن طريق وراثة الصنفين معًا. لا يملك الصنف ‎LegacyRectangle‎ نفس التوابع (‎draw()‎) التي يملكها ‎Rectangle‎، لكن يمكن أن يأخذ Adapter(RectangleAdapter)‎ استدعاءات التابعِ ‎Rectangle‎ ثمّ يعود لاستدعاء التابع ‎LegacyRectangle‎ على ‎oldDraw()‎. class RectangleAdapter: public Rectangle, private LegacyRectangle { public: RectangleAdapter(int x, int y, int w, int h): LegacyRectangle(x, y, x + w, y + h) { std::cout << "RectangleAdapter(x,y,x+w,x+h)\n"; } void draw() { std::cout << "RectangleAdapter: draw().\n"; oldDraw(); } }; يُترجمُ نمط المحوِّل واجهةَ صنف معيّن إلى واجهة أخرى متوافقة، ولكن مختلفة. لذلك، فهو يشبه نمط الوكيل من حيث أنّه مغلّف أحادي المكوّنات (single-component wrapper)، لكن قد تكون واجهة الصنف المحوَّل والصنف الأصلي مختلفة. ويمكن استخدام نمط المحوِّل لإظهار واجهة برمجية (API) معيّنة للسماح لها بالعمل مع شيفرات أخرى كما رأينا في المثال أعلاه. أيضًا، يمكننا أن نأخذ واجهات غير متجانسة، ونحوّلها لواجهة برمجية موحّدة ومتسقة. لدى نمط الجسر بنية مشابهة للكائنات المحوِّلة، بيْد أنّ للجسور هدفًا مختلفًا، إذ يُرادُ منها فصل الواجهة عن التقديم، حتّى يسهل تعديلها بشكل مستقل. أمّا المحوِّل فيُراد منه تعديل واجهة كائن موجود. نمط المراقب (Observer pattern) الهدف من نمط المراقب هو تعريف اعتمادية واحد-إلى-متعدد (one-to-many) بين الكائنات، بحيث إذا تغيرت حالة كائن تُرسل إشعارات إلى جميع الكائنات المتعلّقة به وتحديثها تلقائيًا. يعرِّف الهدف (subject) والمراقبُون (observers) اعتمادية الواحد-إلى-متعدد، وفي هذه الاعتمادية يعتمد المراقبون على الأهداف، وعندما تتغيّر حالة الهدف يتم إشعار المراقبين تلقائيًا. وبناءً على ذلك يمكن تحديث المراقبين وإعطاؤهم قيمًا جديدة. فيما يلي مثال من كتاب "Design Patterns" من تأليف جاما (Gamma). #include <iostream> #include <vector> class Subject; class Observer { public: virtual ~Observer() = default; virtual void Update(Subject&) = 0; }; class Subject { public: virtual ~Subject() = default; void Attach(Observer& o) { observers.push_back(&o); } void Detach(Observer& o) { observers.erase(std::remove(observers.begin(), observers.end(), &o)); } void Notify() { for (auto* o : observers) { o->Update(*this); } } private: std::vector<Observer*> observers; }; class ClockTimer: public Subject { public: void SetTime(int hour, int minute, int second) { this -> hour = hour; this -> minute = minute; this -> second = second; Notify(); } int GetHour() const { return hour; } int GetMinute() const { return minute; } int GetSecond() const { return second; } private: int hour; int minute; int second; }; class DigitalClock: public Observer { public: explicit DigitalClock(ClockTimer& s) : subject(s) { subject.Attach(*this); } ~DigitalClock() { subject.Detach( *this); } void Update(Subject& theChangedSubject) override { if ( &theChangedSubject == &subject) { Draw(); } } void Draw() { int hour = subject.GetHour(); int minute = subject.GetMinute(); int second = subject.GetSecond(); std::cout << "Digital time is " << hour << ":" << minute << ":" << second << std::endl; } private: ClockTimer& subject; }; class AnalogClock: public Observer { public: explicit AnalogClock(ClockTimer& s): subject(s) { subject.Attach( *this); } ~AnalogClock() { subject.Detach( * this);} void Update(Subject& theChangedSubject) override { if ( &theChangedSubject == &subject) { Draw(); } } void Draw() { int hour = subject.GetHour(); int minute = subject.GetMinute(); int second = subject.GetSecond(); std::cout << "Analog time is " << hour << ":" << minute << ":" << second << std::endl; } private: ClockTimer& subject; }; int main() { ClockTimer timer; DigitalClock digitalClock(timer); AnalogClock analogClock(timer); timer.SetTime(14, 41, 36); } الخرج: Digital time is 14: 41: 36 Analog time is 14: 41: 36 توضّح النقاط التالية ملخّص نمط المراقب: تستخدم الكائنات (‎DigitalClock‎ أو ‎AnalogClock‎) واجهات الموضوع (‎Attach()‎ أو ‎Detach()‎) إمّا لأجل الاشتراك (subscribe) كمراقبين، أو إلغاء الاشتراك (إزالة أنفسهم) من كونهم مراقبين (‎subject.Attach(*this);‎، ‎subject.Detach(*this);‎. يمكن أن يكون لكل موضوع عدّة مراقبين (‎vector<Observer*> observers;‎). يحتاج جميع المراقبين إلى تنفيذ واجهة المراقب (Observer interface). لدى هذه الواجهة تابع واحد فقط، وهو ‎Update()‎، ويُستدعى عند تغيّر حالة الموضوع (‎Update(Subject &)‎) بالإضافة إلى التابعين ‎Attach()‎ و ‎Detach()‎، ينفِّذ الهدف الحقيقي التابعَ ‎Notify()‎ الذي يُستخدم لتحديث جميع المراقبين الحاليين عندما تتغيّر الحالة، لكن تتم جميعها في هذه الحالة في الصنف الأب، Subject (Subject::Attach (Observer&)‎ و ‎void Subject::Detach(Observer&)‎ و void Subject::Notify()‎. قد يحتوي الكائن الحقيقي أيضًا على توابع لضبط قيمة حالته، أو الحصول عليها. يمكن أن تكون المراقبات الحقيقية أيّ صنف ينفذ واجهة المراقب (Observer interface)، ويشترك كل مراقب مع هدف حقيقي ليحصل على التحديثات (‎subject.Attach(*this);‎). كائنا نمط المراقب مترابطان بشكل طفيف، إذ يمكنهما التفاعل مع بعضهما البعض، لكنّ معرفتها ببعضهما محدودة. الإشارات والفتحات (Slots) الإشارات والفتحات (Slots) هي بنية لغوية قُدِّمت في Qt، وتسهّل على المطوّرين تقديم نمط المراقب دون الحاجة لاستخدام الشيفرات المُتداولة (boilerplate code). الفكرة الرئيسية وراء الإشارات والفتحات هي أنّ عناصر التحكم (controls) التي تُعرف أيضًا باسم الودجات widgets) يمكنها إرسال إشارات تحتوي على معلومات حول الحدث، والتي يمكن استقبالها من قبل عناصر تحكم أخرى باستخدام دوال خاصة تُعرف باسم الفتحات (slots)، وهي أعضاء أصناف في Qt. يتوافق نظام الإشارة / الفتحة مع تصميم واجهات المستخدم الرسومية، كما يمكن استخدام نظام الإشارة-الفتحة للدخل / الخرج غير المتزامن (asynchronous I/O) بما في ذلك المقابس sockets، والأنابيب pipes، والأجهزة التسلسلية serial devices، وغيرها مما يختص بإشعارات الأحداث أو لربط أزمنة الأحداث (timeout events) مع نُسخ الكائن والتوابع أو الدوالّ المناسبة. لا يلزم كتابة شيفرة خاصة بالتسجيل/إلغاء التسجيل/الاستدعاء، لأنّ الكائن الوصفي للمصرّف (Meta Object Compiler أو اختصارًا MOC) الخاصّ بـ Qt يولّد البنية الأساسية اللازمة تلقائيًا . تدعم لغة C#‎ أيضًا إنشاءات مشابهة، لكنها تستخدم مصطلحات وصيغة مختلفة: فالإشارات تسمّى أحداثًا، والفتحات تسمّى مفوِّضَات (delegates). إضافة إلى ذلك يمكن أن يكون المفوّض متغيّرًا محليًا، مثل مؤشّرات الدوال، بينما يجب أن تكون الفتحة في Qt عضوًا في صنف. نمط المصنع (Factory Pattern) يقسّم نمط المصنع (Factory pattern) عمليّة إنشاء الكائنات، ويتيح الإنشاء بالاسم باستخدام واجهة مشتركة: class Animal { public: virtual std::shared_ptr < Animal > clone() const = 0; virtual std::string getname() const = 0; }; class Bear: public Animal { public: virtual std::shared_ptr < Animal > clone() const override { return std::make_shared < Bear > ( * this); } virtual std::string getname() const override { return "bear"; } }; class Cat: public Animal { public: virtual std::shared_ptr < Animal > clone() const override { return std::make_shared < Cat > ( *this); } virtual std::string getname() const override { return "cat"; } }; class AnimalFactory { public: static std::shared_ptr < Animal > getAnimal(const std::string& name) { if (name == "bear") return std::make_shared < Bear > (); if (name == "cat") return std::shared_ptr < Cat > (); return nullptr; } }; نمط الباني يفصل نمط الباني (Builder Pattern) عملية إنشاء الكائن عن الكائن نفسه، والفكرة الرئيسية وراء ذلك هي أنّ الكائن ليس عليه مسؤولية إنشائه، وقد تكون عمليّة تصريف الكائنات المعقّدة مهمّة معقدة في حدّ ذاتها، لذا يمكن تفويض هذه المهمة إلى صنف آخر. سأنشئ فيما يلي بانيًا بريديًا Email Builder بلغة C++‎، وهو مستوحى من فكرة مشابهة في في C#‎، كائن البريد الإلكتروني ليس بالضرورة كائنًا معقدًا، ولكنّه مثال جيد لتوضيح كيفية عمل هذا النمط. #include <iostream> #include <sstream> #include <string> using namespace std; // التصريح اللاحق للباني class EmailBuilder; class Email { public: friend class EmailBuilder; // Email يمكن للباني الوصول إلى الأعضاء الخاصة في static EmailBuilder make(); string to_string() const { stringstream stream; stream << "from: " << m_from << "\nto: " << m_to << "\nsubject: " << m_subject << "\nbody: " << m_body; return stream.str(); } private: Email() = default; // قصر الإنشاء على الباني string m_from; string m_to; string m_subject; string m_body; }; class EmailBuilder { public: EmailBuilder& from(const string &from) { m_email.m_from = from; return *this; } EmailBuilder& to(const string &to) { m_email.m_to = to; return *this; } EmailBuilder& subject(const string &subject) { m_email.m_subject = subject; return *this; } EmailBuilder& body(const string &body) { m_email.m_body = body; return *this; } operator Email&& () { return std::move(m_email); // لاحظ عمليّة النقل } private: Email m_email; }; EmailBuilder Email::make() { return EmailBuilder(); } // مثال إضافي std::ostream& operator << (std::ostream& stream, const Email& email) { stream << email.to_string(); return stream; } int main() { Email mail = Email::make().from("me@mail.com") .to("you@mail.com") .subject("C++ builders") .body("I like this API, don't you?"); cout << mail << endl; } بالنسبة للإصدارات الأقدم من C++‎، يمكن تجاهل عملية ‎std::move‎ وإزالة && من عامل التحويل لكنّ هذا سيؤدّي إلى إنشاء نسخة مؤقتة. ينهي المنشئ عمله عندما يُرسَل البريد الإلكتروني بواسطة ‎operator Email&&()‎. يكون المنشئ في هذا المثال كائنًا مؤقتًا، ويعيد البريدَ الإلكتروني قبل تدميره. يمكنك أيضًا استخدام عملية صريحة مثل ‎Email EmailBuilder::build() {...}‎ بدلًا من عامل التحويل. تمرير الباني من الميزات الرائعة التي يوفّرها "نمط الباني" هي القدرةُ على استخدام عدّة عوامل (actors) لإنشاء كائن معيّن، ويمكن ذلك عن طريق تمرير المنشئ إلى العوامل الأخرى، والتي سيعطي كل منها بعض المعلومات الإضافية للكائن المبنِيّ. هذا مفيد بشكل خاص عندما تريد بناء الاستعلامات query، أو إضافة المُرشِّحات، وغيرها من المواصفات. void add_addresses(EmailBuilder& builder) { builder.from("me@mail.com") .to("you@mail.com"); } void compose_mail(EmailBuilder& builder) { builder.subject("I know the subject") .body("And the body. Someone else knows the addresses."); } int main() { EmailBuilder builder; add_addresses(builder); compose_mail(builder); Email mail = builder; cout << mail << endl; } الكائنات القابلة للتغيير يمكنك تغيير تصميم نمط الباني بما يناسب احتياجاتك، سنوضّح هذا الأمر في هذه الفقرة: كائن البريد الإلكتروني في المثال السابق كان غير قابل للتغيير (immutable)، أي أنّه لا يمكن تعديل خاصّياته لأنّه لا يمكن الوصول إليها، وقد كانت هذه الميزة مطلوبة، لكن ماذا لو كنت بحاجة إلى تعديل الكائن بعد إنشائه، سيكون عليك أن توفّر له بعض الضوابط (setters). ولمّا كانت تلك الضوابط تتكرّر في المنشئ، فقد تفكر في جمعها جميعًا في صنف واحد -لن تكون هناك حاجة للصنف الباني إذن-. لكن قد يكون الأفضل جعل الكائن المبنِيّ قابلاً للتغيير. نمط تصميم المفردة (Singleton Design Pattern) التهيئة المُرجأة (Lazy Initialization) عثرت على هذا المثال في قسم ‎Q & A‎ في هذا الرابط. انظر أيضًا هذه المقالة للحصول على تصميم بسيط لتقييم مُرجأ مع مفردة مضمونة التدمير. انظر المثال التالي عن مفردة تقليدية ذات تقييم مُرجأ ومُدمَّرة بشكل صحيح. class S { public: static S& getInstance() { static S instance; // تدميرها مضمون // تُستنسخ عند أوّل استخدام return instance; } private: S() {}; // القوسان المعقوصان ضروريان هنا // C++ 03 // ======== // لا تنس التصريح عن هذين الاثنين، احرص على أن يكونا غير مقبولين // وإلّا فقد تُنسخ المفردة S(S const&); // لا تنفذها void operator=(S const&); // لا تنفذها // C++ 11 // ======= // بإمكاننا أيضا استخدام طريقة حذف التوابع، لكنّنا لن نفعل public: S(S const& ) = delete; void operator = (S const& ) = delete; }; ملاحظة: ذكر سكوت مايرز (Scott Meyers) في كتابه Effective Modern C++ أن التوابع المحذوفة يجب أن تكون عامة، فذلك يسهل اكتشاف الأخطاء لأن رسائل الخطأ تكون أفضل حينها، فالمصرِّفات تتحقق من قابلية الوصول (accessibility) قبل الحالة المحذوفة. يمكنك معرفة المزيد عن المتفرّدات من الروابط التالية: توضّح هذه الصفحة متى يجب استخدام نمط المفردة: نمط المفردة Singleton في موسوعة حسوب راجع هاتين المقالتين الأجنبيتين لمزيد من المعلومات حول ترتيب التهيئة وكيفية التعامل معها: Static variables initialisation order Finding C++ static initialization order problems تصف هذه المقالة الأجنبية دورة الحياة لمتغير ساكن في دالة ++C: What is the lifetime of a static variable in a C++ function?‎ تناقش المقالة الأجنبية التالية بعض تأثيرات الخيوط على المفردات: Singleton instance declared as static variable of GetInstance method توضّح هذه المقالة الأجنبية لماذا لن يعمل قفل التحقق المزدوج (double checked locking) في C++‎: ‏What are all the common undefined behaviours that a C++ programmer should know about?‎ المفردات الساكنة الآمنة من إلغاء التهيئة (Static deinitialization-safe singleton) قد تعتمد بعض الكائنات الساكنة (static objects) في بعض الحالات على المفردة، وقد ترغب في ضمان منع تدميرها إلا عند عدم الحاجة إليها. لأجل ذلك يمكن استخدام ‎std::shared_ptr‎ لمنع تدمير المُتفرّدات وإبقائها متاحة لجميع من يستخدمها حتى عندما تُستدعى المدمّرات الساكنة في نهاية البرنامج: class Singleton { public: Singleton(Singleton const&) = delete; Singleton& operator=(Singleton const&) = delete; static std::shared_ptr < Singleton > instance() { static std::shared_ptr < Singleton > s { new Singleton }; return s; } private: Singleton() {} }; ملاحظة: يظهر هذا المثال كإجابة في قسم الأسئلة والأجوبة في موقع SO. المفردات الآمنة خيطيًا (Thread-safe Singeton) الإصدار ≥ C++‎ 11 يضمن معيار C++‎ 11 أنّ كائنات نطاق الدوالّ (function scope objects) تُهيَّأ بطريقة متزامنة، ويمكن استخدام هذا لتقديم مفردة آمنة خيطيًا مع تهيئة مُرجأة. class Foo { public: static Foo& instance() { static Foo inst; return inst; } private: Foo() {} Foo(const Foo&) = delete; Foo& operator =(const Foo&) = delete; }; الأصناف الفرعية (Subclasses) انظر المثال التالي: class API { public: static API& instance(); virtual~API() {} virtual const char* func1() = 0; virtual void func2() = 0; protected: API() {} API(const API& ) = delete; API& operator = (const API& ) = delete; }; class WindowsAPI: public API { public: virtual const char* func1() override { /* شيفرة ويندوز */ } virtual void func2() override { /* شيفرة ويندوز */ } }; class LinuxAPI: public API { public: virtual const char* func1() override { /* شيفرة لينكس */ } virtual void func2() override { /* شيفرة لينكس */ } }; API& API::instance() { #if PLATFORM == WIN32 static WindowsAPI instance; #elif PLATFORM = LINUX static LinuxAPI instance; #endif return instance; } المُصرّف في هذا المثال يربط الصنف ‎API‎ بالصنف الفرعي المناسب، من أجل الوصول إلى ‎API‎ حتّى لو لم يكن مربوطًا بشيفرة مخصوصة بمنصّة معينة. تقنيات إعادة التصميم (Refactoring Techniques) يشير مفهوم إعادة البناء (Refactoring) إلى تعديل الشيفرة واستبدال نسخة مُحسّنة بها، ورغم أنّ إعادة البناء تُجرى غالبًا أثناء تغيير الشيفرة بُغية إضافة بعض الميزات أو تصحيح الأخطاء، إلّا أنّ هذا المصطلح مخصوص أساسًا بعمليات تحسين الشيفرة بدون إضافة ميزات أو تصحيح الأخطاء. Goto Cleanup يُستخدم أحيانًا نمط التصميم ‎goto cleanup‎ في شيفرات C++‎ التي بُنِيت على شيفرات مكتوبة بلغة C، ونظرًا لأنّ الأمر ‎goto‎ يصعِّب فهم سير عمل الدوال، فغالبًا ما يُوصى بتجنّبه. ويمكن استبدال تعليمة return أو الحلقات أو الدوال بالأمر ‎goto‎. بالمقابل، يتيح استخدام ‎goto cleanup‎ التخلُّص من منطق التنظيف (cleanup logic). short calculate(VectorStr **data) { short result = FALSE; VectorStr *vec = NULL; if (!data) goto cleanup; //< return false يمكن أن يُستعاض عنها بـ result = TRUE; cleanup: delete[] vec; return result; } في C++‎، يمكنك استخدام تقنية RAII لحلّ هذه المشكلة: struct VectorRAII final { VectorStr *data { nullptr }; VectorRAII() = default; ~VectorRAII() { delete[] data; } VectorRAII(const VectorRAII & ) = delete; }; short calculate(VectorStr **data) { VectorRAII vec {}; if (!data) return FALSE; //< return false يمكن الاستعاضة عنها بـ return TRUE; } بعد هذا، يمكنك الاستمرار في إعادة بناء الشيفرة. مثلًا، عن طريق استبدال ‎VectorRAII‎ بمؤشّر فريد std::unique_ptr أو متّجه std::vector. هذا الدرس جزء من سلسلة دروس عن C++‎. ترجمة -بتصرّف- للفصل Chapter 112: Design pattern implementation in C++‎ والفصل Chapter 113: Singleton Design Pattern من كتاب C++ Notes for Professionals
  16. إعادة التوجيه التامة (Perfect Forwarding) الدوالّ المُنتِجة (Factory functions) لنفترض أنّنا نرغب في كتابة دالّة منتِجة تقبل قائمة عشوائية من الوسائط، ثمّ تمرّر تلك الوسائط دون تعديل إلى دالّة أخرى. إن دالة ‎make_unique‎ هي مثال على مثل هذه الدوال، وتُستخدَم لاستنساخ نسخة جديدة من ‎T‎ بأمان وإعادة مؤشّر فريد ‎unique_ptr<T>‎ يملك تلك النُسخة. وتسمح لنا القواعد المتعلقة بالقوالب المتغيّرة (variadic templates) والمراجع اليمينية بكتابة مثل هذه الدالّة. template < class T, class...A > unique_ptr<T> make_unique(A&&... args) { return unique_ptr<T> (new T(std::forward<A> (args)...)); } يشير استخدام علامات الحذف ‎...‎ إلى حزمة معاملات تمثّل عددًا عشوائيًا من الأنواع، وسينشر المُصرّف تلك الحزمة إلى العدد الصحيح من الوسائط في موضع الاستدعاء، ثم تُمرَّر تلك الوسائط إلى منشئ ‎T‎ باستخدام ‎std::forward‎. ويُطلَب من هذه الدالّة المحافظة على المؤهّلات المرجعية (ref-qualifiers) للوسائط. struct foo { foo() {} foo(const foo&) {} // مُنشئ النسخ foo(foo&&) {} // مُنشئ النسخ foo(int, int, int) {} }; foo f; auto p1 = make_unique<foo> (f); // foo::foo(const foo&) استدعاء auto p2 = make_unique<foo> (std::move(f)); // foo::foo(foo&&) استدعاء auto p3 = make_unique<foo> (1, 2, 3); تقنية مؤشر إلى تنفيذ (Pimpl) الإصدار ≥ C++‎ 11 في ملف الترويسة: // widget.h #include <memory> // std::unique_ptr #include <experimental/propagate_const> class Widget { public: Widget(); ~Widget(); void DoSomething(); private: struct Impl; // تصريح لاحق std::experimental::propagate_const<std::unique_ptr < Impl>> pImpl; }; في ملف التنفيذ: // widget.cpp #include "widget.h" #include "reallycomplextype.h" // widget.h لا حاجة لتضمين هذه الترويسة في struct Widget::Impl { // widget هنا توضع السمات التي نحتاجها من ReallyComplexType rct; }; Widget::Widget(): pImpl(std::make_unique<Impl> ()) {} Widget::~Widget() = default; void Widget::DoSomething() { // pImpl افعل شيئا هنا بـ } تحتوي ‎pImpl‎ على حالة ‎Widget‎ (أو بعضها)، ويمكن تجنّب كشف وصف ‎Widget‎ في الترويسة، وجعله داخل التنفيذ. ‎pImpl‎ هي اختصار لـ "pointer to implementation" (مؤشّر إلى تنفيذ)، أما التنفيذ "الحقيقي" لـ ‎Widget‎ موجود في ‎pImpl‎. تنبيه: لاحظ أنّه لكي يعمل هذا مع مؤشّر حصري (‎unique_ptr‎)، يجب تنفيذ ‎~Widget()‎ في موضع من الملف حيث يكون ‎Impl‎ مرئيًا بالكامل، يمكنك تحديد الإعداد الافتراضي هناك، لكن إن حدّدت الإعداد الافتراضي في موضع لم تكن فيه ‎Impl‎ مُعرّفة، فقد يؤدّي ذلك إلى عطب في البرنامج. التعبيرات المطوية (Fold Expressions) الطي الأحادي (Unary Folds) يُستخدَم الطيّ الأحادي (Unary fold) لطيّ حزم المعامِلات (parameter packs) الخاصّة بعامل (operator) محدّد، وهناك نوعان من معاملات الطيّ الأحادية: الطي الأحادي اليساري - Unary Left Fold -‏ ‎(... op pack)‎، والذي يُوسَّع على النحو التالي: ((Pack1 op Pack2) op...) op PackN *الطي الأحادي اليميني - Unary Right Fold -‏ ‎(pack op ...)‎، والذي يُوسَّع كما يلي: Pack1 op(...(Pack(N - 1) op PackN)) انظر المثال التالي template < typename...Ts > int sum(Ts...args) { return (...+args); // طيّ أحادي يساري //return (args + ...); // طيّ أحادي يمينيّ // associative سيكونان متكافئين إن كان المعامل تجميعيًا // For +, ((1+2)+3) (left fold) == (1+(2+3)) (right fold) // For -, ((1-2)-3) (left fold) != (1-(2-3)) (right fold) } int result = sum(1, 2, 3); // 6 الطيّ الثنائي أو البتي (Binary Fold) الطيّ الثنائي، أو البتّي (Binary Fold) هو طيّ أحادي بالأساس، لكن مع وسيط إضافي. وينقسم إلى نوعين: الطيات البتية اليسارية - Binary Left Fold -‏ ‎(value op ... op pack)‎، والتي تُوسَّع على النحو التالي: (((Value op Pack1) op Pack2) op...) op PackN الطيات البتّية اليمينية (Binary Right Folds)‏ ‎(pack op ... op value)‎، والتي تُوسَّع كما يلي: Pack1 op(...op(Pack(N - 1) op(PackN op Value))) انظر المثال التالي: template < typename...Ts > int removeFrom(int num, Ts...args) { return (num - ...-args); // طية يسرى ثنائية // لاحظ أنّه لا يمكن استخدام عامل طيّ ثنائي يميني // نظرًا لأنّ العامل غير تجميعي } int result = removeFrom(1000, 5, 10, 15); // => 1000 - 5 - 10 - 15 = 970 طيّ الفاصلة (Folding over a comma) قد ترغب أحيانًا في تطبيق دالّة معيّنة على كل عنصر من عناصر حزمة من المُعاملات. وأفضل حلّ لذلك في C++‎ 11 هو: template < class...Ts > void print_all(std::ostream& os, Ts const&... args) { using expander = int[]; (void) expander { 0, (void(os << args), 0)... }; } ويصبح الأمر أسهل مع طي التعبيرات بحيث لا نحتاج إلى الشيفرات المتداولة (boilerplates) المبهمة، انظر: template < class...Ts > void print_all(std::ostream& os, Ts const&... args) { (void(os << args), ...); } ترجمة -بتصرّف- للفصول Chapter 101: Perfect Forwarding و Chapter 107: Pimpl Idiom و Chapter 110: Fold Expressions من كتاب C++ Notes for Professionals
  17. ينصّ المعيار على ضرورة نسخ الكائنات أو نقلها في بعض المواضع من أجل تهيئتها، وإهمال النسخ (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
  18. تمكّن الكلمة المفتاحية ‎auto‎ من الاستنتاج التلقائي لنوع متغيّر معيّن، وهي مناسبة بشكل خاص عند التعامل مع أسماء الأنواع الطويلة: std::map < std::string, std::shared_ptr < Widget > > table; // C++98 std::map < std::string, std::shared_ptr < Widget > > ::iterator i = table.find("42"); // C++11/14/17 auto j = table.find("42"); يمكن استخدامها مع حلقات for النطاقية: vector v = {0, 1, 2, 3, 4, 5}; for (auto n: v) std::cout << n << ' '; أو مع تعبيرات لامدا: auto f = []() { std::cout << "lambda\n"; }; f(); أو يمكن استخدامها لتجنّب تكرار النوع: auto w = std::make_shared < Widget > (); أو لتجنّب عمليّات النسخ غير الضرورية: auto myMap = std::map<int,float>(); myMap.emplace(1,3.14); std::pair<int,float> const& firstPair2 = *myMap.begin(); // ! نسخ auto const& firstPair = *myMap.begin(); // !لا تنسخ سبب إجراء عمليّة النسخ في الشيفرة أعلاه يعود إلى أنّ النوع المُعاد هو ‎std::pair<const int,float>‎! تعبيرات لامدا العامة (C++‎ 14) الإصدار ≥ C++‎ 14 تسمح C++‎ 14 باستخدام الكلمة المفتاحية ‎auto‎ في وسيط لامدا. auto print = [](const auto& arg) { std::cout << arg << std::endl; }; print(42); print("hello world"); ويكافئ تعبير لامدا في الغالب الشيفرة التالية: struct lambda { template < typename T > auto operator ()(const T& arg) const { std::cout << arg << std::endl; } }; ثمّ: lambda print; print(42); print("hello world"); كائنات auto و proxy في بعض الأحيان، قد لا تتصرّف الكلمة المفتاحية ‎auto‎ كما هو متوقّع، فقد تسعى إلى استنتاج نوع التعبير حتى عندما لا يكون استنتاج النوع مطلوبًا، كما هو الحال عند استخدام الكائنات الوكيلة (proxy objects) في الشيفرة: std::vector flags{true, true, false}; auto flag = flags[0]; flags.push_back(true); في الشيفرة أعلاه، ليست ‎flag‎ من النوع ‎bool‎، بل من النوع ‎std::vector<bool>::reference‎، ففي تخصيص النوع ‎bool‎ للقالب ‎vector‎، يعيد المعامل ‎operator []‎ كائنًا وكيلًا مع مُعامل التحويل ‎operator bool‎ المُحدَّد. وعندما يعدّل التابع ‎flags.push_back(true)‎ الحاويةَ، فقد يصبح المرجع مُعلّقًا (dangling)، أي يشير إلى عنصر لم يعد موجودًا. كما أنّه سيجعل الموقف التالي ممكنًا: void foo(bool b); std::vector < bool > getFlags(); auto flag = getFlags()[5]; foo(flag); تُتجَاهل ‎vector‎ على الفور، لذلك ستكون ‎flag‎ مرجعًا زائفًا (pseudo-reference) يشير إلى عنصر تمّ تجاهله. وسيؤدّي استدعاء ‎foo‎ إلى سلوك غير محدّد. يمكنك التصريح في مثل هذه الحالات عن متغيّر باستخدام ‎auto‎، ثمّ تهيئته عبر تحويله إلى النوع الذي تريد استنتاجه: auto flag = static_cast < bool > (getFlags()[5]); لكن قد يكون من الأفضل إحلال ‎bool‎ مكان ‎auto‎. إحدى الحالات الأخرى التي قد تتسبّب الكائنات الوكيلة فيها بمشاكل هي قوالب التعبير (expression templates)، ففي هذه الحالة لا تكون القوالب مُصمّمة لتستمر إلى ما بعد التعبير الكامل الحالي، وذلك لأجل تحسين الكفاءة، لذا فإنّ استخدام الكائن الوكيل في هذه الحالة قد يؤدّي إلى سلوك غير معرَّف. auto و قوالب التعبير يمكن أن تتسبب ‎auto‎ بمشاكل في حال استخدامها مع قوالب التعبير: auto mult(int c) { return c * std::valarray{1}; } auto v = mult(3); std::cout << v[0]; والسبب في ذلك هو أنّ استخدام المعامل ‎operator*‎ على ‎valarray‎ سيمنحك كائنًا وكيلًا يشير إلى ‎valarray‎، وذلك كوسيلة للتقييم المُرجَأ (lazy evaluation). كذلك فإن استخدام ‎auto‎ سيؤدّي إلى إنشِاء مرجع مُعلّق (dangling reference)، وسيُعاد النوع std::valarray<int>‎ بدلًا من ‎mult‎، وتطبع الشيفرة القيمة 3. auto و const والمراجع تمثل الكلمة المفتاحية ‎auto‎ بحدّ ذاتها نوعًا من القيم، مثل ‎int‎ أو ‎char‎، ويمكن تعديلها باستخدام الكلمة المفتاحية ‎const‎ والرمز ‎&‎ لتمثيل نوع ثابت أو نوع مرجعي (reference type) على التوالي، كما يمكن دمج هذه المعدِّلات معًا. تمثل تمثّل ‎s‎ في هذا المثال، نوع قيمة - value type - (سيُستنتَج نوعها بأنّه سلسلة نصّية ‎std::string‎)، ومن ثم فإنّ كل تكرار للحلقة ‎for‎ سَينسخ سلسلة نصية من [المتجهة](رابط الفصل 49) إلى ‎s‎. std::vector < std::string > strings = { "stuff", "things", "misc" }; for (auto s: strings) { std::cout << s << std::endl; } إذا عدَّل جسمُ حلقة التكرار العنصر ‎s‎ (مثلًا عبر استدعاء ‎s.append(" and stuff")‎)، فلن تُعدَّل إلّا هذه النسخة فقط، وليس العضو الأصلي في المتجهة ‎strings‎. من ناحية أخرى، فإن صُرِّح عن السلسلة النصّية ‎s‎ عبر ‎auto&‎، فستكون نوعًا مرجعيًا - reference type - (يُستنتج على أنّه ‎std::string&‎)، لذا، ففي كلّ تكرار للحلقة سيُسند إليها مرجع يشير إلى سلسلة نصية في المتجهة: for(auto& s : strings) { std::cout << s << std::endl; } ستؤثّر التعديلات على ‎s‎ في جسم هذه الحلقة مباشرةً على العنصر الذي تشير إليه من المتجهة ‎strings‎. وأخيرًا، فإن صُرِِّح عن‎s‎ عبر ‎const auto&‎، فستكون نوعًا مرجعيًا ثابتًا (const reference type)، ممّا يعني أنّه في كل تكرار من الحلقة، سيُسنَد إليها مرجع ثابت يشير إلى سلسلة نصّية في المتجهة: for (const auto& s: strings) { std::cout << s << std::endl; } لا يمكن تعديل ‎s‎ (أي لا يمكن استدعاء توابع غير ثابتة عليها) داخل جسم هذه الحلقة. كذلك يُستحسن عند استخدام ‎auto‎ مع حلقات ‎for‎ النطاقية، أن تُستخدم ‎const auto&‎ إذا كان متن الحلقة لن يعدّلَ البنية التي يُجري عليها التكرار، لأنّ هذا سيُجنّبك عمليات النسخ غير الضرورية. نوع الإعادة الزائد Trailing return type تُستخدَم ‎auto‎ في صياغة النوع المُعاد الزائد: auto main() -> int {} تكافئ الشيفرة أعلاه: int main() {} يمكن استعمال ‎auto‎ مع ‎decltype‎ لاستخدام المُعاملات بدلًا من ‎std::declval<T>‎: template <typename T1, typename T2> auto Add(const T1& lhs, const T2& rhs) -> decltype(lhs + rhs) { return lhs + rhs; } هذا الدرس جزء من سلسلة دروس عن C++‎. ترجمة -بتصرّف- للفصل Chapter 108: auto من كتاب C++ Notes for Professionals
  19. دلالات النقل هي وسيلة لنقل كائن إلى آخر في C++‎، عبر إفراغ الكائن القديم، وتبديل محتوياته بمحتويات الكائن الجديد. ولفهم دلالات النقل، من الضروري فهم المقصود بالمرجع اليميني (rvalue reference). وهي، أي المراجع اليمينية (‎T&&‎ حيث T يمثّل نوع الكائن) لا تختلف كثيرًا عن المراجع العادية (‎T&‎، يُطلق عليها الآن مراجع يسارية - lvalue references)، لكنهما تتصرّفان كنوعين مختلفين. ونستطيع إنشاء مُنشئات أو دوال تأخذ أحد النوعين، وهو أمر ضروري عند التعامل مع دلالات النقل. ونحن نحتاج إلى نوعين مختلفين من أجل تحديد سلوكين مختلفين، إذ ترتبط مُنشئات المراجع اليسارية بالنّسخ، في حين ترتبط مُنشئات المراجع اليمينية بالنقل. ولأجل نقل كائن، سنستخدم الدالة ‎std::move(obj)‎ التي تعيد مرجعًا يمينيًا يشير إلى الكائن، ويمكننا استخدام ذلك لنقل بيانات هذا الكائن إلى كائن جديد. هناك عدّة طرق لفعل ذلك وسنناقشها فيما يلي، فكلّ ما عليك تذكّره الآن هو أنّ استخدام ‎std::move‎ يخلق مرجعًا يمينيًا، أي أن تعليمة std::move(obj)‎ لا تغيّر محتوى الكائن obj، أمّا auto obj2 = std::move(obj)‎ فقد تفعل ذلك. استخدام std::move لتخفيض التعقيد من O(n²)‎‎ إلى O(n)‎‎ أدخلت C++‎ 11 دعم نقل الكائنات في اللغة الأساسية وفي المكتبة القياسية، فإذا كان لدينا كائن o مؤقّت وكنّا نريد إنشاء نسخة منطقية (logical copy) منه، فعندئذ قد يكون من الآمن أخذ موارده ببساطة، مثل المخزن المؤقّت المخصّص ديناميكيًا (dynamically allocated buffer) الخاص به، تاركين الكائن o فارغًا منطقيًا (logically empty)، ولكن سيبقى قابلاً للتدمير والنسخ. يشمل دعم اللغة الأساسية (core language) ما يلي: باني نوع المراجع اليمينية (rvalue reference type builder‏) ‎&&‎، على سبيل المثال، ‎std::string&&‎ هو مرجع يميني يشير إلى سلسلة نصية (‎std::string‎)، ممّا يبيّن أنّ هذا المرجع مؤقّت، ويمكن نقل موارده. دعم خاص لمنشئ النقل - move constructor‏ - ‎T( T&& )‎، الذي يفترض أن ينقل الموارد بكفاءة من الكائن المُحدّد الآخر، بدلاً من نسخ تلك الموارد. دعم خاص لعامل إسناد النقل (move assignment operator‏) ‎auto operator=(T&&) -> T&‎، والذي يفترض أيضًا أن يجري عمليّة النقل من المصدر. يتركّز الدعم الذي تقدمه المكتبة القياسية بالأساس في قالب الدالّة ‎std::move‎ من الترويسة ، وتنتج هذه الدالّة مرجعًا يمينيًا يشير إلى الكائن المُحدّد، وذلك يبيّن أنّه من الممكن النقل منه كما لو كان مؤقّتًا. تعقيد (complexity) عمليّة نسخ حاوية يساوي عادةً O(n)‎‎، حيث يمثّل n عدد عناصر الحاوية، أمّا النقل فتَعقيده يساوي O(1)‎‎، أي أنّ وقته ثابت. وبالنسبة لخوارزمية تنسخ تلك الحاوية n مرّة منطقيًا، فيمكن لذلك أن يقلّل التعقيد من القيمة O (n²)‎‎ (غير العملية)، إلى التعقيد الخطي O(n)‎‎. قدّم أندرو كوينج (Andrew Koenig) في مقالته “Containers That Never Change” مثالًا مثيرًا للاهتمام عن عدم كفاءة الخوارزمية عند استخدام نمط برمجة خاص تكون فيه المتغيّرات غير قابلة للتغيير بعد تهيئتها، ففي مثل هذا النمط البرمجي، تُنفَّذ الحلقات (loops) بشكل عام باستخدام التكرارية، وبالنسبة لبعض الخوارزميات، مثل خوارزمية إنشاء تسلسل Collatz‏ (Collatz sequence‏)‏، يتطلب التكرار نسخ الحاوية بشكل منطقي. انظر المثال التالي المبني على مثال كوينج في الرابط أعلاه: namespace my { template < class Item> using Vector_ = /*E.g. std::vector<Item> */ ; auto concat( Vector_<int> const& v, int const x ) -> Vector_<int> { auto result { v }; result.push_back(x); return result; } auto collatz_aux( int const n, Vector_<int> const& result ) -> Vector_<int> { if (n == 1) { return result; } auto const new_result = concat(result, n); if (n % 2 == 0) { return collatz_aux(n / 2, new_result); } else { return collatz_aux(3 *n + 1, new_result); } } auto collatz( int const n ) -> Vector_<int> { assert(n != 0); return collatz_aux(n, Vector_<int> ()); } please split, thi } // my فضاء الاسم #include <iostream> using namespace std; auto main()->int { for (int const x: my::collatz(42)) { cout << x << ' '; } cout << '\n'; } الخرج: 42 21 64 32 16 8 4 2 عدد عمليات نسخ العناصر الناجمة عن نسخ المتجهات يقارب O (n ²)‎ هنا، لأنّه يساوي المجموع 1 + 2 + 3 + … n. باستخدام المٌصرّفين g++‎ و Visual C++‎، سيُنتج الاستدعاء الوارد أعلاه للتعبير ‎collatz(42)‎ تسلسل Collatz مؤلفًا من 8 عناصر، وسيَنطوي على 36 عملية نسخ للعناصر (‎‎8 * 7/2 = 28، بالإضافة إلى بعض العمليات) في استدعاءات مُنشئ نسخ المتجه. يمكن تجنّب كل عمليات نسخ العناصر تلك عن طريق نقل المتجهات التي لم تعد هناك حاجة لقيَمِها. وكي نفعل هذا فمن الضروري إزالة ‎const‎ والرجوع إلى وسائط نوع المتجه، وتمرير المتجهات بالقيمة (by value). تحسَّن القيم المُعادة من الدالة تلقائيا، أما بالنسبة للاستدعاءات التي تُمرّر فيها المتجهات ثمّ لا تُستخدم مرّة أخرى في الدالّة، فما عليك سوى تطبيق ‎std::move‎ لنقل تلك المخازن المؤقّتة (buffers) بدلاً من نسخها: using std::move; auto concat( Vector_<int> v, int const x )-> Vector_<int> { v.push_back(x); تنبيه: نقل كائن محلي في تعليمة return يمنع ترك النَّسخ، انظر الرابط التالي من SO، نتابع المثال … // return move(v); return v; } auto collatz_aux( int const n, Vector_<int> result ) -> Vector_<int> { if (n == 1) { return result; } auto new_result = concat(move(result), n); struct result; // بعد الآن `result` التحقق من عدم استخدام if (n % 2 == 0) { return collatz_aux(n / 2, move(new_result)); } else { return collatz_aux(3 *n + 1, move(new_result)); } } auto collatz(int const n) - > Vector_ < int> { assert(n != 0); return collatz_aux(n, Vector_<int> ()); } هنا، وباستخدام المصرّفين g++‎ و Visual C++‎، فإنّ عدد عمليات نسخ العناصر الناجمة عن استدعاءات مُنشئ نسخ المتجهات يساوي 0. ما زال تعقيد الخوارزمية يساوي O (n)‎‎، بيْد أنّه أفضل بكثير من O (n ²)‎‎. ومع بعض الدعم من لغة C++‎، يمكننا أن نستخدم النقل ونفرض جمود المتغيّر (immutability) منذ لحظة تهيتئه وحتّى النقل النهائي، بعد ذلك، فأيّ محاولة لاستخدام هذا المُتغيّر ستُعدّ خطأ. بأي حال فإن ++C لا تدعم هذا بدءًا من الإصدار C++‎ 14. يمكن فرض قاعدة عدم الاستخدام بعد النقل بالنسبة للشيفرات الخالية من الحلقات (loop-free code)، عبر إعادة التصريح عن الاسم باعتباره بنية (‎struct‎) غير مكتملة، كما هو الحال في ‎struct result;‎ أعلاه، لكن هذا المنظور سيئ ويستبعد أن يفهمها المبرمجون الآخرون؛ أيضًا، قد يكون التشخيص مربكًا. باختصار، أتاح دعم لغة C++‎ والمكتبة القياسية للنقل تحسينات جذرية على تعقيد الخوارزمية، ولكن بسبب عدم اكتمال الدعم، فإنّ تلك الفائدة ستكون على حساب التخلي عن ضمانات صحّة ووضوح الشيفرة التي يمكن أن توفّرها ‎const‎. كتتمّة للمثال السابق، سنستخدَم صنف المتجه لقياس عدد عمليات نسخ العناصر الناجمة عن استدعاءات منشئ النسخ: template < class Item> class Copy_tracking_vector { private: static auto n_copy_ops() -> int&{ static int value; return value; } vector<Item> items_; public: static auto n() -> int { return n_copy_ops(); } void push_back( Item const& o ) { items_.push_back( o ); } auto begin() const { return items_.begin(); } auto end() const { return items_.end(); } Copy_tracking_vector(){} Copy_tracking_vector( Copy_tracking_vector const& other ) : items_( other.items_ ) { n_copy_ops() += items_.size(); } Copy_tracking_vector( Copy_tracking_vector&& other ) : items_( move( other.items_ ) ) {} }; منشئ النقل (Move constructor) لنقل أن لدينا الشيفرة التالية: class A { public: int a; int b; A(const A &other) { this->a = other.a; this->b = other.b; } }; الطريقة العادية لإنشاء منشئ نسخ هي باختيار الصيغة الموضحة أعلاه، وسيكون لدينا مُنشئ للصنف A يأخُذ مرجعًا إلى كائن آخر من النوع A، وينسخ الكائن يدويا داخل التابع. وبدلًا من ذلك، يمكننا كتابة ‎A(const A &) = default;‎، التي تنسخ تلقائيًا جميع الأعضاء باستخدام مُنشئ النسخ الخاص بها. سنأخذ مرجعًا يمينيًا بدلًا من اليساري، من أجل إنشاء مُنشئ نقل (move constructor)، كما هو مُوضّح هنا. class Wallet { public: int nrOfDollars; Wallet() = default; //default ctor Wallet(Wallet &&other) { this->nrOfDollars = other.nrOfDollars; other.nrOfDollars = 0; } }; يرجى ملاحظة أنّنا ضبطنا القيم القديمة عند القيمة ‎zero‎، ينسخ مُنشئ النقل الافتراضي (‎Wallet(Wallet&&) = default;‎) قيمة ‎nrOfDollars‎، لأنّه نوع بيانات قديم (Plain Old Data أو POD). وبما أن دلالات النقل مُصمّمة للسماح بسرقة الحالة من النُسخة الأصلية، فالسؤال الآن هو كيف ستظهر النسخة الأصلية بعد تلك "السرقة"؟ Wallet a; a.nrOfDollars = 1; Wallet b(std::move(a)); // B(B&& other); استدعاء std::cout << a.nrOfDollars << std::endl; //0 std::cout << b.nrOfDollars << std::endl; //1 وهكذا نكون قد أنشأنا كائنًا بشكل نقليّ (move constructed) من كائن آخر قديم. ربما يكون المثال السابق بسيطًا إلّا أنّه يوضّح آلية عمل مُنشئات النقل، وتظهر فائدته في الحالات الأكثر تعقيدًا، كما يحدث في حالات إدارة الموارد. انظر الشيفرة التالية التي تدير عمليات تتضمن نوعًا بعينه، وتملك مساعِدًا على الكومة (heap) وآخر في ذاكرتها، في المكدَّس غالبًا. وكلا المساعدان DefaultConstructible و CopyConstructible و MoveConstructible: template < typename T, template<typename> typename HeapHelper, template<typename> typename StackHelper> class OperationsManager { using MyType = OperationsManager<T, HeapHelper, StackHelper> ; HeapHelper<T> *h_helper; StackHelper<T> s_helper; // ... public: // Five لـ &Rule المنشئ الافتراضي OperationsManager(): h_helper(new HeapHelper < T>) {} OperationsManager(const MyType& other): h_helper(new HeapHelper<T> (*other.h_helper)), s_helper(other.s_helper) {} MyType& operator=(MyType copy) { swap(*this, copy); return * this; }~OperationsManager() { if (h_helper) { delete h_helper; } } منشئ نقل بدون ()swap، يأخذ *<HeapHelper<T و <StackHelper <T من other من خلال فرض استخدام منشئ النقل الخاص بـ <StackHelper <T. ويٌحِلّ nullptr مكان *<HeapHelper<T الخاص بـ other، لمنع الأخير من حذف مساعِدنا الجديد حين يُدمَّر، نتابع المثال … OperationsManager(MyType&& other) noexcept: h_helper(other.h_helper), s_helper(std::move(other.s_helper)) { other.h_helper = nullptr; } منشئ نقل (مع ()swap)، نضع أعضاءنا في الشرط الذي نريد أن يكون فيه other ثم نبادل الأعضاء معه، نتابع … // OperationsManager(MyType&& other) noexcept : h_helper(nullptr) { // swap(*this, other); // } // Copy/move helper. friend void swap(MyType &left, MyType &right) noexcept { std::swap(left.h_helper, right.h_helper); std::swap(left.s_helper, right.s_helper); } }; إعادة استخدام كائن منقول يمكنك إعادة استخدام كائن منقول على النحو التالي: void consumingFunction(std::vector<int> vec) { // بعض العمليات } int main() { // 1, 2, 3, 4 تهيئة المتجه بالقيم std::vector<int> vec { 1, 2, 3, 4 }; // by move إرسال المتجه بالنقل consumingFunction(std::move(vec)); // في حالة غير محدّدة vec الكائن // نظرا لأنّ الكائن لم يُدمَّر، يمكننا أن نسند إليه محتوى جديدا // في هذه الحالة، سنسند قيمة فارغة إلى المتجه ما يجعله فارغًا vec = {}; // نظرًا لأنّ المتجه قد اكتسب قيمة محدّدة، فيمكننا استخدامه بشكل طبيعي vec.push_back(42); // إرسال المتجه عبر النقل مجددا consumingFunction(std::move(vec)); } إسناد النقل (Move assignment) على غرار إسناد قيمة لكائن باستخدام مرجع يساري، ثمّ نسخه، يمكننا أيضًا نقل القيم من كائن إلى آخر دون إنشاء كائن جديد، إذ نستدعي إسناد النقل، ثمّ ننقل القيم من كائن إلى كائن آخر موجود. سيتعيّن علينا أن نزيد تحميل العامل ‎operator =‎ لنجعله يأخذ مرجعًا يمينيًا. class A { int a; A& operator=(A&& other) { this->a = other.a; other.a = 0; return * this; } }; هذه هي الصيغة النموذجية لتعريف إسناد النقل، إذ نزيد تحميل‎operator =‎ حتى نتمكن من تزويده بمرجع يميني، وإسناده إلى كائن آخر. A a; a.a = 1; A b; b = std::move(a); // A& operator= (A&& other) استدعاء std::cout << a.a << std::endl; //0 std::cout << b.a << std::endl; //1 ومن ثم يمكننا إسناد كائن نقليًا (move assign) إلى كائن لآخر. استخدام دلالات النقل على الحاويات يمكنك نقل حاوية بدلاً من نسخها على النحو التالي: void print(const std::vector<int>& vec) { for (auto&& val: vec) { std::cout << val << ", "; } std::cout << std::endl; } int main() { // 1, 2, 3, 4 بالقيم vec1 تهيئة المتجه // كمتجه فارغ vec2 ثمّ تهيئة std::vector<int> vec1 { 1, 2, 3, 4 }; std::vector<int> vec2; // 1, 2, 3, 4 السطر التالي سيطبع print(vec1); // السطر التالي سيطبع سطرا جديدا print(vec2); // عبر إسناد النقل vec2 أُسنِدت قيمة المتجه // دون نسخها vec1 هذا سيسرق قيمة vec2 = std::move(vec1); // هنا في حالة غير محدّدة، لكن يبقى صالحًا vec1 المتجه // لم يُدمَّر بعد، لكن لا يوجد ضمان بخصوص الكائنات التي يحتويها vec1 الكائن // 1, 2, 3, 4 السطر التالي سيطبع print(vec2); } هذا الدرس جزء من سلسلة دروس عن C++‎. ترجمة -بتصرّف- للفصل Chapter 106: Move Semantics من كتاب C++ Notes for Professionals
  20. ما المقصود بالسلوك غير المعرَّف (undefined behavior أو UB)؟ وفقًا لمعيار ISO C++‎ (الفقرة 1.3.24، N4296)، فهو: هذا يعني أنّه عندما يواجه البرنامج سلوكًا غير معرَّف، فإنّه يُسمَح له بفعل ما يريد. هذا قد يعني غالبًا التوقف عنا العمل، ولكنّه قد يعني تجاهله وعدم فعل أيّ شيء، أو قد يعمل البرنامج بشكل صحيح!، ولا شك أن عليك تجنب كتابة شيفرات تؤدي إلى سلوكيات غير معرَّفة. أما السلوك غير المُحدَّد فهو أيّ سلوك قد يتغير من جهاز إلى آخر، أو من مصرّف إلى آخر. لا يمكن التنبؤ الدقيق بسلوكيات البرامج التي تحتوي سلوكات غير محدّدة في مرحلة الاختبار، لأنّها تتعلق بالتقديم (implementation). لذا يُفضّل عموما تجنّب هذا النوع من السلوكات قدر الإمكان. القراءة أو الكتابة من مؤشّر فارغ int *ptr = nullptr; *ptr = 1; // سلوك غير معرَّف هذا سلوك غير معرَّف لأنّ المؤشّر الفارغ لا يشير إلى أي كائن صالح، لذلك لا يوجد كائن في ‎*ptr‎ للكتابة عليه. ورغم أنّ هذا قد يتسبب غالبًا في حدوث خطأ في التجزئة (segmentation fault)، إلا أنّه غير مُعرَّف، وقد يحدث أي شيء. استخدام متغيّر محلي غير مهيّأ int a; std::cout << a; // سلوك غير معرَّف ينتج عن هذا سلوك غير معرَّف لأنّ ‎a‎ غير مهيّأة. يدّعي البعض (خطأً) أنّ هذا ناتج عن أنّ القيمة "غير معرَّفة "، أو بسبب "القيمة التي كانت سابقًا في ذلك الموقع من الذاكرة". والصحيح أنّ محاولة الوصول إلى قيمة ‎a‎ في المثال أعلاه هي التي أدّت إلى سلوك غير معرَّف. ومحاولة طباعة "قيمة مُهملة" (garbage value) هي عرض شائع في هذه الحالة لكن هذا ليس سوى شكل واحد ممكن من السلوكات غير المعرَّفة. الإصدار ≥ C++‎ 14 استخدام قيمة غير معرَّفة من نوع ‎unsigned char‎ لا ينتج عنه سلوك غير معرَّف عند استخدام القيمة كـ: *معامل ثاني أو ثالث للمعامل الشرطي الثلاثي (ternary conditional operator). معامَل أيمن لمُعامل الفاصلة المُضمّن (comma operator). معامَل التحويل إلى ‎unsigned char‎. معامَل أيمن لمُعامل الإسناد إذا كان المعامل الأيسر من النوع ‎unsigned char‎. مُهيئ لكائن ‎unsigned char‎؛ أو حتى عند تجاهل القيمة. ففي مثل هذه الحالات، تنتشر (propagates) القيمة غير المعرَّفة ببساطة إلى نتيجة التعبير إن أمكن. لاحظ أنّ المتغيّرات الساكنة (‎static‎) تُهيّأ دائمًا عند القيمة صفر (إن أمكن): static int a; std::cout << a; // تساوي 0 'a' سلوك معرَّف، و محاولة الوصول إلى فهرس خارج النطاق محاولة الوصول إلى فهرس خارج نطاق مصفوفة (أو حاوية من المكتبة القياسية، إذ أنّها جميعًا منفَّذة باستخدام مصفوفات خام) تؤدّي إلى سلوك غير معرَّف: int array[] = { 1, 2, 3, 4, 5 }; array[5] = 0; // سلوك غير معرَّف لكن يُسمح بالحصول على مؤشّر يشير إلى نهاية المصفوفة (أي ‎array + 5‎ في هذه الحالة)، بيْد أنّه يُحظر تحصيله (dereference)، لأنّه ليس عنصرًا صالحًا. const int *end = array + 5; // مؤشر إلى الموضع الذي يلي العنصر الأخير for (int *p = array; p != end; ++p) // `p` افعل شيئا ما بـ بشكل عام، لا يُسمح لك بإنشاء مؤشّر خارج الحدود، إذ يجب أن يشير المؤشّر إلى عنصر داخل المصفوفة، أو إلى الموضع الذي يلي نهاية المصفوفة. حذف كائن مشتق عبر مؤشّر يشير إلى صنف أساسي لا يتوفّر على حاذِف وهمي class base {}; class derived: public base {}; int main() { base* p = new derived(); delete p; // سلوك غير معرَّف } تشير الفقرة ‎‎5.3.5 / 3‎‎ من المعيار إلى أنّه إذا استُدعِيت ‎delete‎ على كائن لا يحتوي نوعُه الساكن (static type) على مُدمّر وهمي (‎virtual‎)، فإنّه: هذا يبقى صحيحًا بغض النظر عمّا إذا كان الصنف المشتق قد أضاف أو لم يضف أيّ حقول إلى الصنف الأساسي. توسيع فضاءي الاسم "std" أو "posix" يحظر المعيار (‎‎17.6.4.2.1 / 1) عمومًا توسيع فضاء الاسم ‎std‎: وينطبق نفس الشيء على‎‎‎posix‎‏ (‎‎17.6.4.2.2 / 1): #include <algorithm> namespace std { int foo() {} } لا شيء في المعيار يحظر على ‎algorithm‎ (أو أيٍّ من الترويسات التي يتضمّنها) إعادة تعريف نفس التعريف، وهذا قد يؤدّي إلى انتهاك قاعدة التعريف الواحد (One Definition Rule). ولذلك فهو ممنوع بشكل عام لكن مع بعض الاستثناءات، فمثلًا يُسمح بإضافة تخصيصات للأنواع المُعرَّفة من المستخدمين. في المثال التالي، لنفترض أنّ الشيفرة تحتوي على: class foo { // شيفرة هنا }; الشيفرة التالية صالحة: namespace std { template < > struct hash < foo> { public: size_t operator()(const foo &f) const; }; } حسابيات غير صالحة على المؤشّرات Invalid pointer arithmetic ستتسبّب الاستخدامات التالية لحسابيات المؤشّر في حدوث سلوك غير معرَّف: إضافة أو طرح عدد صحيح إذا لم تنتمي النتيجة إلى نفس المصفوفة التي يشير إليها المؤشّر. (هنا، يُعدّ العنصر الموجود بعد النهاية جزءًا من المصفوفة.) int a[10]; int* p1 = &a[5]; int* p2 = p1 + 4; // a[9] يشير إلى p2 جيد، لأنّ int* p3 = p1 + 5; // a يشير إلى الموضع الذي يلي p2 جيد، لأنّ int* p4 = p1 + 6; // سلوك غير معرَّف int* p5 = p1 - 5; // a[0] يشير إلى p2 جيد، لأنّ int* p6 = p1 - 6; // سلوك غير معرَّف int* p7 = p3 - 5; // a[5] يشير إلى p7 جيد، لأنّ طرح (subtraction) مؤشّرين إذا لم ينتمي كلاهما إلى نفس المصفوفة، مرّة أخرى، يعدّ العنصر الذي يلي العنصر الأخير من المصفوفة جزءًا من المصفوفة، والاستثناء الوحيد هو أنّه يمكن طرح مؤشّرين فارغين، والنتيجة ستكون 0. int a[10]; int b[10]; int *p1 = &a[8], *p2 = &a[3]; int d1 = p1 - p2; // 5 int *p3 = p1 + 2; // a يشير إلى الموضع الذي يلي العنصر الأخير في p2 جيد، لأنّ int d2 = p3 - p2; // 7 int *p4 = &b[0]; int d3 = p4 - p1; // سلوك غير معرَّف طرح مؤشّرين إذا كانت النتيجة تطفح (overflow) عن ‎std::ptrdiff_t‎. أيّ عملية حسابية على المؤشّرات لا يتطابق فيها نوع أحد المعامَليْن (operand) مع النوعَ الديناميكي للكائن المُشار إليه من قبل ذلك المؤشّر (تجاهل التأهيل - cv-qualification). فوفقًا للمعيار فإنه: struct Base { int x; }; struct Derived : Base { int y; }; Derived a[10]; Base* p1 = &a[1]; // جيد Base* p2 = p1 + 1; // Derived يشير إلى p1 سلوك غير معرَّف، لأنّ Base* p3 = p1 - 1; // نفس الشيء Base* p4 = &a[2]; // سلوك غير معرَّف auto p5 = p4 - p1; // Derived يشيران إلى p4 و p1 سلوك غير معرَّف، لأنّ const Derived* p6 = &a[1]; const Derived* p7 = p6 + 1; // لا تهمّ cv-qualifiers جيد، لأن المؤهّلات عدم وجود تعليمة return في دالّة نوعها المُعاد يخالف void سيؤدّي حذف تعليمة ‎return‎ في دالّة نوعها المُعاد غير فارغ (‎void‎) إلى سلوك غير معرَّف. int function() { // return غياب تعليمة } int main() { function(); // سلوك غير معرَّف } تطرح معظم المٌصرّفات الحديثة تحذيرًا في وقت التصريف إذا صادَفَت مثل هذه السلوكيات غير المعرَّفة. ملاحظة: ‎main‎ هي الاستثناء الوحيد لهذه القاعدة. إذا لم تحتو ‎main‎ على تعليمة ‎return‎، فسيُدرِج المصرّف تلقائيًا التعبير ‎return 0;‎ نيابة عنك، لذلك يمكنك عدم وضعها إن شئت. الوصول إلى مرجع معلّق (Accessing a dangling reference) لا يمكن الوصول إلى مرجع يشير إلى كائن خرج عن النطاق أو تمّ تدميره، ويقال أن هذا المرجع "مُعلّق" (dangling)، لأنّه لم يُعد يشير إلى كائن صالح. #include <iostream> int& getX() { int x = 42; return x; } int main() { int& r = getX(); std::cout << r << "\n"; } في هذا المثال، يخرج المتغيّر المحلي ‎x‎ عن النطاق عند إعادة ‎getX‎. لاحظ أنّ تمديد دورة الحياة (lifetime extension) لا يمكنه أن يطيل دورة الحياة الخاصة بالمتغيّر المحلي بعد الخروج من نطاق الكتلة التي عُرِّف فيها. لذلك فإن المرجع ‎r‎ أصبح معلقًا. هذا البرنامج له سلوك غير معرَّف رغم أنّه قد يبدو أنّه يعمل بدون مشاكل، بل إنّه قد يطبع ‎42‎ في بعض الحالات. قسمة عدد صحيح على الصفر int x = 5 / 0; // سلوك غير معرَّف القسمة على ‎0‎ هي عمليّة غير معرَّفة في الرياضيات، فلا جرم أنّها تؤدّي إلى سلوك غير معرَّف في البرمجة. في المثال التالي، x تساوي موجب ما لا نهاية infinity+. float x = 5.0f / 0.0f; تعتمد معظم التنفيذات (implementaions) على المعيار IEEE-754، الذي ينصّ على أنّ قسمة عدد عشري (floating point) على الصفر ستعيد القيمة الخاصة "ليس عددًا" ‎NaN‎ (إذا كان البسْط يساوي ‎0.0f‎)، أو ‎infinity‎ (إذا كان البسط موجبًا)، أو ‎-infinity‎ (إذا كان البسط سالباً). الإزاحة بعدد غير صالح من المنازل بالنسبة لعامل الإزاحة (shift operator) المُضمّن، يجب أن يكون العامل الأيمن غير سالب وأصغر من طول العامل الأيسر بالبتّات. خلاف ذلك، فإنّ السلوك سيكون غير معرَّف. const int a = 42; const int b = a << -1; // سلوك غير معرَّف const int c = a << 0; // ok في السطرين التاليين، ينتج لنا سلوك غير معرَّف إذا كان طول int يساوي 32 أو أقل … const int d = a << 32; const int e = a >> 32; في حالة const int g = f << 10، هذا مقبول حتى لو كان طول signed char أقل من أو يساوي 10 بت، ويجب ألا يقل طول int عن 16 بت … const signed char f = 'x'; const int g = f << 10; تخصيص الذاكرة وتحريرها بشكل غير صحيح لا يمكن تحرير (deallocated) كائن عبر ‎delete‎ إلا إن كان قد خُصِّص من قبل عبر ‎new‎ ولم يكن مصفوفةً. أما إذا لم يكن الوسيط المُمرّر إلى ‎delete‎ مُعادًا من ‎new‎، أو لم يكن مصفوفةً، فسيكون السلوك غير معرَّف. لا يمكن تحرير كائن عبر ‎delete[]‎ إلا إن كان قد خُصِّص من قبل عبر ‎new‎ ولم يكن مصفوفةً. أما إذا لم يكن الوسيط المُمرّر إلى ‎delete[]‎ مُعادًا من ‎new‎، أو لم يكن مصفوفةً، فسيكون السلوك غير معرَّف. إذا لم يكن الوسيط المُمرّر إلى ‎free‎ مُعادًا من ‎malloc‎، فسيكون السلوك غير معرَّف. int* p1 = new int; delete p1; // صحيح // delete[] p1; // غير معرَّف // free(p1); // غير معرَّف int* p2 = new int[10]; delete[] p2; // صحيح // delete p2; // غير معرَّف // free(p2); // غير معرَّف int* p3 = static_cast<int*>(malloc(sizeof(int))); free(p3); // صحيح // delete p3; // غير معرَّف // delete[] p3; // غير معرَّف يمكن تجنّب هذه المشاكل عن طريق تجنّب ‎malloc‎ و ‎free‎ في برامج C++‎، فمن الأفضل استخدام المؤشّرات الذكية بدلًا من ‎new‎ و ‎delete‎ الخام، كما يُفضَّل استخدام ‎std::vector‎ و ‎std::string‎ على ‎new‎ و ‎delete[]‎ طفح الأعداد الصحيحة المُؤشّرة (Signed Integer Overflow) انظر المثال التالي: int x = INT_MAX + 1; // سلوك غير معرَّف هذا أحد أسوأ حالات السلوكيات غير المعرَّفة لأنّها تنتج سلوكًا غير معرَّف متكرّر ولا يوقف البرنامج، لذا قد لا ينتبه إليه المطوّرون. من ناحية أخرى: unsigned int x = UINT_MAX + 1; // تساوي 0 x يؤدي هذا إلى سلوك معرَّف جيدًا بما أنه: (C++‎ 11 الفقرة ‎‎3.9.1 / 4 من المعيار) أحيانًا قد تستغل المٌصرّفات السلوكات غير المعرَّفة وتحسّنها. signed int x; if (x > x + 1) { // افعل شيئا ما } وطالما أنّ طفح الأعداد الصحيحة المُؤشّرة (signed integer overflow) يؤدي إلى سلوك غير معرَّف، فيمكن للمُصرّف تجاهله وافتراض أنّه لن يحدث أبدا، ومن ثم يمكنه استبعاد كتلة if. التعريفات المتعدّدة غير المتطابقة (قاعدة التعريف الواحد) إذا كان أي مما يلي معرَّفًا في عدة واحدات ترجمة فيجب أن تكون جميع التعريفات متطابقة أو سيكون السلوك غير معرَّف وفقًا لقاعدة التعريف الواحد (ODR): الأصناف، التعدادات، الدوال المضمنَّة، القوالب، الأعضاء في القوالب. foo.h‎: class Foo { public: double x; private: int y; }; Foo get_foo(); ‎‎foo.cpp: #include "foo.h" Foo get_foo() { /*التنفيذ*/ } main.cpp: // Foo أريد الوصول إلى العضو الخاص، لذا سأستخدم النوع الخاص بي بدلًا من class Foo { public: double x; int y; }; Foo get_foo(); // foo.h سنصرّح عن هذه الدالة بأنفسنا لأننا لم نُضمَِن int main() { Foo foo = get_foo(); // foo.y افعل شيئا ما بـ } يتسبّب البرنامج أعلاه في سلوك غير معرَّف لأنّه يحتوي على تعريفين غير متطابقين للصنف ‎::Foo‎، والذي له صلة خارجية (external linkage) في وحدات ترجمة مختلفة. وعلى عكس إعادة تعريف صنف داخل وحدة الترجمة نفسها، فإن المٌصرّف ليس مُلزمًا بتشخيص هذه المشكلة. محاولة تعديل كائن ثابت (Modifying a const object) إنّ أيّ محاولة لتعديل كائن ثابت ‎const‎ سينتج عنها سلوك غير معرَّف، هذا ينطبق على المتغيّرات الثابتة وأعضاء الكائنات الثابتة، وأعضاء الأصناف المُصرّح عنها عبر ‎const‎. بالمقابل، فإنّ عضوا متغيّرًا ‎mutable‎ من كائن ثابت ‎const‎ لن يكون ثابتا، وقد تحدث هذه المحاولة عبر ‎const_cast‎: const int x = 123; const_cast<int&>(x) = 456; std::cout << x << '\n'; عادة ما يُضمِّن المٌصرّف قيمة الكائن ‎const int‎، وعليه فيحتمل أن تُصرَّف هذه الشيفرة وتطبع ‎123‎، كذلك يمكن للمٌصرّفات وضع قيم الكائنات الثابتة في ذاكرة مخصّصة للقراءة فقط، ومن ثم قد يحدث خطأ في التجزئة (segmentation fault). وسيكون السلوك بشكل عام غير معرَّف، ولا يمكن التنبؤ بما سيفعله البرنامج. ينطوي البرنامج التالي على خطأ خفِىّ: #include <iostream> class Foo* instance; class Foo { public: int get_x() const { return m_x; } void set_x(int x) { m_x = x; } private: Foo(int x, Foo* &this_ref): m_x(x) { this_ref = this; } int m_x; friend const Foo &getFoo(); }; const Foo &getFoo() { static const Foo foo(123, instance); return foo; } void do_evil(int x) { instance->set_x(x); } int main() { const Foo &foo = getFoo(); do_evil(456); std::cout << foo.get_x() << '\n'; } في الشيفرة السابقة، أنشأت دالة ‎getFoo‎ مفردة (singleton) من نوع ‎const Foo‎، وتمت تهيئة عضوها ‎m_x‎ عند القيمة ‎123‎. ثم استُدعِيت ‎do_evil‎ يبدو أنّ هذا غيّر قيمة ‎foo.m_x‎ إلى 456. فأين مكمن الخطأ؟ لا تفعل ‎do_evil‎ أيّ شيء شرّير رغم اسمها، فكلّ ما تفعله هو استدعاء ضابط (setter) من خلال ‎Foo*‎، لكنّ هذا المؤشّر يشير إلى كائن ‎const Foo‎ رغم عدم استخدام ‎const_cast‎، وقد تمّ الحصول على هذا المؤشّر عبر مُنشئ ‎Foo‎. لا يصبح كائن ثابت ثابتًا (‎const‎) حتى تكتمل عمليّة التهيئة، لذلك فإنّ نوع المؤشّر ‎this‎ يساوي ‎Foo*‎، وليس const Foo*‎ داخل المنشئ، وبسبب ذلك يحدث سلوك غير معرَّف، رغم عدم وجود أيّ إنشاءات خطيرة في هذا البرنامج. محاولة إعادة قيمة من دالة لا تعيد قيمة الإصدار ≥ C++‎ 11 هذا مثال من المعيار [dcl.attr.noreturn]: [[noreturn]] void f() { throw "error"; // حسنا } [ [noreturn] ] void q(int i) { // السلوك غير معرَّف في حال كان الوسيط أصغر من أو يساوي 0 if (i > 0) throw "positive"; } تكرار لا نهائي للقالب هذا مثال من المعيار ‎‎[temp.inst] / 17: يتطلب التوليد الضمني لـ <X<T استنساخًا ضمنيًا والذي يتطلب بدوره استنساخًا ضمنيًا لـ <*X<T والذي يتطلب بدوره استنساخًا ضمنيًا لـ <**X<T وهكذا … template < class T > class X { X<T> *p; // OK X<T*> a; }; التدفق الزائد الناتج عن التحويل من وإلى عدد عشري عند تحويل: نوع عددي صحيح إلى نوع عدد عشري. أو نوع عدد عشري إلى نوع عددي صحيح. أو نوع نوع عدد عشري إلى نوع عدد عشري أقصر. فسيحدث سلوك غير معرَّف إن كانت القيمة المصدرية (source value) خارج نطاق القيم التي يمكن تمثيلها في النوع المقصود، انظر المثال التالي حيث لا يمكن لـ int تخزين عناصر كبيرة إلى هذا الحد، لذا يحدث سلوك غير معرَّف. double x = 1e100; int y = x; تعديل سلسلة نصّية مجردة الإصدار < C++‎ 11 ‎"hello world"‎ في المثال أدناه هي سلسلة نصّية مجردة، لذا فإنّ محاولة تعديلها ستؤدّي إلى سلوك غير معرَّف. char *str = "hello world"; str[0] = 'H'; طريقة تهيئة ‎str‎ في المثال أعلاه أصبحت مهملة رسميًا في C++‎ 03 (من المقرر إزالتها من الإصدارات المستقبلية للمعيار)، قد تصدر بعض المٌصرّفات القديمة (قبل عام 2003) تحذيرًا بشأن ذلك، وأصبحت المٌصرّفات تحذّر عادة من أنّ هناك تحويلات متجاوَزة بعد عام 2003. الإصدار ≥ C++‎ 11 المثال أعلاه غير جائز ويؤدّي إلى إطلاق عملية تشخيص (diagnostic) من قبل المصرّف في الإصدار C++‎ 11 والإصدارات الأحدث. يمكن إنشَاء مثال لتوضيح السلوك غير المعرَّف من خلال السماح بتحويل النوع بشكل صريح على النحو التالي: char *str = const_cast<char*> ("hello world"); str[0] = 'H'; الوصول إلى كائن بافتراض أنّه من النوع الخاطئ لا تجوز محاولة الوصول إلى كائن من نوع معيّن كما لو كان من نوع آخر مختلف في معظم الحالات -متجاهلين المؤهِّلات cv- qualifiers. انظر المثال التالي: float x = 42; int y = reinterpret_cast<int&>(x); النتيجة ستكون سلوكًا غير معرَّف. هناك بعض الاستثناءات لقاعدة التسمية البديلة الصارمة (strict aliasing rule): يمكن الوصول إلى كائن كما لو كان من صنف أساسي (base class) للصنف الفعلي الذي ينتمي إليه. يمكن الوصول إلى أيّ نوع كما لو كان من النوع ‎char‎ أو ‎unsigned char‎، لكنّ العكس ليس صحيحًا، مثلًا: لا يمكن الوصول إلى مصفوفة مكوّنة من حروف كما لو كانت نوعًا عشوائيًا. يمكن الوصول إلى نوع عددي صحيح مؤشّر كما لو كان من النوع غير المؤشَّر المقابل له، والعكس صحيح. هناك قاعدة قريبة من هذا تقول أنّه إذا استُدعِي تابع غير ساكن على كائن من صنف آخر غير الصنف الذي عُرّف فيه ذلك التابع، أو صنف مشتقّ منه، فسيحدث سلوك غير معرَّف. ويبقى هذا صحيحًا حتى لو لم تحاول الدالّة الوصول إلى ذلك الكائن. struct Base { }; struct Derived : Base { void f() {} }; struct Unrelated {}; Unrelated u; Derived& r1 = reinterpret_cast<Derived&>(u); // ok r1.f(); // سلوك غير معرَّف Base b; Derived& r2 = reinterpret_cast<Derived&>(b); // ok r2.f(); // سلوك غير معرَّف التحويلات غير الصالحة من صنف مشتق إلى صنف أساسي، للمؤشّرات العضوية (pointers to members) عند استخدام ‎static_cast‎ لتحويل ‎T D::*‎ إلى ‎T B::*‎، يجب أن ينتمي العضو المشار إليه إلى صنف يمثّل صنفًا أساسيًا (base class) أو مشتقًّا من ‎B‎. بخلاف ذلك، فإنّ السلوك سيكون غير معرَّف. محاولة تدمير كائن دُمِّر من قبل في هذا المثال، يُستدعى المُدمّر بشكل صريح على كائن سيتم تدميره تلقائيًا في وقت لاحق. struct S { ~S() { std::cout << "destroying S\n"; } }; int main() { S s; s.~S(); } // سلوك غير معرَّف، بسبب تدمير العنصر للمرة الثانية هنا تحدث مشكلة مماثلة عندما يشير مؤشّر ‎std::unique_ptr<T>‎ إلى نوع ‎T‎ ذي مدّة تخزين (storage duration) تلقائية أو ساكنة، إذ يؤدي تدمير s في حال عودة f إلى سلوك غير معرَّف، لأن s مدمَّرة فعلًا. void f(std::unique_ptr<S> p); int main() { S s; std::unique_ptr<S> p(&s); f(std::move(p)); } محاولة تدمير كائن ما مرتين قد ينتج عنها وجود مؤشّرين مشتركين ‎shared_ptr‎ يديران الكائن دون مشاركة الملكية مع بعضهما البعض. void f(std::shared_ptr<S> p1, std::shared_ptr<S> p2); int main() { S* p = new S; // أريد تمرير نفس الكائن مرّتين std::shared_ptr<S> sp1(p); std::shared_ptr<S> sp2(p); f(sp1, sp2); } نتيجة الشيفرة السابقة سلوك غير معرف لأن sp2 و sp1 سيحاولان تدمير s بشكل منفصل، لكن بأي حال، فما يلي صحيح: std::shared_ptr<S> sp(p); f(sp, sp); محاولة الوصول إلى عضو غير موجود عبر مؤشّر عضوي عند محاولة الوصول إلى عضو غير ساكن من كائن معيّن عبر مؤشّر عضوي (pointer to member)، فسيحدث سلوك غير معرَّف إذا لم يكن الكائن يحتوي فعليًا ذلك العضو الذي يشير إليه المؤشّر، إذ يمكن الحصول على المؤشّر العضوي عبر ‎static_cast‎. struct Base { int x; }; struct Derived : Base { int y; }; int Derived::*pdy = &Derived::y; int Base::*pby = static_cast<int Base::*>(pdy); Base* b1 = new Derived; b1->*pby = 42; // Derived في كائن من النوع y جيد، تعيين Base* b2 = new Base; b2->*pby = 42; // Base في y سلوك غير معرَّف، لأنّه لا يوجد عضو تحويل غير صالح من الصنف الأساسي إلى صنف مشتق عند استخدام ‎static_cast‎ على مؤشّر أو مرجع يشير إلى صنف أساسي، من أجل تحويله إلى مؤشّر أو مرجع يشير إلى صنف مشتق منه، فسيكون السلوك غير معرَّف إن لم يكن المُعامل يشير (يرجِع) إلى كائن من الصنف المشتق. طفح الأعداد العشرية Floating point overflow إذا أنتجت عملية حسابية قيمة لا تنتمي إلى مجال القيم التي يمكن تمثيلها في النوع الناتج، وكان يُفترض أن تعيد عددًا عشريًا، فإنّ السلوك سيكون غير معرَّف وفقًا لمعيار C++‎، لكن قد يكون معرَّفا في معايير أخرى قد تتوافق معها الآلة، مثل IEEE 754. float x = 1.0; for (int i = 0; i < 10000; i++) { x *= 10.0; // قد يؤدّي في النهاية إلى حدوث تدفق زائد، وسينتج عن ذلك سلوك غير معرَّف } استدعاء أعضاء وهمية (خالصة) من مُنشئ أو مدّمر ينصّ المعيار (10.4) على ما يلي: وعمومًا، يقترح بعض كُبراء C++‎، مثل سكوت مايرز (Scott Meyers) تجنّب استدعاء الدوال الوهميّة (حتى غير الخالصة منها) من المنشئات والمُدمِّرات. انظر المثال التالي المُعدّل من الرابط أعلاه: class transaction { public: transaction() { log_it(); } virtual void log_it() const = 0; }; class sell_transaction: public transaction { public: virtual void log_it() const { /* افعل شيئا ما */ } }; لنفترض أنّنا أنشأنا كائنًا ‎sell_transaction‎: sell_transaction s; يستدعي هذا ضمنيًا مُنشئ ‎sell_transaction‎، الذي يستدعي أولًا مُنشئ ‎transaction‎. وفي لحظة استدعاء مُنشئ ‎transaction‎ لن يكون الكائن من النوع ‎sell_transaction‎ بعدُ وإنّما سيكون من النوع ‎transaction‎. وعليه فإنّ استدعاء ‎log_it‎ في ‎transaction::transaction()‎ لن يقوم بالمُتوقّع منه - أي استدعاء ‎sell_transaction::log_it‎. إذا كانت ‎log_it‎ وهمية خالصة، كما هو مُوضّح في هذا المثال، فإنّ السلوك سيكون غير معرَّف إذا لم تكن ‎log_it‎ وهمية خالصة، فستُستدعى ‎transaction::log_it‎. استدعاء دالّة عبر مؤشّر دالّة من نوع غير مطابق من أجل استدعاء دالة عبر مؤشّر دالة، يجب أن يتطابق نوع مؤشّر الدالّة مع نوع الدالّة، أما خلاف ذلك فإنّ السلوك سيكون غير معرَّف. هذا مثال على ذلك: int f(); void(*p)() = reinterpret_cast<void(*)() > (f); p(); // غير معرَّف الإشارة إلى أعضاء غير ساكنة في قائمة مهيئ فيما يلي مزيد من الأمثلة عن كيفية حدوث الأخطاء في C++‎. يمكن أن تؤدي الإشارة إلى أعضاء غير ساكنين (non-static members) في قوائم المهيئات (initializer lists) قبل بدء تنفيذ المُنشئ إلى حدوث سلوك غير معرَّف، ذلك أنه في هذه المرحلة لن يكون جميع الأعضاء قد أُنشِؤوا بعد. انظر هذا المُقتطف من المسوَّدة القياسية: هذا مثال على ذلك: struct W { int j; }; struct X : public virtual W { }; struct Y { int *p; X x; Y() : p(&x.j) { // لم يُنشأ بعد x غير محدد، لأنّ } }; نكتفي بالحديث عن السلوك غير المُعرَّف وننتقل بدءًا من هذا القسم وما يليه إلى الحديث عن السلوك غير المُحدَّد (Unspecified behavior). قيمة لتعداد خارج النطاق عند تحويل تعداد إلى نوع عددِي صحيح، وكان ذلك النوع أصغر من أن يحتفظ بقيمة التعداد، فستكون القيمة الناتجة غير محددة، انظر المثال التالي: enum class E { X = 1, Y = 1000, }; // char نفترض أنّ العدد 1000 لا يُمكن أن يُخزَّن في char c1 = static_cast < char > (E::X); // تساوي 1 c1 قيمة char c2 = static_cast < char > (E::Y); // غير معينة c2 قيمة كذلك عند تحويل عدد صحيح إلى تعداد، وكانت قيمة العدد الصحيح خارج نطاق قيم التعداد، فإنّ القيمة الناتجة ستكون غير محددة. انظر المثال التالي: enum Color { RED = 1, GREEN = 2, BLUE = 3, }; Color c = static_cast < Color > (4); بالمقابل، لمّا كانت قيمة المصدر في المثال التالي تقع في نطاق التعداد، فإنّ السلوك لن يكون "غير معيّن"، رغم أنّها لا تساوي أيّ عدّاد (enumerator): enum Scale { ONE = 1, TWO = 2, FOUR = 4, }; Scale s = static_cast < Scale > (3); هنا، ستكون قيمة ‎s‎ مساوية لـ 3، ولن تساوي ‎ONE‎ أو ‎TWO‎ أو ‎FOUR‎. ترتيب تقييم وسائط دالة (Evaluation order of function arguments) إذا كان لدالّة ما عدّة وسائط، فسيكون ترتيب تقييمها غير محدد. انظر المثال التالي، حيث يحتمل أن تطبع الشيفرة التالية إمّا ‎x = 1, y = 2‎ أو ‎x = 2, y = 1‎. int f(int x, int y) { printf("x = %d, y = %d\n", x, y); } int get_val() { static int x = 0; return ++x; } int main() { f(get_val(), get_val()); } الإصدار ≥ C++‎ 17 في C++‎ 17، يظل ترتيب تقييم وسائط الدالّة غير محدد، رغم أن كل وسائط الدالة تُقيّم بشكل كامل كما يُضمن تقييم كائن الاستدعاء calling object قبل تقييم وسائط الدالة. struct from_int { from_int(int x) { std::cout << "from_int (" << x << ")\n"; } }; int make_int(int x) { std::cout << "make_int (" << x << ")\n"; return x; } void foo(from_int a, from_int b) {} void bar(from_int a, from_int b) {} auto which_func(bool b) { std::cout << b?"foo":"bar" << "\n"; return b ? foo : bar; } int main(int argc, char const*const* argv) { which_func(true)(make_int(1), make_int(2)); } سيكون الخرج إمّا: bar make_int(1) from_int(1) make_int(2) from_int(2) أو: bar make_int(2) from_int(2) make_int(1) from_int(1) قد لا تطبع الشيفرة السلسلة النصية ‎bar‎ بعد ‎make‎ أو ‎from‎، وقد لا تطبع ما يلي أيضًا: bar make_int(2) make_int(1) from_int(2) from_int(1) كانت طباعة ‎bar‎ بعد ‎make_int‎ قبل C++‎ 17 غير جائزة، ولا تنفيذ ‎make_int‎ s قبل ‎from_int‎. نتيجة التحويلات من النوع reinterpret_cast تكون نتيجة التحويل ‎reinterpret_cast‎ من نوع مؤشر دالة إلى آخر أو من نوع مرجع دالة ما إلى آخر، تكون غير محددة. انظر المثال التالي حيث تكون قيمة fp غير محددة. int f(); auto fp = reinterpret_cast<int(*)(int)>(&f); الإصدار ≤ C++‎ 03 بالمثل، ستكون نتيجة تحويل ‎reinterpret_cast‎ من نوع مؤشّر كائن (object pointer) إلى آخر، أو من نوع مرجع كائن (object reference) إلى آخر، غير محددة. انظر المثال التالي حيث تكون قيمة p غير محددة: int x = 42; char* p = reinterpret_cast<char*>(&x); يكافئ هذا في معظم المُصرِّفات تحويلَ ‎static_cast<char*>(static_cast<void*>(&x))‎، لذلك يشير المؤشّر الناتج ‎p‎ إلى البايت الأول من ‎x‎، وهذا هو السلوك القياسي في C++‎ 11. المساحة التي يشغلها مرجع ما المرجع ليس كائنًا، إذ أنّه على عكس الكائنات، لا يحتل بالضرورة مجموعة متجاورة من البايتات في الذاكرة، ولا يحدّد المعيار مسألة ما إذا كان المرجع يتطلب أيّ تخزين أصلًا. وبعض المزايا في C++‎ 17 تجعل مسألة التحقق من أيّ تخزين قد يشغله المرجع بشكل محمول (portably) أمرًا مستحيلًا: فعند تطبيق ‎sizeof‎ على مرجع ما فإنها تُعيد حجم النوع المشار إليه، ومن ثم لن تعطي معلومات عمّا إذا كان المرجع يشغل مساحة تخزين ما. مصفوفات المراجع (Arrays of references) غير جائزة، لذا لا يمكن التحقّق من عنواني عنصرين متتاليين لمرجع يشير إلى مصفوفات بُغية تحديد حجم المرجع. في حال أخذ عنوان مرجع ما، فإنّ النتيجة ستكون عنوان العنصر المشار إليه في ذلك المرجع، لذا لا يمكننا الحصول على مؤشّر يشير إلى المرجع نفسه. إذا كان لصنف ما عضو مرجعي (reference member)، فإنّ محاولة استخراج عنوان ذلك العضو باستخدام ‎offsetof‎ ستؤدي إلى سلوك غير معرَّف، لأنّ مثل هذا الصنف لن يكون صنفَ تخطيط قياسي (standard-layout class). إذا كان لصنف ما عضوٌ مرجعي، فلن يعدّ الصنف تخطيطًا قياسيًا (standard layout)، لذا سينتج عن محاولة الوصول إلى البيانات المُستخدمة لتخزين المرجع سلوكًا غير معرَّف، أو سلوكًا غير محدد. وعمليًا، يمكن في بعض الحالات تنفيذ متغيّر مرجعي (reference variable) على هيئة متغيّر مؤشّر (pointer variable)، وسيشغل حينها نفس مساحة التخزين التي يشغلها المؤشّر، بينما قد لا يشغل المرجع في حالات أخرى أيّ مساحة على الإطلاق نتيجة لعمليات التحسين (optimisation). على سبيل المثال، في الشيفرة التالية: void f() { int x; int& r = x; // r افعل شيئا ما بـ } يستطيع المُصرِّف معاملة ‎r‎ كاسم بديل (alias) لـ ‎x‎، واستبدال كل تكرارات ‎x‎ في بقية الدالّة ‎f‎ بـ r، مع عدم تخصيص أيّ ذاكرة لـ ‎r‎. حالة "منقول-منه" Moved-from لأغلب أصناف المكتبات القياسية الإصدار ≥ C++‎ 11 تُترك جميع حاويات المكتبات القياسية في حالة غير محددة لكن صالحة بعد النقل منها. على سبيل المثال، في الشيفرة التالية، ‎v2‎ ستتضمّن ‎{1, 2, 3, 4}‎ بعد النقل، لكن لن تكون ‎v1‎ فارغة بالضرورة. int main() { std::vector v1{1, 2, 3, 4}; std::vector < int > v2 = std::move(v1); } تكون لبعض الأصناف حالة مُحدّدة بدقة بعد النقل منها، والحالة الأهم هي حالة std::unique_ptr<T>‎، التي تكون فارغة بعد النقل منها. نتيجة المقارنة بين المؤشّرات ستكون النتيجة غير محددة عند مقارنة مؤشّرين باستخدام العوامل ‎<‎ أو ‎>‎ أو ‎<=‎ أو ‎>=‎، وذلك في الحالات التالية: إذا كانت المؤشّرات تشير إلى مصفوفات مختلفة، إذ تُعدّ الكائنات التي ليست مصفوفات عبارة عن مصفوفات من الحجم 1. int x; int y; const bool b1 = &x < &y; // غير محدد int a[10]; const bool b2 = &a[0] < &a[1]; // true const bool b3 = &a[0] < &x; // غير محدد const bool b4 = (a + 9) < (a + 10); // true // إلى الموضع الذي يلي المصفوفة a+10 تشير إذا كانت المؤشرات تشير إلى داخل نفس الكائن، لكن إلى أعضاء ذات متحكمات وصول (access control) مختلفة. class A { public: int x; int y; bool f1() { return &x < &y; } // true bool f2() { return &x < &z; } // غير محدد private: int z; }; ‎ التحويل الساكن من من قيمة من النوع void*‎ إذا حُوِّلَت قيمةٌ من نوع ‎void*‎ إلى مؤشّر يشير إلى نوع ‎T*‎، لكن لم تحاذى بشكل صحيح مع ‎T‎، فستكون قيمة المؤشّر الناتجة غير محددة. انظر المثال التالي، لنفترض أن (alignof(int تساوي 4: int x = 42; void* p1 = &x; // إنجاز بعض العمليات الحسابية على المؤشر void* p2 = static_cast<char*>(p1) + 2; int* p3 = static_cast<int*>(p2); قيمة ‎p3‎ غير محددة لأنّ ‎p2‎ لا يمكن أن تشير إلى كائن من النوع ‎int‎؛ فمُحاذاة عنوان قيمتها غير صحيحة. ترتيب تهيئة الكائنات العامّة عبر وحدة الترجمة يكون ترتيب تهيئة المتغيّرات العامة محددًا داخل وحدة الترجمة، بينما يكون ترتيب التهيئة عبر عدة وحدات ترجمة غير محدد. لذلك فالبرنامج الذي به الملفات التالية: foo.cpp #include <iostream> int dummyFoo = ((std::cout << "foo"), 0); bar.cpp #include <iostream> int dummyBar = ((std::cout << "bar"), 0); main.cpp int main() {} قد يُنتِج الخرج التالي: foobar أو: barfoo وقد يؤدّي ذلك إلى إخفاق ترتيب التهيئة الساكنة (Static Initialization Order Fiasco). الاتحادات Unions والسلوك غير المعرَّف انظر المثال التالي: union U { int a; short b; float c; }; U u; u.a = 10; if (u.b == 10) { } سينجم عن الشيفرة أعلاه سلوك غير معرَّف، ذلك أن a كان آخر عضو يُكتَب فيه. وتجيز الكثير من المصرّفات هذا الأمر، وقد تكتفي بإصدار تحذير، بيْد أنّ النتيجة ستكون "كما هو متوقع"؛ أن هذه إضافة للمصرّف (extension compiler)، لذا لا يمكن ضمان هذا السلوك في جميع المصرّفات (فهي شيفرة غير محمولة ولا متوافقة). الاتحادات (Unions) هي بنيات مخصصة تحتلّ أعضاؤها مساحة مشتركة في الذاكرة. union U { int a; short b; float c; }; U u; //سيكونان متساويين a و b عنوانا (void*)&u.a == (void*)&u.b; (void*)&u.a == (void*)&u.c; // إسناد قيمة إلى أيّ عضو من الاتحاد يغير الذاكرة المشتركة u.c = 4. f; u.a = 5; u.c != 4. f; تساعد الاتحادات على ترشيد استخدام الذاكرة المخصّصة للبيانات الحصرية (exclusive data)، كما في تنفيذ أنواع مختلطة من البيانات. struct AnyType { enum { IS_INT, IS_FLOAT } type; union Data { int as_int; float as_float; } value; AnyType(int i): type(IS_INT) { value.as_int = i; } AnyType(float f): type(IS_FLOAT) { value.as_float = f; } int get_int() const { if (type == IS_INT) return value.as_int; else return (int) value.as_float; } float get_float() const { if (type == IS_FLOAT) return value.as_float; else return (float) value.as_int; } }; هذا الدرس جزء من سلسلة دروس عن C++‎. ترجمة -بتصرّف- للفصول: Chapter 104: Undefined Behavior Chapter 140: More undefined behaviors in C++‎ Chapter 121: Unspecified behavior Chapter 111: Unions من كتاب C++ Notes for Professionals
  21. كلمة SFINAE هي اختصار للجملة: Substitution Failure Is Not An Error، وتشير إلى أنّه لا تُعد الشيفرات سيئة الصيغة بسبب تعويض الأنواع (أو القيم) لأجل استنساخ قالب دالة (instantiate a function template) أو قالب صنف، لا تُعدُّ خطأً تصريفيا فادحًا (hard compile error)، وإنّما يتم التعامل معها على أنّها فشل في استنتاج النوع فقط. فشل استنتاج النوع في قوالب دوالّ الاستنساخ (instantiating function templates) أو تخصيصات قالب الصنف (class template specializations) يؤدي إلى إهمال ذلك النوع أثناء محاولة استنتاج النوع (كما لو أنّ ذلك النوع المرشح المُستبعد لم يكن موجودًا من البداية). template < class T> auto begin(T& c) -> decltype(c.begin()) { return c.begin(); } template < class T, size_t N> T* begin(T (&arr)[N]) { return arr; } int vals[10]; begin(vals); تفشل المحاولة الأولى لتعويض قالب الدالة في (begin(vals من الشيفرة السابقة، لأن صيغة ()vals.begin خطأ، لكن لا يعطي هذا خطأً بل ستُزال تلك الدالة من ترشيحها للتحميل الزائد، تاركة إيانا مع التحميل الزائد للمصفوفات (array overload). لا يعدُّ فشل الاستبدال فشلًا في الاستنتاج إلّا في السياق الفوري (immediate context)، أمّا في الحالات الأخرى، فستُعد أخطاء فادحة (hard errors). template < class T> void add_one(T& val) { val += 1; } int i = 4; add_one(i); // حسنا std::string msg = "Hello"; add_one(msg); // error. msg += 1 is ill-formed for std::string, but this // T لم يحدث الفشل في السياق الفوري لتعويض void_t الإصدار ≥ C++‎ 11 ‎void_t‎ هي دالّة وصفية (meta-function) تحول الأنواع إلى النوع الفارغ (type void)، وغرضها الأساسي هو تسهيل كتابة سمات النوع (type traits). ستكون std::void_t جزء من C++‎ 17، ولكنّ تنفيذها سهل على أي حال: template < class... > using void_t = void; تتطّلب بعض المٌصرّفات تنفيذًا مختلفًا قليلاً: template < class... > struct make_void { using type = void; }; template < typename...T > using void_t = typename make_void < T... >::type; التطبيق الأساسي لـ ‎void_t‎ هو كتابة سمات النوع التي تتحقّق من صحّة عبارة برمجية. على سبيل المثال، دعنا نتحقّق ممّا إذا كان نوع ما له دالة تابعة ‎foo()‎ لا تأخذ أيّة وسائط: template < class T, class = void > struct has_foo: std::false_type {}; template < class T> struct has_foo<T, void_t<decltype(std::declval<T&>().foo())>> : std::true_type {}; سيحاول المُصرّف عند محاولة استنساخ ‎has_foo<T>::value‎ أن يبحث عن أفضل تخصيص لـ ‎has_foo<T, void>‎. لدينا خياران هنا، الأوليّ والثانوي، يتطلب الثانوي استنساخ التعبير الأساسي (underlying expression): إذا لم يحتوي‎T‎ على دالة تابعة ‎foo()‎، فسيُحوّل النوع المُعاد إلى ‎void‎، وسيُفضَّل التخصِيص (specialization) على القالب الأوّلي بناءً على قواعد الترتيب الجزئي للقوالب (template partial ordering rules). لذا، فإنّ has_foo<T>::value ستساوي true إذا لم يكن ‎T‎ يتحوي على تلك الدالة التابعة (أو إذا كانت موجودة، بيْد أنّها تتطّلب أكثر من وسيط واحد)، فستفشل عملية الاستبدال بالتخصيص، وسنعود إلى القالب الأساسي. وعندئذ ستساوي ‎has_foo<T>::value‎ القيمة ‎false‎. هذا المثال لا يستخدم ‎std::declval‎ أو ‎decltype‎: template < class T, class = void > struct can_reference: std::false_type {}; template < class T> struct can_reference<T, std::void_t<T&>> : std::true_type {}; لاحظ النمط الشائع لاستخدام الوسيط الفارغ (void argument). يمكننا كتابة الشيفرة التالية: struct details { template<template < class... > class Z, class = void, class...Ts > struct can_apply: std::false_type {}; template<template < class... > class Z, class...Ts > struct can_apply<Z, std::void_t<Z < Ts... >>, Ts... >: std::true_type {}; }; template<template < class... > class Z, class...Ts > using can_apply = details::can_apply<Z, void, Ts... > ; والتي تتجّنب استخدام ‎std::void_t‎، وتستخدم ‎can_apply‎ بدلًا من ذلك، والتي تتصرّف كمحدّد (indicator) يوضح ما إذا كان النوع المتوفّر كوسيط أوّل للقالب مُصاغًا صيغة صحيحة بعد استبدال الأنواع الأخرى فيه. يمكن الآن إعادة كتابة الأمثلة السابقة باستخدام ‎can_apply‎ على النحو التالي: template<class T> using ref_t = T&; template<class T> using can_reference = can_apply<ref_t, T>; // مصاغة صيغة صحيحة T& و: template<class T> using dot_foo_r = decltype(std::declval<T&>().foo()); template<class T> using can_dot_foo = can_apply< dot_foo_r, T >; // مصاغة صيغة صَحيحة T.foo() والتي تبدو أبسط من النسخ السابقة، هناك مقترحات بعد الإصدار C++‎ 17 لإنشاء سمات ‎std‎ مماثلة لـ ‎can_apply‎. يُعزى اكتشاف فائدة ‎void_t‎ إلى والتر براون (Walter Brown)، في العرض الرائع الذي قدّمه في CppCon 2016. enable_if std::enable_if هي أداة مساعدة لاستخدام الشروط المنطقية لتفعيل قاعدة SFINAE. وتُعرّف على النحو التالي: template < bool Cond, typename Result = void > struct enable_if {}; template < typename Result> struct enable_if<true, Result> { using type = Result; }; بمعنى أنّ ‎enable_if<true, R>::type‎ هو اسم بديل (alias) لـ ‎R‎، في حين أنّ صيغة ‎enable_if<false, T>::type‎ غير صحيحة نظرًا لأنّ تخصيص ‎enable_if‎ لا يحتوي على نوع عضوي (member type‏) ‎type‎. نستطيع استخدام std::enable_if لتقييد القوالب: int negate(int i) { return -i; } template <class F> auto negate(F f) { return -f(); } هنا سيفشل استدعاء ‎negate(1)‎ بسبب الغموض، لكنّ التحميل الزائد الثاني ليس مُعدًّا لاستخدامه مع الأنواع العددية الصحيحة، لذلك يمكننا إضافة: int negate(int i) { return -i; } template <class F, class = typename std::enable_if<!std::is_arithmetic<F>::value>::type> auto negate(F f) { return -f(); } الآن، سيؤدّي استنساخ ‎negate<int>‎ إلى فشل التعويض لأنّ ‎!std::is_arithmetic<int>::value‎ تساوي ‎false‎. لكن بسبب قاعدة SFINAE، فلَن يكون هذا خطأ فادحًا (hard error)، وإنّما سيُزال هذا المرشّح من مجموعة المرشحين للتحميل الزائد وحسب. ونتيجة لذلك، لن يكون لـ negate(1)‎ إلّا مرشح واحد وهو الذي سيُستدعى. متى نستخدَمها تذكر أنّ ‎std::enable_if‎ هو مُساعد يعمل مع قاعدة SFINAE، لكن ليس هو الذي يجعلها تعمل في المقام الأول، دعنا ننظر في البديلين التاليين لتنفيذ وظائف مماثلة لـ ‎std::size‎، وهي مجموعة تحميل زائد لـ ‎size(arg)‎ تعيد حجم الحاوية أو المصفوفة: // للحاويات template < typename Cont> auto size1(Cont const& cont) -> decltype( cont.size() ); // للمصفوفات template<typename Elt, std::size_t Size> std::size_t size1(Elt const(&arr)[Size]); // حذف التنفيذ template < typename Cont> struct is_sizeable; // للحاويات template<typename Cont, std::enable_if_t<std::is_sizeable<Cont>::value, int> = 0> auto size2(Cont const& cont); // للمصفوفات template < typename Elt, std::size_t Size> std::size_t size2(Elt const(&arr)[Size]); على افتراض أنّ ‎is_sizeable‎ مكتوبة بشكل صحيح، فيجب أن يكون هذان التصريحان متكافئين تمامًا بحسب قاعدةSFINAE، فأيّهما أسهل في الكتابة وفي المراجعة والفهم؟ سنحاول الآن تنفيذ بعض المساعِدات الحسابية التي تتفادى طفح (overflow) الأعداد الصحيحة غير المؤشّرة لصالح سلوك الالتفاف (wraparound) أو السلوك القابل للتعديل (modular). هذا يعني مثلًا أنّ ‎incr(i, 3)‎ ستكافئ ‎i += 3‎ باستثناء حقيقة أنّ النتيجة ستكون دائمًا مُعرّفة حتى لو كان ‎i‎ عددًا صحيحًا يساوي ‎INT_MAX‎. ما يلي بديلان ممكنان: // معالجة الأنواع المؤشَّرة template < typename Int> auto incr1(Int& target, Int amount) -> std::void_t<int[static_cast<Int>(-1) < static_cast<Int>(0)]>; // target += amount معالجة الأنواع غير المؤشَّرة عبر // بما أنّ حسابيات العناصر غير المؤشَّرة تتصرف كما هو مطلوب template < typename Int> auto incr1(Int& target, Int amount) -> std::void_t<int[static_cast<Int>(0) < static_cast<Int>(-1)]>; template<typename Int, std::enable_if_t<std::is_signed<Int>::value, int> = 0> void incr2(Int& target, Int amount); template<typename Int, std::enable_if_t<std::is_unsigned<Int>::value, int> = 0> void incr2(Int& target, Int amount); مرّة أخرى، أيّهما أسهل في الكتابة، وأيّهما أسهل في المراجعة والفهم؟ تتمثّل قوة ‎std::enable_if‎ في طريقة تعاملها مع إعادة الإنتاج (refactoring) وتصميم الواجهات البرمجية، فإذا كان الغرض من ‎is_sizeable<Cont>::value‎ هو التحقّق من صحّة ‎cont.size()‎، فقد يكون استخدام التعبير كما يظهر في ‎size1‎ أوجز، رغم أنّ ذلك قد يعتمد على ما إذا كانت ‎is_sizeable‎ ستُستخدَم في العديد من المواضع أم لا. على النقيض من ذلك، فإنّ ‎std::is_signed‎ أكثر وضوحًا ممّا كانت عليه عندما كان تنفيذها يتسرّب إلى تصريح ‎incr1‎. is_detected لتعميم إنشاء type_trait استنادًا إلى قاعدة SFINAE، فهناك بعض السمات التجريبية، وهي: ‎detected_or‎ و ‎detected_t‎ و ‎is_detected‎. ومع معاملات القوالب ‎typename Default‎ و ‎template <typename...> Op‎ و ‎typename ... Args‎: ‎is_detected‎: هو اسم بديل لـ std::true_type أو std::false_type اعتمادًا على صلاحية Op<Args...> ‎detected_t‎: هو اسم بديل لـ Op<Args...>‎ أو ‎nonesuch‎ اعتمادًا على صلاحية Op<Args...>‎. ‎detected_or‎: هو اسم بديل لبنية لها ‎value_t‎ مرصودة (‎is_detected‎)، ونوع ‎type‎ يحقّقOp<Args...>‎ أو ‎Default‎ اعتمادًا على صلاحية ‎Op<Args...>‎ ويمكن تنفيذ باستخدام ‎std::void_t‎ لأجل قاعدة SFINAE على النحو التالي: الإصدار ≥ C++‎ 17 namespace detail { template < class Default, class AlwaysVoid, template < class... > class Op, class...Args > struct detector { using value_t = std::false_type; using type = Default; }; template < class Default, template < class... > class Op, class...Args > struct detector<Default, std::void_t<Op < Args... >>, Op, Args... > { using value_t = std::true_type; using type = Op < Args... > ; }; } // تفاصيل فضاء الاسم // نوع خاص للإشار إلى رصد الخطأ struct nonesuch { nonesuch() = delete; ~nonesuch() = delete; nonesuch(nonesuch const&) = delete; void operator=(nonesuch const&) = delete; }; template <template<class...> class Op, class... Args> using is_detected = typename detail::detector<nonesuch, void, Op, Args...>::value_t; template <template<class...> class Op, class... Args> using detected_t = typename detail::detector<nonesuch, void, Op, Args...>::type; template <class Default, template<class...> class Op, class... Args> using detected_or = detail::detector<Default, void, Op, Args...>; يمكن تنفيذ السمات التي ترصد وجود تابع على النحو التالي: typename <typename T, typename ...Ts> using foo_type = decltype(std::declval<T>().foo(std::declval<Ts>()...)); struct C1 {}; struct C2 { int foo(char) const; }; template < typename T> using has_foo_char = is_detected<foo_type, T, char> ; static_assert(!has_foo_char<C1>::value, "Unexpected"); static_assert(has_foo_char<C2>::value, "Unexpected"); static_assert(std::is_same<int, detected_t<foo_type, C2, char>>::value, "Unexpected"); static_assert(std::is_same<void, // افتراضي detected_or<void, foo_type, C1, char>>::value, "Unexpected"); static_assert(std::is_same<int, detected_or<void, foo_type, C2, char>>::value, "Unexpected"); تحليل تحميل زائد له عدد كبير من الخيارات إذا كنت بحاجة إلى الاختيار بين عدة خيارات، فقد يكون تمكين خيار واحد فقط عبر ‎enable_if<>‎ مرهقًا للغاية، إذ يجب إلغاء العديد من الشروط لاستبعاد الخيارات الأخرى، وبدلاً من ذلك، يمكن اختيار ترتيب التحميلات الزائد باستخدام الوراثة، أي بإرسال الوسم (tag dispatch). وبدلاً من التحقق من صحّة الصيغة وكذلك التحقق من أنّ جميع الشروط الأخرى غير متحقّقة، فإنّنا سنكتفي باختبار الأشياء التي نحتاجها وحسب، ويفضل أن يكون ذلك في ‎decltype‎ في إعادة زائدة (trailing return). قد يؤدّي هذا إلى تجاهل الكثير من الخيارات ذات الصيغة الصحيحة، لكن نستطيع التفريق بينها باستخدام "الوسوم" (tags)، على غرار وسوم مكرّرات السمات (‎random_access_tag‎). وسيعمل هذا بدون مشاكل لأنّ الحصول على تطابق مباشر أفضل من الصنف الأساسي (base class)، والذي هو بدوره أفضل من الصنف الأساسي لصنف أساسي (base class of a base class)، وهكذا دواليك. #include <algorithm> #include <iterator> namespace detail { // سيعطينا هذا عددا غير محدود من الأنواع التي ترث بعضها بعضا template<std::size_t N> struct pick: pick < N - 1 > {}; template < > struct pick < 0> {}; التحميل الزائد الذي نريد له أن يكون مفضَّلًا يجب أن تكون قيمة N له أكبر في <pick<N، ما يلي أول دالة قالب مساعِدة، نتابع المثال … template < typename T> auto stable_sort(T& t, pick<2>)-> decltype( t.stable_sort(), void() ) { إن كانت الحاوية لديها stable_sort فاستخدمه، … t.stable_sort(); } // المساعد سيكون ثاني أفضل تطابق ممكن template < typename T> template<typename T> auto stable_sort(T& t, pick<1>)-> decltype( t.sort(), void() ) { إذا كان للحاوية عضو sort لكن لم يكن فيها stable_sort فسيكون sort مستقرًا غالبًا، نتابع … t.sort(); } // هذا المساعد سيكون آخر مرشح template < typename T> auto stable_sort(T& t, pick<0>)-> decltype( std::stable_sort(std::begin(t), std::end(t)), void() ) { الحاوية لا تحتوي على عضو sort أو stable_sort … std::stable_sort(std::begin(t), std::end(t)); } } // 'tags' هذه هي الدالة التي يستدعيها المستخدم، ستقوم بإرسال الاستدعاء إلى التقديم الصحيح بمساعدة template < typename T> void stable_sort(T& t) { // مع قيمة أكبر من القيم السابقة N استخدم // هذا سيختار أعلى تحميل زائد من بين التحميلات الزائدة صحيحة الصيغة detail::stable_sort(t, detail::pick < 10> {}); } هناك عدّة طرق أخرى للتمييز بين التحميلات الزائدة، مثل أنّ المطابقة التامة أفضل من التحويل، والتي هي بدورها أفضل من علامة الحذف (ellipsis). بالمقابل، يمكن أن يتوسّع وسم الإرسال إلى أيّ عدد من الخيارات، وهو أكثر وضوحًا وصراحة في العادة. الكلمة المفتاحية decltype الزائدة في قوالب الدوالّ الإصدار ≥ C++‎ 11 يمكن استخدام ‎decltype‎ زائدة (trailing) لتحديد نوع القيمة المُعادة: namespace details { using std::to_string; // to_string(T) ينبغي أن تكون قادرة على استدعاء template < class T> auto convert_to_string(T const& val, int )-> decltype(to_string(val)) } // ellipsis argument هذه غير مقيّدة، لكن يُفضَّل عدم استخدامها بسبب وسيط علامة الحذف template < class T> std::string convert_to_string(T const& val, ... ) { std::ostringstream oss; oss << val; return oss.str(); } } template < class T> std::string convert_to_string(T const& val) { return details::convert_to_string(val, 0); } في حال استدعاء ‎convert_to_string()‎ باستخدام وسيط يمكن استدعاء الدالة ‎to_string()‎ من خلاله، فعندها ستكون لدينا دالتان قابلتان للتطبيق على ‎details::convert_to_string()‎، والأولى مُفضّلة لأنّ تسلسل التحويل الضمني من ‎0‎ إلى ‎int‎ أفضل من التحويل من ‎0‎ إلى ‎...‎ أما في حال استدعاء ‎convert_to_string()‎ باستخدام وسيط لا يمكننا عبره استدعاء ‎to_string()‎، فحينئذٍ سيؤدّي استنساخ قالب الدالة الأوّل إلى فشل الاستبدال - substitution failure - (ليس هناك ‎decltype(to_string(val))‎)، ونتيجة لذلك، يُزال هذا المرشح من مجموعة التحميل الزائد. قالب الدالّة الثاني غير مقيّد ولذا تم اختياره، وسنمرّ عبر ‎operator<<(std::ostream&, T)‎، أمّا في حال لم يكن معرَّفًا فسيحدث خطأ فادح في التصريف لمكدّس القالب (template stack) في سطر ‎oss << val‎. enableifall / enableifany الإصدار ≥ C++‎ 11 مثال تحفيزي لدينا في الشيفرة التالية حزمة قالب متغيّرة (variadic template pack) في قائمة معاملات القالب: template < typename...Args > void func(Args && ...args) { //... }; لا تعطينا المكتبة القياسية (قبل الإصدار C++‎ 17) أيّ طريقة مباشرة لكتابة enable_if لفرض قيود قاعدة SFINAE على جميع (أو أيٍّ من) المعاملات في ‎Args‎. توفّر C++‎ 17 حلّين لهذه المشكلة، وهما: ‎std::conjunction‎ و ‎std::disjunction‎. انظر المثال التالي: قيود SFINAE على جميع المعامِلات في Args: template<typename ...Args, std::enable_if_t<std::conjunction_v<custom_conditions_v<Args>...>>* = nullptr> void func(Args &&...args) { //... }; template<typename ...Args, std::enable_if_t<std::disjunction_v<custom_conditions_v<Args>...>>* = nullptr> void func(Args &&...args) { //... }; إذا كنت تعمل بإصدار أقل من C++‎ 17، فهناك العديد من الحلول الممكنة لتحقيق ذلك، أحدها هو استخدام صنف الحالة الأساسية (base-case class) والتخصيصات الجزئية، كما هو مُوضّح في جواب هذا السؤال. يمكن أيضًا تنفيذ سلوك ‎std::conjunction‎ و ‎std::disjunction‎ بطريقة مباشرة، وسأوضح في المثال التالي، كيفيّة كتابة التنفيذات وسأجمعها مع std::enable_if لإنتاج كُنيتين: ‎enable_if_all‎ و ‎enable_if_any‎، واللّتان تفعلان بالضبط ما يفترض بهما فعله. قد يكون هذا الحلّ أكثر قابلية للتوسيع. تطبيق ‎enable_if_all‎ و ‎enable_if_any‎ أولاً، سنحاكي ‎std::conjunction‎ و ‎std::disjunction‎ باستخدام ‎seq_and‎ و ‎seq_or‎: /// C++14 مساعد لاستخدامه في الإصدارات التي تسبق template<bool B, class T, class F > using conditional_t = typename std::conditional<B,T,F>::type; /// Emulate C++17 std::conjunction. template<bool...> struct seq_or: std::false_type {}; template<bool...> struct seq_and: std::true_type {}; template<bool B1, bool... Bs> struct seq_or<B1,Bs...>: conditional_t<B1,std::true_type,seq_or<Bs...>> {}; template<bool B1, bool... Bs> struct seq_and<B1,Bs...>: conditional_t<B1,seq_and<Bs...>,std::false_type> {}; الآن سيصبح التنفيذ واضحًا: template < bool...Bs > using enable_if_any = std::enable_if<seq_or < Bs... >::value > ; template < bool...Bs > using enable_if_all = std::enable_if<seq_and < Bs... >::value > ; وأخيرًا بعض المساعِدات: template < bool...Bs > using enable_if_any_t = typename enable_if_any < Bs... >::type; template < bool...Bs > using enable_if_all_t = typename enable_if_all < Bs... >::type; كيفية الاستخدام الاستخدام واضح ومباشر: قيود SFINAE على جميع المعامِلات في Args: template<typename ...Args, enable_if_all_t<custom_conditions_v<Args>...>* = nullptr> void func(Args &&...args) { //... }; template<typename ...Args, enable_if_any_t<custom_conditions_v<Args>...>* = nullptr> void func(Args &&...args) { //... }; هذا الدرس جزء من سلسلة دروس عن C++‎. ترجمة -بتصرّف- للفصل Chapter 103: SFINAE (Substitution Failure Is Not An Error)‎ من كتاب C++ Notes for Professionals
  22. آلية RTTI: معلومات الأنواع في وقت التشغيل (Run-Time Type Information) dynamic_cast استخدم ‎dynamic_cast<>()‎ كدالة تساعدك على التخفيض النوعي (downcasting) في التسلسل الهرمي للوراثة (الوصف الرئيسي). وإذا كنت بحاجة إلى إجراء بعض الأعمال غير متعددة الأشكال (non-polymorphic) على صنفين مشتقّين ‎B‎ و ‎C‎ عبر الصنف الأب ‎class A‎، فستحتاج إلى كتابة ما يلي: class A { public: virtual ~A(){} }; class B: public A { public: void work4B(){} }; class C: public A { public: void work4C(){} }; void non_polymorphic_work(A* ap) { if (B* bp =dynamic_cast<B*>(ap)) bp->work4B(); if (C* cp =dynamic_cast<C*>(ap)) cp->work4C(); } الكلمة المفتاحية typeid الكلمة المفتاحية ‎typeid‎ هي عامل أحادي يعطي معلومات حول النوع المُمرّر إليها في وقت التشغيل في حال كان معامَلها (operand) كائنًا من صنف متعدد الأشكال. تعيد ‎typeid‎ قيمةً يسارية من النوع ‎const std::type_info‎، كما تتجاهَل التأهيل عالي المستوى (Top-level cv-qualification). struct Base { virtual~Base() = default; }; struct Derived: Base {}; Base *b = new Derived; assert(typeid(*b) == typeid(Derived {})); // حسنا يمكن أيضًا تطبيق ‎typeid‎ على النوع مباشرة، ويتم تجريد مراجع المستوى الأعلى الأولى (first top-level references) في هذه الحالة، ثم يُتجاهَل التأهيل عالي المستوى. ومن ثم يمكن كتابة المثال أعلاه باستخدام ‎typeid(Derived)‎ بدلاً من typeid(Derived{})‎: assert(typeid(*b) == typeid(Derived {})); // OK إذا طُبِّقت ‎typeid‎ على تعبير من غير النوع متعدد الأشكال فلن يُقيَّم المعامَل، أمّا معلومات النوع المُعادة فستخصّ النوع الساكن. struct Base { // ملاحظة: لا مدمّرات وهمية }; struct Derived: Base {}; Derived d; Base &b = d; assert(typeid(b) == typeid(Base)); // غير مشتق assert(typeid(std::declval<Base> ()) == typeid(Base)); // لابأس، لأنّه غير مُقيَّم أسماء الأنواع تستطيع الحصول على الاسم المعرَّف من قِبل التنفيذ لنوع معيّن في وقت التشغيل باستخدام الدالة التابعة ‎.name()‎ الخاص بالكائن ‎std::type_info‎ المُعاد من قِبل ‎typeid‎. #include <iostream> #include <typeinfo> int main() { int speed = 110; std::cout << typeid(speed).name() << '\n'; } يكون الخرج ما يلي (متعلق بالتنفيذ): int كيف تعرّف التحويل الذي ينبغي استخدامه استخدم التحويل الديناميكي dynamic_cast لتحويل المؤشّرات/المراجع داخل التسلسل الهرمي للوراثة. استخدم التحويل الساكن static_cast لإجراء تحويلات الأنواع العادية. استخدم تحويل إعادة التفسير reinterpret_cast لإعادة تفسير أنماط البتات منخفضة المستوى، لكن استخدمه بحذر شديد. استخدم التحويل الثابت const_cast للتخلص من الثباتيّة أو التغايرية (const/volatile). تجنّب هذا الخيار ولا تستخدمه إلّا كنت مضطرًّا لاستخدام واجهة برمجية غير صحيحة ثباتيًّا (const-incorrect API). شطب الأنواع Type Erasure شطب النوع (Type Erasure) هو مجموعة من التقنيات الهادفة لإنشاء نوع يمكن أن يوفّر واجهة موحّدة للأنواع الأساسية (underlying types)، مع إخفاء معلومات النوع الأساسي عن العميل. وتُعدُّ ‎std::function<R(A...)>‎، التي يمكنها تخزين كائنات قابلة للاستدعاء من مختلف الأنواع، أفضل مثال معروف على شطب الأنواع في C++‎. std::function للنقل فقط شطب النوع std::function ينحصر في عدد قليل من العمليات، وأحد الأشياء التي يتطّلّبها الشطب أن تكون القيمة المخزّنة قابلة للنسخ. لكن هذا قد يتسبّب بمشاكل في بعض السياقات، كما في حالة تخزين تعابير لامبدا للمؤشرات الحصريّة (unique ptrs)، وقد يضيف هذا المتطلَّب حِملًا إضافيًا على البرنامج إذا كنت تستخدم ‎std::function‎ في سياق لا يهمّ فيه النسخ، كساحة خيوط (thread pool) مثلًا، حيث توفد (dispatch) المهامّ إلى الخيوط. الكائن ‎std::packaged_task<Sig>‎ هو كائن قابل للاستدعاء، كما أنه قابل للنقل فقط (move-only)، وتستطيع تخزين std::packaged_task<R(Args...)>‎ في std::packaged_task<void(Args...)>‎، إلا أنّها طريقة بطيئة لإنشاء صنف شطب للنوع (type-erasure class) يكون للنقل فقط (move-only) وقابلا للاستدعاء (callable) في نفس الوقت. يوضّح المثال التالي كيف يمكنك كتابة نوع ‎std::function‎ بسيط، سنحذف مُنشئ النسخ - copy constructor - (والذي يتضمّن إضافة تابع ‎clone‎ إلى ‎details::task_pimpl<...>‎). سنضعه في فضاء اسم (namespace) إذ سيسمح لنا ذلك بتخصيصه للقيمة المعادة الفارغة void: template < class Sig> struct task; namespace details { template < class R, class...Args > struct task_pimpl { virtual R invoke(Args && ...args) const = 0; virtual~task_pimpl() {}; virtual const std::type_info &target_type() const = 0; }; // store an F. invoke(Args&&...) calls the f template < class F, class R, class...Args > struct task_pimpl_impl: task_pimpl<R, Args... > { F f; template < class Fin> task_pimpl_impl(Fin && fin): f(std::forward<Fin> (fin)) {} virtual R invoke(Args && ...args) const final override { return f(std::forward<Args> (args)...); } virtual const std::type_info &target_type() const final override { return typeid(F); } }; سيتجاهل إصدار void القيمة التي تعيدها f، نتابع … template < class F, class...Args > struct task_pimpl_impl<F, void, Args... >: task_pimpl<void, Args... > { F f; template < class Fin> task_pimpl_impl(Fin && fin): f(std::forward<Fin> (fin)) {} virtual void invoke(Args && ...args) const final override { f(std::forward<Args> (args)...); } virtual const std::type_info &target_type() const final override { return typeid(F); } }; }; template < class R, class...Args > struct task < R(Args...) > { // semi-regular: task() = default; task(task &&) = default; // no copy private: هنا ننشئ أسماءً بديلة أو كُنى (aliases) لتحسين مظهر شيفرة sfinae أدناه، … template < class F> using call_r = std::result_of_t < F const &(Args...) > ; template < class F> using is_task = std::is_same<std::decay_t < F>, task> ; public: // قابل للاستدعاء F يمكن تدميرها من كائن template < class F, class= decltype( (R)(std::declval<call_r<F>>()) ), // ليس النوع نفسه std::enable_if_t<!is_task<F>{}, int>* = nullptr> task(F&& f):m_pImpl( make_pimpl(std::forward<F>(f)) ) {} R operator()(Args...args) const { return m_pImpl->invoke(std::forward<Args> (args)...); } explicit operator bool() const { return (bool) m_pImpl; } void swap(task & o) { std::swap(m_pImpl, o.m_pImpl); } template < class F> void assign(F && f) { m_pImpl = make_pimpl(std::forward<F> (f)); } // std::function جزء من واجهة const std::type_info &target_type() const { if (! *this) return typeid(void); return m_pImpl->target_type(); } template < class T> T* target() { return target_impl<T> (); } template < class T> const T* target() const { return target_impl<T> (); } // nullptr مقارنة مع friend bool operator==(std::nullptr_t, task const &self) { return !self; } friend bool operator==(task const &self, std::nullptr_t) { return !self; } friend bool operator!=(std::nullptr_t, task const &self) { return !!self; } friend bool operator!=(task const &self, std::nullptr_t) { return !!self; } private: template < class T> using pimpl_t = details::task_pimpl_impl<T, R, Args... > ; template < class F> static auto make_pimpl(F && f) { using dF = std::decay_t<F> ; using pImpl_t = pimpl_t<dF> ; return std::make_unique<pImpl_t> (std::forward<F> (f)); } std::unique_ptr<details::task_pimpl<R, Args... >> m_pImpl; template < class T> T* target_impl() const { return dynamic_cast<pimpl_t<T> *> (m_pImpl.get()); } }; ربما تود إضافة تحسين للمخزن المؤقت الصغير (Small Buffer Optimization) لهذه المكتبة كي لا تخزِّن كل الاستدعاءات في الكومة (heap). وستجد أنك محتاج إلى استخدام ‎task(task&&)‎ غير الافتراضية من أجل إضافة ذلك التحسين، كذلك ستحتاج إلى ‎std::aligned_storage_t‎ داخل الصنف، ومؤشّر حصري ‎unique_ptr‎ يشير إلى ‎m_pImpl‎ مع حاذف (deleter) يمكن ضبطه على خاصّية التدمير فقط (مع عدم إعادة الذاكرة إلى الكومة). أيضًا، ستحتاج إلى كتابة emplace_move_to( void* ) = 0‎ في ‎task_pimpl‎. انظر هذا المثال الحي (بدون خوارزمية تحسين المخزن المؤقت الصغير SBO). الشطب إلى نوع نمطي مع جدول (vtable) وهمي تعتمد C++‎ على ما يُعرف بالأنواع النمطية - Regular types - (أو على الأقل شبه النمطية - Pseudo-Regular). والنوع النمطي هو نوع يمكن إنشاؤه والإسناد إليه أو منه عبر النسخ أو النقل، ويمكن تدميره، ويمكن مقارنته عبر معامل المساواة. ويمكن أيضًا أن يُنشأ بدون وسائط، كما يدعم بعض العمليات الأخرى المفيدة في خوارزميات وحاويات المكتبة القياسية ‎std‎. اطلع إن شئت على هذا الرابط الأجنبي إلى الورقة الأصلية التي تأسس عليها هذا المفهوم. قد ترغب في إضافة دعم لـ ‎std::hash‎ في C++‎ 11. وهنا، سنستخدم منهج الجدول الوهمي vtable لأجل شطب النوع (type erasure). using dtor_unique_ptr = std::unique_ptr<void, void(*)(void*) > ; template < class T, class...Args > dtor_unique_ptr make_dtor_unique_ptr(Args && ...args) { return { new T(std::forward<Args> (args)...), [](void *self) { delete static_cast<T*> (self); } }; } struct regular_vtable { void(*copy_assign)(void *dest, void const *src); // T&=(T const&) void(*move_assign)(void *dest, void *src); // T&=(T&&) bool(*equals)(void const *lhs, void const *rhs); // T const&==T const& bool(*order)(void const *lhs, void const *rhs); // std::less<T>{}(T const&, T const&) std::size_t(*hash)(void const *self); // std::hash<T>{}(T const&) std::type_info const &(*type)(); // typeid(T) dtor_unique_ptr(*clone)(void const *self); // T(T const&) }; template < class T> regular_vtable make_regular_vtable() noexcept { return { [](void *dest, void const *src) {*static_cast<T*> (dest) = *static_cast< T const*> (src); }, [](void *dest, void *src) {*static_cast<T*> (dest) = std::move(*static_cast<T*> (src)); }, [](void const *lhs, void const *rhs) { return * static_cast< T const*> (lhs) == *static_cast< T const*> (rhs); }, [](void const *lhs, void const *rhs) { return std::less < T> {}(*static_cast< T const*> (lhs), *static_cast< T const*> (rhs)); }, [](void const *self) { return std::hash < T> {}(*static_cast< T const*> (self)); }, []()->decltype(auto) { return typeid(T); }, [](void const *self) { return make_dtor_unique_ptr<T> (*static_cast< T const*> (self)); } }; } template < class T> regular_vtable const* get_regular_vtable() noexcept { static const regular_vtable vtable = make_regular_vtable<T> (); return &vtable; } struct regular_type { using self = regular_type; regular_vtable const *vtable = 0; dtor_unique_ptr ptr { nullptr, [](void*) {} }; bool empty() const { return !vtable; } template < class T, class...Args > void emplace(Args && ...args) { ptr = make_dtor_unique_ptr<T> (std::forward<Args> (args)...); if (ptr) vtable = get_regular_vtable<T> (); else vtable = nullptr; } friend bool operator==(regular_type const &lhs, regular_type const &rhs) { if (lhs.vtable != rhs.vtable) return false; return lhs.vtable->equals(lhs.ptr.get(), rhs.ptr.get()); } bool before(regular_type const &rhs) const { auto const &lhs = *this; if (!lhs.vtable || !rhs.vtable) return std::less < regular_vtable const*> {}(lhs.vtable, rhs.vtable); if (lhs.vtable != rhs.vtable) return lhs.vtable->type().before(rhs.vtable->type()); return lhs.vtable->order(lhs.ptr.get(), rhs.ptr.get()); } من الناحية الفنية، فإن >friend bool operator التي تستدعي before مطلوبة هنا، نتابع المثال … std::type_info const* type() const { if (!vtable) return nullptr; return &vtable->type(); } regular_type(regular_type && o): vtable(o.vtable), ptr(std::move(o.ptr)) { o.vtable = nullptr; } friend void swap(regular_type &lhs, regular_type &rhs) { std::swap(lhs.ptr, rhs.ptr); std::swap(lhs.vtable, rhs.vtable); } regular_type &operator=(regular_type && o) { if (o.vtable == vtable) { vtable->move_assign(ptr.get(), o.ptr.get()); return * this; } auto tmp = std::move(o); swap(*this, tmp); return * this; } regular_type(regular_type const &o): vtable(o.vtable), ptr(o.vtable ? o.vtable->clone(o.ptr.get()) : dtor_unique_ptr { nullptr, [](void*) {} }) { if (!ptr && vtable) vtable = nullptr; } regular_type &operator=(regular_type const &o) { if (o.vtable == vtable) { vtable->copy_assign(ptr.get(), o.ptr.get()); return * this; } auto tmp = o; swap(*this, tmp); return * this; } std::size_t hash() const { if (!vtable) return 0; return vtable->hash(ptr.get()); } template < class T, std::enable_if_t<!std::is_same<std::decay_t < T>, regular_type> {}, int>* = nullptr > regular_type(T && t) { emplace<std::decay_t < T>> (std::forward<T> (t)); } }; namespace std { template < > struct hash < regular_type> { std::size_t operator()(regular_type const &r) const { return r.hash(); } }; template < > struct less < regular_type> { bool operator()(regular_type const &lhs, regular_type const &rhs) const { return lhs.before(rhs); } }; } هذا مثال حيّ على ذلك. يمكن استخدام مثل هذا النوع النمطي كمفتاح لقاموس ‎std::map‎ أو قاموس غير مرتب ‎std::unordered_map‎، والذي يقبل أيّ كائن نمطي كمفتاح، وستكون قيم القاموس كائنات قابلة للنسخ. مثلًا: std::map<regular_type, std::any> وعلى عكس ‎any‎، فلا يحسِّن النوع النمطي ‎regular_type‎ الذي أنشأناه في المثال أعلاه الكائنات الصغيرة (small object optimization)، ولا يدعم استعادة البيانات الأصلية، لكنّ ليس من الصعب الحصول على النوع الأصلي على أيّ حال. يتطّلب تحسين الكائنات الصغيرة حفظ مخزن مؤقّت مُحاذَى (aligned storage buffer) داخل ‎regular_type‎، وتعديل الحاذف ‎ptr‎ بحذر كي ندمر الكائن دون حذفه. وسنبدأ من ‎make_dtor_unique_ptr‎، ونعلّمه كيفيّة تخزين البيانات في المخزن المؤقّت، ثمّ في الكومة (heap) إذا لم يكن هناك مُتّسع في المخزن المؤقّت. الآلية الأساسية شطب النوع طريقةٌ لإخفاء نوع الكائن عن الشيفرة التي تستخدمه، حتى لو لم يكن مشتقًّا من أحد الأصناف الأساسية الشائعة، ويوفّر جسرًا بين عوالم تعددية الأشكال الساكنة (static polymorphism)، إذ يجب أن يكون النوع معروفًا بشكل تام عند استخدام القوالب في وقت التصريف، لكن لا يلزم أن يكون مُصرّحًا ليتوافق مع واجهة معيّنة عند التعريف، وبين تعددية الأشكال الديناميكية، إذ لا يلزم أن يكون النوع معروفًا بشكل كامل في وقت التصريف عند استخدام الوراثة والدوال الوهميّة، لكن يجب التصريح بأنّه يتوافق مع واجهة معيّنة عند التعريف. توضّح الشيفرة التالية الآلية الأساسية لشطب النوع. #include <ostream> class Printable { public: template < typename T> Printable(T value): pValue(new Value<T> (value)) {} ~Printable() { delete pValue; } void print(std::ostream &os) const { pValue->print(os); } private: Printable(Printable const &) /*in C++1x: =delete */ ; // not implemented غير منفَّذ void operator=(Printable const &) /*in C++1x: =delete */ ; // غير منفَّذ struct ValueBase { virtual~ValueBase() = default; virtual void print(std::ostream &) const = 0; }; template < typename T> struct Value: ValueBase { Value(T const &t): v(t) {} virtual void print(std::ostream &os) const { os << v; } T v; }; ValueBase * pValue; }; وحده التعريف أعلاه من يلزم أن يكون مرئيًا في موقع الاستخدام، تمامًا كما في الأصناف الأساسية ذات الدوال الوهميّة. مثلا: #include <iostream> void print_value(Printable const &p) { p.print(std::cout); } لاحظ أنّ هذا ليس قالبًا، وإنّما دالة عادية لا يلزم التصريح عنها إلا في ملف الترويسة، ويمكن تعريفها في ملف تنفيذ (implementation file) على عكس القوالب، التي يجب أن يكون تعريفها مرئيًا في مكان الاستخدام. كذلك لا يلزم معرفة أي شيء عن ‎Printable‎ في تعريفات الأنواع الحقيقية (concrete types)، باستثناء أن يكون متوافقًا مع الواجهة كما هو الحال مع القوالب: struct MyType { int i; }; ostream &operator<<(ostream &os, MyType const &mc) { return os << "MyType {" << mc.i << "}"; } يمكننا الآن تمرير كائن من هذا الصنف إلى الدالّة المُعرَّفة أعلاه: MyType foo = { 42 }; print_value(foo); شطب النوع إلى مخزن مؤقّت متجاور يضمّ عناصر من النوع T لا يستدعي شطبُ الأنواعِ الوراثةَ الوهمية بالضرورة أو تخصيصات الذاكرة أو حتى مؤشّرات الدوالّ، وما يميّزه أنّه يصف مجموعة من السلوكيات ويأخذ أيّ نوع يدعم تلك السلوكيات ويغلّفه، أمّا السلوكيات الأخرى التي تميّز ذلك النوع غير الموجودة في تلك المجموعة "فتُنسى" أو "تُشطب". تأخذ ‎array_view‎ مجالًا (range) أو نوع حاوية وتشطب كلّ شيء باستثناء حقيقة أنّه مخزن مؤقّت متجاور يحتوي عناصر من النوع ‎T‎. // SFINAE سمة مساعدة لقاعدة template < class T> using data_t = decltype(std::declval<T> ().data()); template < class Src, class T> using compatible_data = std::integral_constant<bool, std::is_same<data_t<Src>, T*> {} || std::is_same<data_t < Src>, std::remove_const_t<T> *> {} > ; template < class T> struct array_view { // نواة الصنف T *b = nullptr; T *e = nullptr; T* begin() const { return b; } T* end() const { return e; } // توفير التوابع المتوقّعة من مجال متجاور T* data() const { return begin(); } bool empty() const { return begin() == end(); } std::size_t size() const { return end() - begin(); } T &operator[](std::size_t i) const { return begin()[i]; } T &front() const { return* begin(); } T &back() const { return *(end() - 1); } // مساعدات مفيدة لتوليد مجالات أخرى من هذا المجال بشكل سريع وآمن array_view without_front(std::size_t i = 1) const { i = (std::min)(i, size()); return { begin() + i, end() }; } array_view without_back(std::size_t i = 1) const { i = (std::min)(i, size()); return { begin(), end() - i }; } array_view هو منسق بيانات بصيغة البيانات القديمة، لذا النسخة الافتراضية: … array_view(array_view const &) = default; // empty range توليد مجال فارغ array_view() = default; // المنشئ النهائي array_view(T *s, T *f): b(s), e(f) {} array_view(T *s, std::size_t length): array_view(s, s + length) {} منشئ sfinae، يأخذ أي حاوية تدعم ()data. أو مدىً (range) آخَر في خطوة واحدة. … template < class Src, std::enable_if_t<compatible_data<std::remove_reference_t<Src> &, T> {}, int>* = nullptr, std::enable_if_t<!std::is_same<std::decay_t < Src>, array_view> {}, int>* = nullptr > array_view(Src && src): array_view(src.data(), src.size()) {} // منشئ مصفوفات template<std::size_t N> array_view(T(&arr)[N]): array_view(arr, N) {} // قائمة مهيئ template < class U, std::enable_if_t<std::is_same<const U, T> {}, int>* = nullptr > array_view(std::initializer_list<U> il): array_view(il.begin(), il.end()) {} }; تأخذ ‎array_view‎ أيّ حاوية تدعم تابعَ ‎.data()‎ يعيد مؤشّرًا إلى النوع ‎T‎، وتابعَ ‎.size()‎ آخر أو مصفوفة، ثم تشطبها لتصبح مجالًا عشوائيّ الوصول (random-access range) إلى عناصر متجاورة من النوع ‎T‎. ‎array_view‎ يمكن أن تأخذ ‎std::vector<T>‎ أو ‎std::string<T>‎ أو ‎std::array<T, N>‎ أو ‎T[37]‎ أو قائمة مهيئ (initializer list)، بما في ذلك تلك القوائم المبنية بـ ‎{}‎، أو أيّ شيءٍ آخر يدعمها (عبر ‎T* x.data()‎ و ‎size_t x.size()‎). هنا في هذه الحالة، ستعني البيانات التي نستطيع استخراجها من الشيء الذي نشطبه إضافة إلى الحالة غير المالكة (non-owning state) الخاصة بنا أننا لسنا مضطرين إلى تخصيص ذاكرة أو كتابة دوال مخصّصة تعتمد على النوع. انظر هذا المثال الحيّ للتوضيح. قد يكون أحد التحسينات الممكنة هو استخدام ‎data‎ و ‎size‎ غير أعضاء (non-member) في سياق تمكين البحث القائم على الوسائط (ADL). شطب النوع عبر std::any يستخدم هذا المثال C++‎ 14 و ‎boost::any‎. أمّا في C++‎ 17، فيمكنك استخدام ‎std::any‎ بدلاً من ذلك. const auto print = make_any_method<void(std::ostream&)>([](auto&& p, std::ostream& t){ t << p << "\n"; }); super_any < decltype(print) > a = 7; (a->*print)(std::cout); يعتمد هذا المثال على العمل الذي قام به ‎‎@dyp و ‎‎@cpplearner، مع مساهمةٍ من آدم نيفراومونت. سنستخدم أولاً وسمًا لتمرير الأنواع: template < class T > struct tag_t { constexpr tag_t() {}; }; template < class T > constexpr tag_t<T> tag {}; يحصل هذا الصنف على البصمة (signature) المُخزّنة باستخدام ‎any_method‎، وينشئ هذا نوع مؤشّر دالة (function pointer type)، ومُنتِجًا - factory - لمؤشّرات الدوال المذكورة: template<class any_method> using any_sig_from_method = typename any_method::signature; template<class any_method, class Sig=any_sig_from_method<any_method>> struct any_method_function; template<class any_method, class R, class...Args> struct any_method_function<any_method, R(Args...)> { template < class T> using decorate = std::conditional_t< any_method::is_const, T const, T >; using any = decorate<boost::any > ; using type = R(*)(any&, any_method const*, Args&&...); template < class T> type operator()(tag_t < T>) const { return +[](any& self, any_method const* method, Args&&...args) { return (*method)( boost::any_cast<decorate<T>&>(self), decltype(args)(args)... ); }; } }; any_method_function::type - يمثّل نوع مؤشّر الدالّة الذي سنخزّنه بجانب النُسخة. any_method_function::operator()‎ - يأخذ tag_t<T>‎ ويكتب نُسخةً مخصّصة من النوع any_method_function::type، والتي تفترض أنّ ‎any‎ سيساوي ‎T‎. نحن نريد أن نشطب نوع عدّة توابع في الوقت نفسه، لذا سنجمعها في صفوف (tuples) ونكتب مغلِّفًا (wrapper) مساعدًا لتثبيت الصفّ في موقع تخزين ساكن (static storage) لكل نوع على حدة، مع الحفاظ على مؤشّر يشير إليها. template<class...any_methods> using any_method_tuple = std::tuple< typename any_method_function<any_methods>::type... >; template<class...any_methods, class T> any_method_tuple<any_methods...> make_vtable( tag_t<T> ) { return std::make_tuple( any_method_function<any_methods>{}(tag<T>)... ); } template < class...methods > struct any_methods { private: any_method_tuple < methods... > const *vtable = 0; template < class T> static any_method_tuple<methods...> const* get_vtable( tag_t<T> ) { static const auto table = make_vtable<methods...>(tag<T>); return &table; } public: any_methods() = default; template < class T> any_methods(tag_t < T>): vtable(get_vtable(tag < T>)) {} any_methods& operator=(any_methods const&)=default; template < class T> void change_type( tag_t<T> ={} ) { vtable = get_vtable(tag<T>); } template < class any_method> auto get_invoker(tag_t<any_method> = {}) const { return std::get < typename any_method_function<any_method>::type > (*vtable); } }; يمكننا تخصيص هذا للحالات التي يكون فيها الجدول الوهمي vtable صغيرًا، على سبيل المثال إن كان مؤلفًا من عنصر واحد، واستخدام المؤشّرات المباشرة المخزّنة في الصنف في تلك الحالات لتحسين الكفاءة. سنستخدم ‎super_any_t‎ لتيسير تصريح ‎super_any‎. template < class...methods > struct super_any_t; يبحث هذا في التوابع التي يدعمها super_any لأجل تطبيق قاعدة "فشل التعويض ليس خطأ أو (SFINAE)" وكذلك تحسين رسائل الخطأ: template<class super_any, class method> struct super_method_applies_helper : std::false_type {}; template<class M0, class...Methods, class method> struct super_method_applies_helper<super_any_t<M0, Methods...>, method> : std::integral_constant<bool, std::is_same<M0, method>{} || super_method_applies_helper<super_any_t<Methods...>, method>{}> {}; template<class...methods, class method> auto super_method_test( super_any_t<methods...> const&, tag_t<method> ) { return std::integral_constant<bool, super_method_applies_helper< super_any_t<methods...>, method >{} && method::is_const >{}; } template<class...methods, class method> auto super_method_test( super_any_t<methods...>&, tag_t<method> ) { return std::integral_constant<bool, super_method_applies_helper< super_any_t<methods...>, method >{} >{}; } template<class super_any, class method> struct super_method_applies: decltype( super_method_test( std::declval<super_any>(), tag<method> ) ) {}; بعد ذلك سننشئ النوع ‎any_method‎، وهو عبارة عن مؤشّر إلى تابع زائف (pseudo-method-pointer). سنجعله عامًّا (globally) و ثابتًا (‎const‎) باستخدام الصياغة التالية: const auto print = make_any_method([](auto && self, auto && os) { os << self; }); و في C++‎ 17: const any_method print =[](auto && self, auto && os) { os << self; }; لاحظ أنّ عدم استخدام تعابير لامدا قد يتسبّب في فوضى إذ أنّنا نستخدم النوع في البحث، يمكن إصلاح هذا الخلل، لكنّ ذلك سيجعل هذا المثال أطول ممّا هو عليه. وبشكل عام، يجب أن تهيئ توابع any بواسطة تعبير لامدا، أو صنف ذي معامِلات غير محددة النوع (Parameterised type) في لامدا. template < class Sig, bool const_method, class F> struct any_method { using signature = Sig; enum { is_const = const_method }; private: F f; public: template < class Any, // تطابق هذا النوع Anys من أنّ أحد كائنات SFINAE تتحقق قاعدة std::enable_if_t<super_method_applies < Any &&, any_method> {}, int>* = nullptr > friend auto operator->*( Any&& self, any_method const& m ) { لا تستخدم قيمة any_method إذ لكل تابع any_method نوعًا خاصًا، وتحقق أن لكل عنصر *auto في super_any مؤشرًا يشير إليك. ثم أرسل إلى any_method_data، تابع … m](auto&&...args)->decltype(auto) { return invoke( decltype(self)(self), &m, decltype(args)(args)... ); }; } any_method(F fin): f(std::move(fin)) {} template < class...Args > decltype(auto) operator()(Args&&...args)const { return f(std::forward<Args> (args)...); } }; هذا تابعٌ منتِج (factory method)، لكني لا أراه ضروريًا في C++‎ 17: template<class Sig, bool is_const=false, class F> any_method<Sig, is_const, std::decay_t<F>> make_any_method( F&& f ) { return {std::forward<F>(f)}; } هذه هي ‎any‎ المُعزّزة، فهي من النوع ‎any‎، وتحمل معها حزمة من مؤشّرات دوال شطب النوع (type-erasure function pointers) التي تتغيّر كلما تغيّرت ‎any‎ المحتواة: template < class...methods > struct super_any_t: boost::any, any_methods < methods... > { using vtable = any_methods < methods... > ; public: template < class T, std::enable_if_t< !std::is_base_of<super_any_t, std::decay_t<T>>{}, int> =0 > super_any_t( T&& t ): boost::any(std::forward<T> (t)) { using dT = std::decay_t<T> ; this->change_type(tag < dT>); } boost::any& as_any()&{return *this;} boost::any&& as_any()&&{return std::move(*this);} boost::any const& as_any()const&{return *this;} super_any_t() = default; super_any_t(super_any_t&& o): boost::any(std::move(o.as_any())), vtable(o) {} super_any_t(super_any_t const& o): boost::any(o.as_any()), vtable(o) {} template < class S, std::enable_if_t<std::is_same<std::decay_t<S>, super_any_t> {}, int> = 0 > super_any_t( S&& o ): boost::any(std::forward<S> (o).as_any()), vtable(o) {} super_any_t& operator=(super_any_t&&) = default; super_any_t& operator=(super_any_t const &) = default; template < class T, std::enable_if_t< !std::is_same<std::decay_t<T>, super_any_t>{}, int>* =nullptr > super_any_t& operator=( T&& t ) { ((boost::any&)*this) = std::forward<T>(t); using dT=std::decay_t<T>; this->change_type( tag<dT> ); return *this; } }; تخزين التوابع ‎any_method‎ ككائنات ثابتة (‎const‎) يُسهّل ‎super_any‎ بعض الشيء: template<class...Ts> using super_any = super_any_t< std::remove_cv_t<Ts>... >; شيفرة تختبر ما سبق: const auto print = make_any_method<void(std::ostream&)>([](auto&& p, std::ostream& t){ t << p << "\n"; }); const auto wprint = make_any_method<void(std::wostream&)>([](auto&& p, std::wostream& os ){ os << p << L"\n"; }); int main() { super_any < decltype(print), decltype(wprint) > a = 7; super_any < decltype(print), decltype(wprint) > a2 = 7; (a->*print)(std::cout); (a->*wprint)(std::wcout); } هذا مثال حيّ على ذلك. الأنواع الذرية Atomic Types يمكن استخدام الأنواع الذرية للقراءة والكتابة بأمان في موضع من الذاكرة مشترك بين خيطين، وهذا ما يسمى الوصول متعدّد الخيوط Multi-threaded Access. انظر المثال التالي على نموذج سيء ويحتمل أن يتسبب في مشكلة سباق بيانات (Data Race)، ستضيف الدالة كل القيم الموجودة بين a و b إلى result: #include <thread> #include <iostream> void add(int a, int b, int *result) { for (int i = a; i <= b; i++) { *result += i; } } int main() { // نوع بيانات أولي غير مؤمَّن خيطيًا int shared = 0; هنا، أنشئ خيطًا يمكنه العمل بشكل موازي للخيط الرئيسي، لينفّذ دالة add المعرَّفة أعلاه بحيث يكون معامِل a يساوي 1، وb يساوي 100، والنتيجة result تساوي shared&. هذا مماثل لـ (;add, 1, 100, &shared) انظر … std::thread addingThread(add, 1, 100, &shared); حاول طباعة قيمة shared إلى الشاشة، سيكرر الخيط الرئيسي ذلك إلى أن يصبح addingThread قابلًا للضم، انظر… while (!addingThread.joinable()) { قد يتسبب هذا في سلوك غير محدد أو في طباعة قيمة غير صالحة إن حاول addingThread كتابة shared أثناء قراءة الخيط الرئيسي لها، … std::cout << shared << std::endl; } // إعادة ضم الخيط عند نهاية التنفيذ لأغراض التنظيف addingThread.join(); return 0; } قد يتسبب المثال أعلاه في قراءة خاطئة وقد يؤدّي إلى سلوك غير مُحدّد. انظر المثال التالي على استخدام آمن للخيوط (thread safety)، ستضيف الدالة كل القيم الواقعة بين a و b إلى result: #include <atomic> #include <thread> #include <iostream> void add(int a, int b, std::atomic<int> *result) { for (int i = a; i <= b; i++) { // تلقائيا result إلى 'i' إضافة result->fetch_add(i); } } int main() { // استخدام قالب ذري لتخزين كائنات غير ذرية std::atomic<int> shared = 0; هنا، أنشئ خيطًا يمكنه العمل بشكل موازي للخيط الرئيسي، لينفّذ دالة add المعرَّفة أعلاه بحيث يكون معامِل a يساوي 1، وb يساوي 100، والنتيجة result تساوي shared&. هذا مماثل لـ (;add, 1, 100, &shared) انظر … std::thread addingThread(add, 1, 100, &shared); حاول طباعة قيمة shared إلى الشاشة، سيكرر الخيط الرئيسي ذلك إلى أن يصبح addingThread قابلًا للضم، انظر… while (!addingThread.joinable()) { // طريقة آمنة لقراءة القيمة المشارَكة من أجل قراءة آمنة خيطيًا. std::cout << shared.load() << std::endl; } // إعادة ضم الخيط عند نهاية التنفيذ لأغراض التنظيف addingThread.join(); return 0; } المثال أعلاه آمن لأنّ جميع عمليات ‎store()‎ و ‎load()‎ الخاصّة بالبيانات الذرية (‎atomic‎) تحمي العدد ‎int‎ المغلّف من محاولات الوصول الآني (simultaneous access). تحويلات النوع الصريحة Explicit type conversions يمكن تحويل تعبير بشكل صريح إلى نوع ‎T‎ باستخدام ‎dynamic_cast<T>‎ أو ‎static_cast<T>‎ أو ‎reinterpret_cast‎ أو ‎const_cast‎ ، وذلك اعتمادًا على النوع الذي تريد التحويل إليه. كذلك تدعم C++‎ صيغة التحويل الدالّية - function-style cast -‏ ‎T(expr)‎، وصيغة التحويل على نمط C ‏‎(T)expr‎. التحويل على نمط C سُمّي كذلك لأنّه التحويل الوحيد الذي يمكن استخدامه في C. وصياغته على الشكل التالي ‎(NewType)variable‎. يستخدم هذا التحويل أحد تحويلات C++‎ التالية (بالترتيب): const_cast<NewType>(variable)‎ static_cast<NewType>(variable)‎ const_cast<NewType>(static_cast<const NewType>(variable))‎ reinterpret_cast<const NewType>(variable)‎ const_cast<NewType>(reinterpret_cast<const NewType>(variable))‎ صيغة التحويل الدوالي مشابهة جدًا رغم وجود بعض القيود الناتجة عن الصيغة: ‎NewType(expression)‎، ونتيجة لذلك لا يمكن التحويل إلّا إلى الأنواع التي لا تحتوي على مسافات فارغة. يُفضّل استخدام تحويل C++‎ الجديد لأنّه أسهل قراءةً ويمكن رصده بسهولة داخل شيفرة C++‎ المصدرية، كما يمكن رصَد الأخطاء في وقت التصريف وليس في وقت التشغيل. ونظرًا لأنّ هذا التحويل قد ينتج عنه ‎reinterpret_cast‎ غير مقصود، فإنّه غالبًا ما يُعدُّ خطيرًا. التخلّص من الثباتية Casting away constness يمكن تحويل مؤشّر-إلى-كائن-ثابت إلى مؤشّر-إلى-كائن-غير-ثابت باستخدام الكلمة المفتاحية ‎const_cast‎، وسنستخدمها لاستدعاء دالّة غير صحيحة ثباتيًا (const-correct) ولا تقبل إلّا وسيطا غير ثابت من النوع ‎char*‎، رغم أنّها لا تكتُب في المؤشّر أبدًا: void bad_strlen(char*); const char *s = "hello, world!"; bad_strlen(s); // خطأ تصريفي bad_strlen(const_cast<char*> (s)); في الشيفرة السابقة، يفضل جعل bad_strlen تقبل معامِل *const char. يمكن أن يُستخدم إشارة ‎const_cast‎ إلى نوع معيّن في تحويل قيمة يسارية مُؤهّلة ثباتيًا (const-qualified) إلى قيمة يمينية غير مؤهّلة ثباتيًا. لكن يكتنف استخدام‎const_cast‎ بعض الخطورة لأنّه يجعل من المستحيل على نظام الأنواع في C++‎ أن يمنعك من محاولة تعديل كائن ثابت. وهو أمر سيؤدّي إلى سلوك غير مُحدّد. const int x = 123; int& mutable_x = const_cast< int&> (x); mutable_x = 456; // قد يُصرَّف، لكنه قد يُنتِجُ سلوكًا غير محدد التحويل من صنف أساسي إلى صنف مشتق منه يمكن تحويل مؤشّر لصنف أساسي إلى مؤشّر لصنف مشتق منه باستخدام ‎static_cast‎، ولا تُجري ‎static_cast‎ أيّ تحقّق في وقت التشغيل، لذلك قد يحدث سلوك غير مُحدّد في حال كان المؤشّر يشير إلى نوعٍ غير النوع المطلوب. struct Base {}; struct Derived : Base {}; Derived d; Base* p1 = &d; Derived* p2 = p1; // خطأ، لا بد من التحويل Derived* p3 = static_cast<Derived*>(p1); // يشير الآن إلى الكائن المشتق p2 لا بأس فـ Base b; Base* p4 = &b; Derived* p5 = static_cast<Derived*>(p4); // p4 سلوك غير محد، لأن // لا يشير إلى كائن مشتق وبالمثل، يمكن تحويل مرجع يشير إلى الصنف الأساسي إلى مرجع يشير إلى صنف مشتق باستخدام ‎static_cast‎. struct Base {}; struct Derived : Base {}; Derived d; Base& r1 = d; Derived& r2 = r1; // خطأ، لا بدّ من التحويل Derived& r3 = static_cast<Derived&>(r1); // يشير الآن إلى الكائن المشتق p3 لا بأس فـ إذا كان النوع المصدري متعدد الأشكال فيمكن استخدام ‎dynamic_cast‎ للتحويل من الصنف الأساسي إلى صنف مشتقّ منه، إذ أنّها تُجري فحصًا في وقت التشغيل، ويمكن معالجة الفشل هذه المرّة بدلاً من حدوث سلوك غير محدّد. أما إن كان مؤشّرًا، سيُعاد مؤشّر فارغ عند الفشل. وفي حال كان مرجعًا، سيُطرَح استثناء عند فشل ‎std::bad_cast‎ (أو صنف مشتق من ‎std::bad_cast‎). struct Base { virtual~Base(); }; // هي بنية متعدد الأشكالBase struct Derived: Base {}; Base* b1 = new Derived; Derived* d1 = dynamic_cast<Derived*>(b1); // يشير الآن إلى الكائن المشتق p1 لا بأس فـ Base* b2 = new Base; Derived* d2 = dynamic_cast<Derived*>(b2); // هو مؤشر فارغ d2 التحويل بين المؤشرات والأعداد الصحيحة يمكن تحويل مؤشّر كائن (بما في ذلك ‎void*‎) أو مؤشّر دالّة إلى نوع عددي صحيح باستخدام ‎reinterpret_cast‎، ولن يُصرَّف هذا إلّا إذا كان النوع المقصود طويلاً بما فيه الكفاية. تتعلّق النتيجة بالتنفيذ، وتعيد في العادةً العنوان العددي للبايت الذي يشير إليه المؤشّر في الذاكرة. وعمومًا فالنوعين ‎long‎ و ‎unsigned long‎ طويلان بما يكفي لحفظ أيّ قيمة للمؤشّر، لكنّ هذا غير مضمون من قبل المعيار. الإصدار ≥ C++‎ 11 في حال وجود النوعين ‎std::intptr_t‎ و ‎std::uintptr_t‎ فإننا نضمن أن يكفي طولهما لاحتواء ‎void*‎ (ومن ثم أيّ مؤشّر إلى نوع كائن)، لكن لا نضمن أن يكفي طولهما لاحتواء مؤشّر دالّة. وبالمثل، يمكن استخدام ‎reinterpret_cast‎ لتحويل نوع عددي صحيح إلى نوع مؤشّر. ومرّة أخرى، فالنتيجة تتعلّق بالتنفيذ، غير أنّه يُضمَن ألّا يتم تغيير قيمة المؤشّر عن طريق تقريب (round-trip) نوع عددي صحيح، كما لا يضمن المعيار أنّ القيمة صفر ستُحوّل إلى مؤشّر فارغ. void register_callback(void (*fp)(void*), void* arg); // C على الأرجح واجهة برمجة تطبيقات للغة void my_callback(void* x) { std::cout << "the value is: " <<reinterpret_cast<long> (x); // ستُصرَّف على الأرجح } long x; std::cin >> x; register_callback(my_callback, reinterpret_cast<void*> (x)); // نأمل عدم فقدان أيّ معلومات التحويل عبر مُنشئ صريح أو دالة تحويل صريحة التحويلات التي تشمل استدعاء مُنشئ صريح أو دالّة تحويل لا يمكن إجراؤها ضمنيًا، لكن نستطيع طلب إجراء التحويل بشكل صريح باستخدام ‎static_cast‎، ذلك يشبه التهيئة المباشرة (direct initialization) باستثناء أنّ النتيجة تكون مؤقّتة. class C { std::unique_ptr<int> p; public: explicit C(int* p): p(p) {} }; void f(C c); void g(int* p) { f(p); // error: C::C(int*) is explicit f(static_cast<C> (p)); // ok f(C(p)); // يكافئ السطر الماضي C c(p); f(c); // error: C is not copyable } التحويل الضمني Implicit conversion تستطيع أن تُجري ‎static_cast‎ أيّ تحويل ضمني، وقد يكون هذا مفيدًا أحيانًا كما في الأمثلة التالية: عند تمرير الوسائط إلى علامة حذف (ellipsis)، لن يكون نوع الوسيط "المُتوقَّع" معروفًا بشكل ساكن (statically known)، لذا لن يحدث أي تحويل ضمني. const double x = 3.14; printf("%d\n", static_cast<int> (x)); // 3 // printf("%d\n", x); // تتوقع عددا صحيحا هنا printf سلوك غير محد، لأنّ // حل بديل // const int y = x; printf("%d\n", y); بدون التحويل الصريح للنوع، سيُمرَّر كائن من النوع ‎double‎ إلى علامة الحذف وسيحدث سلوك غير مُحدّد. يمكن لمُعامل الإسناد الخاصّ بصنف مشتقّ أن يستدعي مُعامل الإسناد الخاصّ بصنفه الأساسي على النحو التالي: struct Base { /*... */ }; struct Derived: Base { Derived& operator=(const Derived& other) { static_cast<Base&>(*this) = other; // :حل بديل // Base& this_base_ref = *this; this_base_ref = other; } }; تحويل التعدادات Enum conversions يمكن أن تجري ‎static_cast‎ عمليّة تحويل من نوع عددي صحيح أو عشري إلى نوع تعدادي (سواء كان نطاقًيا - scoped - أو غير نطاقي - unscoped) والعكس صحيح، كما أنّها تحوّل بين أنواع التعدادات. *التحويل من نوع تعداد غير نطاقي إلى نوع حسابي (arithmetic type) يُعدُّ تحويلًا ضمنيًا؛ وهو جائز لكنه غير ضروري لاستخدام ‎static_cast‎. الإصدار ≥ C++‎ 11 عند تحويل نوع تعداد نطاقي إلى نوع حسابي: إذا كان من الممكن تمثيل قيمة التعداد في النّوع المقصود بشكل تامّ، فإنّ النتيجة ستساوي تلك القيمة. وإلا، ستكون النتيجة غير مُحدّدة إن كان النوع المقصود نوعًا صحيحًا. بخلاف ذلك، إذا كان النوع المقصود نوعًا عشريا (floating point type)، فإنّ النتيجة ستساوي نتيجة التحويل إلى النوع الأساسي، ثم منه إلى نوع الأعداد العشرية. انظر: enum class Format { TEXT = 0, PDF = 1000, OTHER = 2000, }; Format f = Format::PDF; int a = f; // error int b = static_cast<int> (f); // يساوي 1000 b char c = static_cast<char> (f); // char غير محدد في حال لم يتناسب 1000 مع النوع double d = static_cast<double> (f); // يساوي على الأرجح 1000.0 d عند تحويل عدد صحيح أو نوع تعداد إلى نوع تعداد: إذا كانت القيمة الأصلية ضمن مجال (range) التعداد المقصود، فإنّ النتيجة ستساوي تلك القيمة. لاحظ أنّ هذه القيمة قد تختلف من عدّاد (enumerator) لآخر. وإلا، فإنّ النتيجة ستكون غير محددة (unspecified) في ‏(C++‎ 14<=)، أو غير معرَّفة (undefined) في ‏(C++‎ 17>=). انظر المثال التالي: enum Scale { SINGLE = 1, DOUBLE = 2, QUAD = 4 }; Scale s1 = 1; // خطأ Scale s2 = static_cast<Scale> (2); // DOUBLE من النوع s2 Scale s3 = static_cast<Scale> (3); // تساوي 3، وهي غير مساوية لأيّ عدّاد s3 قيمة Scale s9 = static_cast<Scale> (9); // C++17 سلوك غير محدد في // C++14 وقيمة غير موصوفة في الإصدار ≥ C++‎ 11 عند تحويل نوع عشري إلى نوع تعداد، فإنّ النتيجة ستكون مساوية لنتيجة التحويل إلى النوع الأساسي للتعداد ثمّ إلى نوع التعداد. enum Direction { UP = 0, LEFT = 1, DOWN = 2, RIGHT = 3, }; Direction d = static_cast<Direction> (3.14); تحويل المؤشّرات العضوية يمكن تحويل مؤشّر يشير إلى عضو من صنف مشتق، إلى مؤشّر يشير إلى عضو من صنفه الأساسي، باستخدام ‎static_cast‎، لكن يجب أن تتطابق الأنواع المشار إليها. وإذا كان العامل مؤشّرًا عضويًا فارغًا فإنّ النتيجة ستكون أيضًا مؤشّرًا عضويًا فارغًا. ولن يكون التحويل صالحًا خلاف ذلك، إلّا إذا كان العضو المُشار إليه عبر المعامَل موجودًا بالفعل في الصنف المقصود، أو كان الصنف المقصود صنفًا أساسيًا أو مشتقًّا من الصنف الذي يحتوي على العضو الذي يشير إليه المعامَل، إذ لا تتحقّق ‎static_cast‎ من الصحّة (validity). ولن يكون السلوك الناتج محددًا إن لم يكن التحويلُ صالحًا. struct A {}; struct B { int x; }; struct C: A, B { int y; double z; }; int B::*p1 = &B::x; int C::*p2 = p1; // حسنا، تحويل ضمني int B::*p3 = p2; // خطأ int B::*p4 = static_cast<int B::*>(p2); // p1 يساوي p4 حسنا ما يلي غير معرَّف، إذ يشير p2 إلى x وهو عضو من الصنف B وهو غير ذي صلة هنا، نتابع …. int A::*p5 = static_cast<int A::*>(p2); double C::*p6 = &C::z; double A::*p7 = static_cast<double A::*>(p6); // z لا تحتوي A لا بأس، رغم أنّ int A::*p8 = static_cast<int A::*>(p6); // error: types don't match التحويل من *void‎‎ إلى *T‎‎ في C++‎، لا يمكن تحويل ‎void*‎ ضمنيًا إلى ‎T*‎ إن كان ‎T‎ يمثّل نوعَ كائن، بل يجب استخدام ‎static_cast‎ لإجراء التحويل بشكل صريح. وإذا كان العامل يشير فعليًا إلى كائن ‎T‎، فإنّ النتيجة ستشير إلى ذلك الكائن، وإلا فإنّ النتيجة ستكون غير مُحدّدة. الإصدار ≥ C++‎ 11 وحتى لو لم يكن العامل يشير إلى كائن ‎T‎، فما دام المعامَل يشير إلى بايت تمت محاذاة عنوانه بشكل صحيح مع النوع ‎T‎، فإنّ نتيجة التحويل ستشير إلى نفس البايت. // تخصيص مصفوفة من 100 عدد صحيح بالطريقة الصعبة. int* a = malloc(100*sizeof(*a)); // error; malloc returns void* int* a = static_cast<int*>(malloc(100*sizeof(*a))); // حسنا // int* a = new int[100]; // لا حاجة للتحويل // std::vector<int> a(100); // هذا أفضل const char c = '!'; const void* p1 = &c; const char* p2 = p1; // خطأ const char* p3 = static_cast<const char*>(p1); // c يشير إلى p3 حسنا const int* p4 = static_cast<const int*>(p1); السطر السابق غير معرَّف في C++03، وقد يكون غير معرَّف في C++11 أيضًا إن كان (alignof(int أكبر من (alignof(int، بقية المثال … char* p5 = static_cast<char*>(p1); // error: casting away constness تحويل مواراة النوع Type punning conversion يمكن تحويل مؤشّر (مرجع) يشير إلى نوع كائن، إلى مؤشّر (مرجع) يشير إلى أيّ نوع كائن آخر باستخدام ‎reinterpret_cast‎ دون الحاجة إلى أيّ منشِئات أو دوالّ تحويل. int x = 42; char* p = static_cast<char*>(&x); // error: static_cast cannot perform this conversion char* p = reinterpret_cast<char*>(&x); // حسنا *p = 'z'; // x ربما يغيّر هذا الإصدار ≥ C++‎ 11 تمثّل نتيجة ‎reinterpret_cast‎ نفس العنوان الذي يمثّله العامل، شرط أن تتناسب محاذاة العنوان مع النوع المقصود، وتكون النتيجة غير مُحدّدة في خلاف ذلك. int x = 42; char& r = reinterpret_cast<char&>(x); const void* px = &x; const void* pr = &r; assert(px == pr); الإصدار < C++‎ 11 نتيجة ‎reinterpret_cast‎ غير مُحدّدة، إلّا أنّ المؤشّر (المرجع) سيتجاوز التقريب (round trip) من نوع المصدر إلى نوع الوجهة، والعكس صحيح طالما أن متطلّبات محاذاة النوع المقصود ليست أكثر صرامة من نظيرتها في نوع المصدر. int x = 123; unsigned int& r1 = reinterpret_cast<unsigned int&>(x); int& r2 = reinterpret_cast<int&>(r1); r2 = 456; // 456 إلى x تعيين لا تغيّر ‎reinterpret_cast‎ العنوانَ في معظم التنفيذات، وهو أمر لم يُعتمد معيارًا حتى الإصدار C++‎ 11. كذلك يمكن استخدام ‎reinterpret_cast‎ للتحويل من نوع مؤشّر بيانات عضوية (pointer-to-data-member type) إلى آخر، أو من نوع مؤشر إلى دالة تابعة (pointer- to-member-function type) إلى آخر. يكتنف استخدام ‎reinterpret_cast‎ بعض الخطورة، لأنّ القراءة أو الكتابة عبر مؤشّر أو مرجع تم الحصول عليه باستخدام ‎reinterpret_cast‎ قد يؤدّي إلى حدوث سلوك غير مُحدّد في حال كان نوعي المصدر والوجهة غير مترابطين. الأنواع غير المسماة Unnamed types الأصناف غير المسماة Unnamed classes على عكس الأصناف أو البنيات المُسمّاة، فإن الأصناف والبنيات غير المُسمّاة يجب استنساخها في موضع تعريفها، ولا يمكن أن يكون لها مُنشئات أو مدمّرات. struct { int foo; double bar; } foobar; foobar.foo = 5; foobar.bar = 4.0; class { int baz; public: int buzz; void setBaz(int v) { baz = v; } } barbar; barbar.setBaz(15); barbar.buzz = 2; استخدام أنواع الأصناف ككُنى للنوع (type aliases) يمكن أيضًا استخدام أنواع الأصناف غير المسماة عند إنشاء كُنى (alias) للنوع عبر ‎typedef‎ و ‎using‎: الإصدار < C++‎ 11 using vec2d = struct { float x; float y; }; typedef struct { float x; float y; } vec2d; vec2d pt; pt.x = 4. f; pt.y = 3. f; الأعضاء المجاهيل Anonymous members تسمح المصرّفات الشهيرة باستخدام الأصناف كأعضاء مجهولين، لكن هذا غير قياسي في لغة C++‎. struct Example { struct { int inner_b; }; int outer_b; // يمكن الوصول إلى أعضاء الهيكل المجهولة كما لو كانت أعضاءً من الهيكل الأب Example(): inner_b(2), outer_b(4) { inner_b = outer_b + 2; } }; Example ex; // نفس الشيء بالنسبة للشيفرات الخارجية التي تشير إلى البنية ex.inner_b -= ex.outer_b; الاتحادات المجهولة Anonymous Unions تنتمي أسماء الأعضاء الخاصة باتحاد مجهول (anonymous union) إلى نطاق تصريح الاتّحاد، ويجب أن تكون مميّزة عن جميع الأسماء الأخرى الموجودة في ذلك النطاق. يستخدم المثال التالي نفس الإنشاء (Construction) الذي استخدمناه في مثال الأعضاء المجهولين أعلاه، ولكنّه هذه المرة متوافق مع المعايير. struct Sample { union { int a; int b; }; int c; }; int main() { Sample sa; sa.a = 3; sa.b = 4; sa.c = 5; } سمات النوع Type Traits خاصيات النوع Type Properties الإصدار ≥ C++‎ 11 تقارن خاصيات النوع المُعدِّلات (modifiers) التي يمكن تطبيقها على عدة متغيّرات، لكنّ فائدة تلك السمات لا تكون واضحة في كل حالة. لاحظ أن التحسين الذي يوفّره المثال التالي لن يكون له أثر إلّا إن لم يكن المُصرّف مُحسّنا من الأساس، فهو مجرّد مثال توضيحي. template < typename T> inline T FastDivideByFour(cont T &var) { // unsigned integral سيحدث خطأ في حال لم يكن النوع المُدخَل نوع عدديا صحيحا غير مؤشّر static_assert(std::is_unsigned<T>::value && std::is_integral<T>::value, "This function is only designed for unsigned integral types."); return (var >> 2); } is_const ستُعاد القيمة true إن كان النوع ثابتًا. std::cout << std::is_const<const int >::value << "\n"; // true std::cout <<std::is_const<int>::value << "\n"; // false is_volatile ستُعاد القيمة true إن كان النوع متغيّرًا (volatile). std::cout <<std::is_volatile < static volatile int>::value << "\n"; // true. std::cout << std::is_const<const int >::value << "\n"; // false. is_signed ستُعاد القيمة true إن كان النوع مؤشَّرًا (signed). std::cout <<std::is_signed<int>::value << "\n"; // true. std::cout <<std::is_signed<float>::value << "\n"; // true. std::cout <<std::is_signed < unsigned int>::value << "\n"; // false. std::cout <<std::is_signed<uint8_t>::value << "\n"; // false. is_unsigned ستُعاد القيمة true إن كان النوع غير مؤشّر. std::cout <<std::is_unsigned < unsigned int>::value << "\n"; // true std::cout <<std::is_signed<uint8_t>::value << "\n"; // true std::cout <<std::is_unsigned<int>::value << "\n"; // false std::cout <<std::is_signed<float>::value << "\n"; // false أنواع السمات القياسية Standard type traits الإصدار ≥ C++‎ 11 تحتوي الترويسة ‎type_traits‎ على مجموعة من أصناف القوالب والمساعِدات التي يمكن استخدامها لتحويل خاصيات الأنواع وتفحّصها في وقت التصريف، وتُستخدم هذه السمات عادةً في القوالب للتحقّق من أخطاء المستخدم، ودعم البرمجة العامة، والسماح بتحسينات الأداء. كذلك تُستخدم معظم أنواع السمات للتأكّد ممّا إذا كان النوع يحقّق بعض المعايير. وتُصاغ على النحو التالي: template < class T > struct is_foo; عند استنساخ صنف من القالب بواسطة نوع يحقّق معيارًا ‎foo‎، فإنّ ‎is_foo<T>‎ سيرث من ‎std::integral_constant<bool,true>‎ (يُعرَف أيضًا باسم std::true_type)، وإن كان غير ذلك فإنّه يرث من std::integral_constant<bool,false>‎ ( ويعرف أيضًا باسم std::false_type). هذا سيمنح للسمة الأعضاء التالية: الثوابت static constexpr bool value تعيد true إن كان T يحقّق المعيار foo، وتعيد ‎false‎ خلاف ذلك. الدوال operator bool تعيد ‎value‎ الإصدار ≥ C++‎ 14 bool operator()‎ تعيد ‎value‎ الأنواع الاسم التعريف value_type bool type std::integral_constant<bool,value>‎ table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; } يمكن استخدام السمة في بنيات مثل ‎static_assert‎ أو ‎std::enable_if‎. المثال التالي يستخدم std::is_pointer: template < typename T> void i_require_a_pointer(T t) { static_assert(std::is_pointer<T>::value, "T must be a pointer type"); } // نوعًا مؤشّرًا T التحميل الزائد في حال لم يكن template < typename T> typename std::enable_if<!std::is_pointer<T>::value>::type does_something_special_with_pointer(T t) { // افعل شيئا عاديا } // نوعًا مؤشّرًا T التحميل الزائد في حال لم يكن template < typename T> typename std::enable_if<std::is_pointer<T>::value>::type does_something_special_with_pointer(T t) { // افعل شيئا خاصا } هناك المزيد من السمات التي تُحوّل الأنواع، مثل ‎std::add_pointer‎ و ‎std::underlying_type‎، وتعرض هذه السمات عمومًا نوع عضو يُسمَّى ‎type‎ يحتوي النوعَ المُحوَّل، كما في std::add_pointer<int>::type is int*‎. std::is_same‎‎ الإصدار ≥ C++‎ 11 تُستخدم دالة العلاقة بين الأنواع - type relation‏ - ‎std::is_same<T, T>‎ لمقارنة نوعين، إذ تعيد true إذا كان النوعان متساويين، و تعيد false خلاف ذلك. في المثال التالي، سيطبع السطر الأول true في أغلب مصرِّفات x86 و x86_64، بينما يطبع السطران الثاني والثالث false في جميع المصرفات. std::cout <<std::is_same<int, int32_t>::value << "\n"; std::cout <<std::is_same<float, int>::value << "\n"; std::cout <<std::is_same < unsigned int, int>::value << "\n"; تعمل علاقة الأنواع ‎std::is_same‎ بغض النظر عن التعريفات النوعية typedefs، وهو مُبيَّن في المثال الأوّل عند مقارنة ‎int == int32_t‎، لكن ربما لا يكون هذا واضحًا كفاية. انظر المثال التالي إذ سيطبع true في كل المصرفات: typedef int MyType std::cout <<std::is_same<int, MyType>::value << "\n"; استخدام std::is_same لإطلاق تحذير عند استخدام صنف أو دالة قالب بطريقة غير صحيحة يمكن استخدام ‎std::is_same‎ مع static_assert لفرض الاستخدام السليم للأصناف والدوال المُقوْلبة. على سبيل المثال، هذه دالّة لا تسمح إلّا بالمُدخلات من النوع ‎int‎، والاختيار بين بنيتين. #include <type_traits> struct foo { int member; // متغيّرات أخرى }; struct bar { char member; }; template < typename T> int AddStructMember(T var1, int var2) { إن كان T != foo || T != bar، اعرض رسالة خطأ، … static_assert(std::is_same<T, foo>::value || std::is_same<T, bar>::value, "This function does not support the specified type."); return var1.member + var2; } سمات النوع الأساسية الإصدار ≥ C++‎ 11 لدينا في ++C عدة أنواع من السمات تقارن بعض الأنواع العامّة كما سنشرح أدناه: is_integral تعيد القيمة true بالنسبة لجميع أنواع الأعداد الصحيحة، مثل ‎int‎ و ‎char‎ و ‎long‎ و ‎unsigned int‎ وغيرها. std::cout <<std::is_integral<int>::value << "\n"; // true. std::cout <<std::is_integral<char>::value << "\n"; // true. std::cout <<std::is_integral<float>::value << "\n"; // false. is_floating_point تعيد القيمة true بالنسبة لجميع أنواع الأعداد العشرية، مثل ‎float‎ و ‎double‎ و ‎long double‎ وغيرها std::cout <<std::is_floating_point<float>::value << "\n"; // true. std::cout <<std::is_floating_point<double>::value << "\n"; // true. std::cout <<std::is_floating_point<char>::value << "\n"; // false. is_enum تعيد القيمة true بالنسبة لجميع أنواع التعدادات، بما في ذلك ‎enum class‎. enum fruit { apple, pair, banana }; enum class vegetable { carrot, spinach, leek }; std::cout <<std::is_enum<fruit>::value << "\n"; // true. std::cout <<std::is_enum<vegetable>::value << "\n"; // true. std::cout <<std::is_enum<int>::value << "\n"; // false. is_pointer تعيد القيمة true بالنسبة لجميع المؤشّرات: std::cout <<std::is_pointer<int*>::value << "\n"; // true. typedef int *MyPTR; std::cout <<std::is_pointer<MyPTR>::value << "\n"; // true. std::cout <<std::is_pointer<int>::value << "\n"; // false. is_class تعيد القيمة true بالنسبة لجميع الأصناف والبنيات، باستثناء ‎enum class‎. struct FOO { int x, y; }; class BAR { public: int x, y; }; enum class fruit { apple, pair, banana }; std::cout <<std::is_class<FOO>::value << "\n"; // true. std::cout <<std::is_class<BAR>::value << "\n"; // true. std::cout <<std::is_class<fruit>::value << "\n"; // false. std::cout <<std::is_class<int>::value << "\n"; // false. تباين النوع المُعاد Return Type Covariance يُقصَد بتباين نوع القيمة المعادة (Return Type Covariance) من تابع وهمي m السلوك الذي يصبح فيه نوع ذلك التابع (T) أكثر تحديداً عند إعادة تعريف m في صنف مشتق. ونتيجة لذلك، يتغيّر تحديد (specificity ) النوع T كما هو حال الصنف الذي يحتوي التابع m. انظر المثال التوضيحي التالي: // 2. نسخة النتيجة المتباينة من المثال الأساسي، تحقق النوع الساكن. class Top { public: virtual Top* clone() const = 0; virtual~Top() = default; }; class D: public Top { public: D* /*← Covariant return */ clone() const override { return new D(*this); } }; class DD: public D { private: int answer_ = 42; public: int answer() const { return answer_; } DD * /*← Covariant return */ clone() const override { return new DD(*this); } }; #include <iostream> using namespace std; int main() { DD *p1 = new DD(); DD *p2 = p1->clone(); تحقق النوع الساكن يضمن النوع الديناميكي الصحيح DD لـ p2*، نتابع المثال … cout << p2->answer() << endl; // "42" delete p2; delete p1; } نتيجة المؤشّرات الذكية المتباينة إليك المثال التالي: // 3. (نتيجة تباين المؤشر الذكي (تنظيف آلي. #include <memory> using std::unique_ptr; template < class Type> auto up(Type *p) { return unique_ptr<Type> (p); } class Top { private: virtual Top* virtual_clone() const = 0; public: unique_ptr<Top> clone() const { return up(virtual_clone()); } virtual~Top() = default; }; class D: public Top { private: D * /*← Covariant return */ virtual_clone() const override { return new D(*this); } public: unique_ptr<D> /*← Apparent covariant return */ clone() const { return up(virtual_clone()); } }; class DD: public D { private: int answer_ = 42; DD * /*← Covariant return */ virtual_clone() const override { return new DD(*this); } public: int answer() const { return answer_; } unique_ptr<DD> /*← Apparent covariant return */ clone() const { return up(virtual_clone()); } }; #include <iostream> using namespace std; int main() { auto p1 = unique_ptr<DD> (new DD()); auto p2 = p1->clone(); تحقق النوع الساكن يضمن النوع الديناميكي الصحيح DD لـ p2*، نتابع المثال … cout << p2->answer() << endl; // "42" // unique_ptr التنظيف يتم تلقائيا عبر المؤشر الحصري } مخطط أنواع الكائنات Layout of object types أنواع الأصناف نعني بكلمة "صنف" (class)، أيّ نوع عُرِّف باستخدام الكلمة المفتاحية ‎class‎ أو ‎struct‎ (ولكن ليس بـ ‎enum struct‎ أو ‎enum ‎class‎). حتى إن كان الصنف فارغًا فإنه يحتلّ بايت واحدًا على الأقل في الذاكرة؛ ومن ثم سيتألف من الحشو (padding) فقط، هذا يضمن أنّه إذا أشار مؤشّر ‎p‎ إلى كائن من صنف فارغ، فإنّ ‎p + 1‎ ستكون عنوانًا مختلفًا وستشير إلى كائن مختلف. لكن يمكن أن يساوي حجم صنف فارغ القيمة 0 عند استخدامه كصنف أساسي. راجع: تحسين الأصناف الأساسية الفارغة. class Empty_1 {}; // sizeof(Empty_1) == 1 class Empty_2 {}; // sizeof(Empty_2) == 1 class Derived: Empty_1 {}; // sizeof(Derived) == 1 class DoubleDerived: Empty_1, Empty_2 {}; // sizeof(DoubleDerived) == 1 class Holder { Empty_1 e; }; // sizeof(Holder) == 1 class DoubleHolder { Empty_1 e1; Empty_2 e2; }; // sizeof(DoubleHolder) == 2 class DerivedHolder: Empty_1 { Empty_1 e; }; // sizeof(DerivedHolder) == 2 التمثيل الخاص بكائن صنف معيّن يحتوي تمثيل كائن الصنف الأساسي، وكذلك أنواع الأعضاء غير الساكنة (non-static member types). على سبيل المثال، في الصنف التالي: struct S { int x; char *y; }; يوجد تسلسل متتالي حجمه ‎sizeof(int)‎ بايت داخل كائن من النوع ‎S‎، ويُطلق عليه "كائن فرعي" (subobject)، يحتوي قيمة ‎x‎، إضافة إلى كائن فرعي آخر حجمه ‎sizeof(char*)‎ بايت ويحتوي قيمة ‎y‎، ولا يمكن أن يتداخل الاثنان. إذا كان لنوع صنف معيّن أعضاء و/أو أصناف أساسية من الأنواع ‎t1, t2,...tN‎، فينبغي ألّا يقلّ الحجم عن sizeof(t1) + sizeof(t2) + ... + sizeof(tN)‎ نظرًا للنقاط السابقة، لكن بناءً على متطلّبات المحاذاة الخاصّة بالأعضاء والأصناف الأساسية، فقد يضطر المٌصرّف إلى إدراج حشو بين الكائنات الفرعية، أو في بداية الكائن أو نهايته. struct AnInt { int i; }; // sizeof(AnInt) == sizeof(int) // sizeof(AnInt) == 4 (4) :في أنظمة 32 أو 64 بت struct TwoInts { int i, j; }; // sizeof(TwoInts) >= 2* sizeof(int) // sizeof(TwoInts) == 8 (4 + 4) :في أنظمة 32 أو 64 بت struct IntAndChar { int i; char c; }; // sizeof(IntAndChar) >= sizeof(int) + sizeof(char) // sizeof(IntAndChar) == 8 (4 + 1 + padding) :في أنظمة 32 أو 64 بت struct AnIntDerived: AnInt { long long l; }; // sizeof(AnIntDerived) >= sizeof(AnInt) + sizeof(long long) // sizeof(AnIntDerived) == 16 (4 + padding + 8) :في أنظمة 32 أو 64 بت في حال إدراج الحشو في كائن بسبب متطلّبات المحاذاة، فإنّ الحجم سيكون أكبر من مجموع أحجام الأعضاء والأصناف الأساسية، أما إن كانت المحاذاة مؤلّفة من ‎n‎ بايت، فسيكون الحجم عادةً أصغر مضاعَف لـ‏ ‎n‎، وهو أكبر من حجم جميع الأعضاء والأصناف الأساسية. كذلك سيوضع كل عضو ‎memN‎ في عنوان من مضاعفات ‎alignof(memN)‎، وسيساوي ‎n‎ عادةً محاذاة العضو الذي له أكبر محاذاة. نتيجة لهذا، في حال أُتبِع عضوٌ بعضو آخر ذي محاذاة أكبر فهناك احتمال بأنّ العضو الأخير لن يُحاذى بشكل صحيح في حال وضعه مباشرة بعد العضو السابق، وهنا سيُوضع الحشو (المعروف أيضًا بمحاذاة العضو - alignment member) بين العضوين، بحيث يتيح للعضو الأخير أن يحصل على المحاذاة المرغوبة. بالمقابل، إذا أُتبِع عضو بعضوٍ آخر ذي محاذاة أصغر، فلن تكون هناك حاجة إلى الحشو، تُعرف هذه العملية أيضًا باسم "التعبئة" (packing). ونظرًا لأنّ الأصناف تشارك عادة محاذاة أعضائها مع أكبر محاذاة (largest alignof)، فستأخذ الأصناف عادةً محاذاة النوع المُضمن (built-in type) بشكل مباشر أو غير مباشر والذي له أكبر محاذاة. انظر المثال التالي: افترض أن sizeof(short) == 2 و sizeof(int) == 4 و sizeof(long long) == 8، وكذلك افترض تحديد محاذاة 4-بت للمصرِّف struct Char { char c; }; // sizeof(Char) == 1 (sizeof(char)) struct Int { int i; }; // sizeof(Int) == 4 (sizeof(int)) struct CharInt { char c; int i; }; // sizeof(CharInt) == 8 (1 (char) + 3 (padding) + 4 (int)) struct ShortIntCharInt { short s; int i; char c; int j; }; // sizeof(ShortIntCharInt) == 16 (2 (short) + 2 (padding) + 4 (int) + 1 (char) + // 3 (padding) + 4 (int)) struct ShortIntCharCharInt { short s; int i; char c; char d; int j; }; // sizeof(ShortIntCharCharInt) == 16 (2 (short) + 2 (padding) + 4 (int) + 1 (char) + // 1 (char) + 2 (padding) + 4 (int)) struct ShortCharShortInt { short s; char c; short t; int i; }; // sizeof(ShortCharShortInt) == 12 (2 (short) + 1 (char) + 1 (padding) + 2 (short) + // 2 (padding) + 4 (int)) struct IntLLInt { int i; long long l; int j; }; // sizeof(IntLLInt) == 16 (4 (int) + 8 (long long) + 4 (int)) // إذا لم تُحدَّد التعبئة بصراحة، فإنّ معظم المصرفات ستعبّئ هذا مع محاذاة من 8 بتات، بحيث // sizeof(IntLLInt) == 24 (4 (int) + 4 (padding) + 8 (long long) + // 4 (int) + 4 (padding)) لتكن sizeof(bool) == 1 و sizeof(ShortIntCharInt) == 16، و sizeof(IntLLInt) == 24، والمحاذاة الافتراضية: alignof(ShortIntCharInt) == 4 و alignof(IntLLInt) == 8، نتابع المثال … struct ShortChar3ArrShortInt { short s; char c3[3]; short t; int i; }; // ShortChar3ArrShortInt لديه محاذاة 4 بت: alignof(int) >= alignof(char) && // alignof(int) >= alignof(short) // sizeof(ShortChar3ArrShortInt) ==12 (2 (short) + 3 (char[3]) + 1 (padding) + // 2 (short) + 4 (int)) // موضوع عند المحاذاة 2 وليس 4 t لاحظ أنّ // alignof(short) == 2 struct Large_1 { ShortIntCharInt sici; bool b; ShortIntCharInt tjdj; }; // تساوي 4 بتات Large_1 محاذاة // alignof(ShortIntCharInt) == alignof(int) == 4 // alignof(b) == 1 // alignof(Large_1) == 4 وعليه تكون // sizeof(Large_1) ==36 (16 (ShortIntCharInt) + 1 (bool) + 3 (padding) + // 16 (ShortIntCharInt)) struct Large_2 { IntLLInt illi; float f; IntLLInt jmmj; }; // تساوي 8 بتات Large_2 محاذاة // alignof(IntLLInt) == alignof(long long) == 8 // alignof(float) == 4 // alignof(Large_2) == 8 وعليه // sizeof(Large_2) == 56 (24 (IntLLInt) + 4 (float) + 4 (padding) + 24 (IntLLInt)) الإصدار ≥ C++‎ 11 في حال فَرْض المحاذاة الصارمة عبر ‎alignas‎، فسيُستخدَم الحشو لإجبار النوع على الالتزام بالمحاذاة المُحدّدة، حتى لو كان أصغر. في المثال أدناه، سيكون لـ ‎Chars<5>‎ ثلاث (أو ربّما أكثر) أُثمونات محشُوّة (padding bytes) مُدرَجة في النهاية لكي يبلغ الحجم الإجمالي 8 بتّات. لا يمكن لصنف محاذاته 4 أن يكون حجمه 5 لأنّه سيكون من المستحيل إنشاء مصفوفة من هذا الصنف، لذلك يجب "تقريب" الحجم إلى أحد مضاعفات العدد 4 عبر حشو البايتات. // ينبغي أن تكون محاذاة هذا النوع من مضاعفات 4، وينبغي إضافة الحشو عند الحاجة // Chars<1>..Chars<4> are 4 bytes, Chars<5>..Chars<8> are 8 bytes, etc. template < size_t SZ> struct alignas(4) Chars { char arr[SZ]; }; static_assert(sizeof(Chars < 1>) == sizeof(Chars < 4>), "Alignment is strict.\n"); إذا كان لعضوين غير ساكنين من صنف معيّن نفس مُحدّد الوصول (access specifier)، فإنّ العضو المُصَرَّح عنه أخيرًا سيأتي آخرًا في تمثيل الكائن. ولكن إذا اختلفت مُحدّدات الوصول، فإنّ ترتيبَهما النسبي داخل الكائن سيكون غير مُحدّد. الترتيب الذي تظهر به الكائنات الفرعية للصنف الأساسي داخل الكائن غير محدّد، سواء أكانت ستظهر بالتتابع، أو ستظهر قبل أو بعد أو بين الكائنات العضوية الفرعية. الأنواع الحسابية Arithmetic types أنواع الأحرف الضيقة Narrow character types يستخدِم نوع الحروف غير المؤشّرة ‎unsigned char‎ كل البتات لتمثيل عدد ثنائي (binary number). لذا إذا كان طول ‎unsigned char‎ يساوي 8 بتات، فإنّ كل الأنماط الممكن تمثيلها بـ 8 بتّات -والتي يبلغ عددها 256- للكائن ‎char‎ ستمثِّل 256 عددًا في المجال {0، 1، …، 255}. العدد 42 مثلا، سيُمثَّل بالسلسلة البتّية ‎00101010‎. ليس هناك حشو للبتّات في نوع الأحرُف المؤشّرة ‎signed char‎، أي أنّه إذا كان طول ‎signed char‎ يساوي 8 بتات، فسَيستخدم 8 بتات لتمثيل الأعداد. لاحظ أنّ هذه الضمانات لا تنطبق على الأنواع الأخرى. أنواع الأعداد الصحيحة تستخدم أنواع الأعداد الصحيحة غير المُؤشّرة نظامًا ثنائيًا خالصًا، لكنّها قد تحتوي على بتّات محشُوّة. على سبيل المثال، من الممكن -رغم بعداحتماله- أن يساوي طول عدد صحيح غير مؤشّر ‎unsigned int‎ ‏64 بتّة، لكن لن يكون بمقدوره تخزين الأعداد الصحيحة بين 0 و ‎‎232 - 1‎‎‎‎ (ضمنيّة)، لأنّ البتات الأخرى البالغ عددها 32 بتة ستكون عبارة عن بتّات حشو، وتلك لا ينبغي كتابتها مباشرة. تستخدم أنواع الأعداد الصحيحة المُؤشّرة نظامًا ثنائيًا يحتوي على بتّة مخصّصة للإشارة (بتّة الإشارة)، وربّما بتات محشوة. ويكون للقيم التي تنتمي إلى المجال المشترك بين نوع عددي صحيح مؤشّر ونوع عددي صحيح غير مؤشّر، يكون لها نفس التمثيل. على سبيل المثال، إذا كانت السلسلة البتّية ‎0001010010101011‎ لعدد صحيح قصير غير مؤشّر (‎unsigned short‎) تمثّل القيمة ‎5291‎، فإنّها ستمثّل أيضًا القيمة ‎5291‎ عند تفسيرها كعدد قصير (short). ويحدد التنفيذ أنظمة التمثيل المستخدمة، سواء كانت المكمّل الثنائي (two's complement)، أو المكمّل الأحادي (one's complement)، أو تمثيل الإشارة-السعة (sign-magnitude)، لأنّ الأنظمة الثلاثة تفي بالمتطلّبات الواردة في الفقرة السابقة. أنواع الأعداد العشرية Floating point types يتعلق تمثيل أنواع الأعداد العشرية بالتنفيذ، ويتوافق النوعان الأكثر شيوعًا ‎float‎ و ‎double‎ مع توصيف IEEE 754، ويبلغ طولهما 32 و 64 بت (مثلًا، ستتألّف دقّة النوع ‎float‎ من ‏23 بت، مع 8 بتّات للأسّ، وبتّ واحدة للإشارة)، لكن المعيار لا يضمن أيّ شيء، ويعتري تمثيل الأعداد العشرية غالبًا بعض الثغرات، والتي تتسبّب بأخطاء عند استخدامها في العمليات الحسابية. المصفوفات ليس هناك حشو بين عناصر أنواع المصفوفات، لذا فالمصفوفة التي تحتوي عناصر من النوع ‎T‎ هي مجرد سلسلة من كائنات ‎T‎ متجاورة في الذاكرة بالترتيب، والمصفوفات متعدّدة الأبعاد هي مصفوفات مكوّنة من مصفوفات، وينطبق عليها ما سبق تكراريًا. على سبيل المثال، إذا كان لدينا: int a[5][3]; فتكون ‎a‎ مصفوفة من 5 مصفوفات ثلاثية تحتوي أعدادًا صحيحة (‎int‎)، لذا فإنّ ‎‎‎‎a[0]‎ التي تتكون من العناصر الثلاثة a[0][0]‎ و a[0][1]‎ و a[0][2]‎، توضع في الذاكرة قبل ‎‎‎‎a[1]‎، التي تتكوّن من a[1][0]‎ و a[1][1]‎ و a[1][2]‎. ويُسمّى هذا النظام بالترتيب الكبير للصفوف (row major order). استنباط النوع Type Inference يناقش هذا الموضوع استنباط النوع ويشمل الكلمة المفتاحية ‎auto‎ المتاحة منذ الإصدار C++‎ 11. نوع البيانات: Auto يوضّح هذا المثال استنباطات النوع الأساسية التي يمكن للمٌصرّف القيام بها. auto a = 1; // a = int auto b = 2u; // b = unsigned int auto c = &a; // c = int* const auto d = c; // d = const int* const auto& e = b; // e = const unsigned int& auto x = a + b // x = int, #compiler warning unsigned and signed auto v = std::vector<int>; // v = std::vector<int> لا تنجح الكلمة المفتاحية auto دائمًا في استنباط النوع المتوقع إذا لم تُعطَ تلميحات إضافية بخصوص ‎&‎ أو ‎const‎ أو ‎constexpr‎. في المثال التالي حيث y تساوي unsigned int، لاحظ أننا لا نستطيع استنباط أن y من نوع &const unsigned int، وكان المترجم سينتج نسخة بدلًا من قيمة مرجعية إلى e أو b: auto y = e; Lambda auto يمكن استخدام الكلمة المفتاحية auto للتصريح عن دوالّ لامدا، إذ تساعد على اختصار الشيفرة اللازمة للتصريح عن مؤشّر دالّة. auto DoThis =[](int a, int b) { return a + b; }; هذا إن كان Do this من نوع (int)(*DoThis)(int, int)، وإلا فنكتب ما يلي: int(*pDoThis)(int, int) =[](int a, int b) { return a + b; }; auto c = Dothis(1, 2); // c = int auto d = pDothis(1, 2); // d = int // يختصر تعريف دوال لامدا 'auto' استخدام السلوك الافتراضي إذا لم يُعرَّف نوع القيمة المُعادة لدوال لامدا، هو استنباطها تلقائيًا من عبارة return. في المثال التالي، الأسطر الثلاث التالية متكافئة: [](int a, int b) -> int { return a + b; }; [](int a, int b) -> auto { return a + b; }; [](int a, int b) { return a + b; }; الحلقات و auto يوضّح هذا المثال كيف يمكن استخدام auto لاختصار تصريح أنواع حلقات for: std::map<int, std::string > Map; for (auto pair: Map) // pair = std::pair<int, std::string> for (const auto pair: Map) // pair = const std::pair<int, std::string > for (const auto &pair: Map) // pair = const std::pair<int, std::string>& for (auto i = 0; i < 1000; ++i) // i = int for (auto i = 0; i < Map.size(); ++i) // size_t وليس i = int لاحظ أنّ for (auto i = Map.size(); i > 0; --i) // i = size_t استنتاج الأنواع type deduction استنتاج مُعامل القالب الخاص بالمنشئات لم يكن بمقدور "استنتاج القالب" (template deduction) قبل الإصدار C++‎ 17 أن يستنتج نوع الصنف في مُنشئ، بل كان يجب تحديده بشكل صريح، وبما أن تسمية تلك الأنواع مرهقة أحيانًا أو (في حال تعبيرات لامدا) مستحيلة، فقد وُجِدت عدّة مصانع للأنواع (مثل ‎make_pair()‎ و ‎make_tuple()‎ و ‎back_inserter()‎ وما إلى ذلك). الإصدار ≥ C++‎ 17 غير أن هذا لم يُعدّ هذا ضروريًا بعد الآن: std::pair p(2, 4.5); // std::pair<int, double> std::tuple t(4, 3, 2.5); // std::tuple<int, int, double> std::copy_n(vi1.begin(), 3, std::back_insert_iterator(vi2)); // back_insert_iterator<std::vector < int>> إنشاء std::lock_guard lk(mtx); // std::lock_guard < decltype(mtx)> يُتوقّع من المُنشئات استنتاج معامِلات قالب الصنف (class template parameters)، لكن هذا قد لا يكفي أحيانًا، لذا يمكننا تقديم بعض التوجيهات الصريحة لتسهيل الاستنتاج: template <class Iter> vector(Iter, Iter) -> vector<typename iterator_traits<Iter>::value_type> int array[] = {1, 2, 3}; std::vector v(std::begin(array), std::end(array)); // std::vector < int> استنتاج استنتاج النوع عبر Auto الإصدار ≥ C++‎ 11 يعمل استنتاج النوع باستخدام الكلمة المفتاحية ‎auto‎ بطريقة مشابهة لاستنتاج نوع القالب (Template Type Deduction). انظر الأمثلة التالية: x في الشيفرة أدناه ليست لا مؤشرًا ولا مرجعًا، بل هي من النوع int، وcx ليست هذا ولا ذاك أيضًا، وإنما هي من نوع const int، بينما تكون rx مرجعًا غير عام (non-universal)، فهي مرجع إلى ثابت. انظر: auto x = 27; const auto cx = x; const auto& rx = x; وفي الشيفرة أدناه، تكون x عددًا صحيحًا int وقيمة يسارية أيضًا lvalue، وعليه يكون نوع uref1 هو &int، وبالمثل فإن cx نوعها const int وقيمة يسارية، لذا يكون uref2 من نوع & const int. أما 27 فهي عدد صحيح وقيمة يسارية، لذا يكون uref3 من نوع &&int. انظر .. auto&& uref1 = x; auto&& uref2 = cx; auto&& uref3 = 27; الاختلافات بين المثالين السابقين مُوضّحة أدناه: يكون نوع x1 و x2 هو int وقيمتهما 27، أما x3 و x4 فنوعهما <std::initializer_list<int وقيمة كل منهما { 27 }. قد يُستنتج النوع في بعض المصرِّفات على أنه int مع قيمة تساوي 27. auto x1 = 27; auto x2(27); auto x3 = { 27 }; auto x4{ 27 }; auto x5 = { 1, 2.0 } // error! can't deduce T for std::initializer_list < t> إذا استخدمت مُهيّئات الأقواس المعقوصة (braced initializers)، فسيُفرض على auto إنشاء متغيّر من النوع std::initializer_list<T>‎، وإذا لم يكن من الممكن استنتاج ‎T‎، فستُرفض الشيفرة. عندما تُستخدَم ‎auto‎ كنوع القيمة المُعادة من دالّة، فإنّ نوع الإعادة سيكون زائدًا (trailing return type). auto f() -> int { return 42; } الإصدار ≥ C++‎ 14 يسمح الإصدار C++‎ 14، بالإضافة إلى استخدام auto المسموح بها في C++‎ 11، بما يلي: عند استخدامها كنوع للقيمة المُعادة من دالة بدون نوع إعادة زائد (trailing return type)، فإنّها تشير إلى أنّ النوع المُعاد من الدالة يجب أن يُستنتَج من تعليمات الإعادة في متن الدالّة، إن وُجِدت. // int تعيد f auto f() { return 42; } // void تعيد g auto g() { std::cout << "hello, world!\n"; } عند استخدامها مع نوع مُعامل خاص بتعبير لامدا، فإنّها تشير إلى أنّ لامدا عامّة (generic). في المثال أدناه تكون x من نوع const int وقيمتها 126. auto triple = [](auto x) { return 3*x; }; const auto x = triple(42); يستنتج الشكل الخاصّ ‎decltype(auto)‎ النوع باستخدام قواعد استنتاج النوع في ‎decltype‎، وليس قواعد ‎auto‎. في المثال التالي، x عدد صحيح، و y مرجع إلى p*: int* p = new int(42); auto x = *p; decltype(auto) y = *p; في C++‎ 03 والإصدارات الأقدم، كان للكلمة المفتاحية ‎auto‎ معنى مختلف تمامًا، إذ كانت مُحدِّد صنف تخزين (storage class specifier)، وقد ورِثتها من C. استنتاج نوع القالب الصيغة العامة للقالب: template < typename T> void f(ParamType param); f(expr); الحالة 1: إذا كان ‎ParamType‎ مرجعًا أو مؤشّرًا، ولم يكن مرجعًا عامًا (Universal) أو لاحقًا (Forward). فسيعمل استنتاج النوع بالطريقة التالية: سيتجاهل المُصرّف جزء المرجع إذا كان موجودًا في ‎expr‎، وسيحاول المُصرّف بعد ذلك مطابقة ‎expr‎ الخاص بالنوع مع ‎ParamType‎ لتحديد ‎T‎. في المثال التالي يكون param مرجعًا، وx من نوع int، وcx من نوع const int، أما rx فهو مرجع إلى x كـ const int، انظر: template < typename T> void f(T& param); int x = 27; const int cx = x; const int& rx = x; وفي بقية المثال أدناه، في حالة (f(x تكون T عددًا صحيحًا، ونوع param هو &int، أما في السطرين الثاني والثالث، تكون T من نوع const int و param من نوع &const int، انظر … f(x); f(cx); f(rx); الحالة 2: إذا كان ‎ParamType‎ مرجع عامًا أو مرجعًا لاحقًا، فسيكون استنتاج النوع مماثلًا لاستنتاج النوع في الحالة 1 إن كانت ‎expr‎ عبارة عن قيمة يمينية. أمّا إذا كانت ‎expr‎ قيمة يسارية، فسيُستنتَج أنّ ‎T‎ و ‎ParamType‎ مرجعان يساريان. انظر المثال التالي حيث يكون param مرجعًا عامًا، وx من نوع int، وcx من نوع const int، و rx مرجع إلى x كـ const int: template < typename T> void f(T&& param); int x = 27; const int cx = x; const int& rx = x; في بقية المثال أدناه، (f(x: تكون x قيمة يسارية وعليه فإن T و param يكون نوعهما &int. (f(cx: تكون cx قيمة يسارية وعليه فإن T و param يكون نوعهما &const int. (f(rx: تكون rx قيمة يسارية وعليه فإن T و param يكون نوعهما &const int. (f(27: تكون 27 قيمة يمينية وعليه فإن T تكون int ومن ثم فإن param يكون نوعها &&int. f(x); f(cx); f(rx); f(27); الحالة 3: في حال لم يكن ‎ParamType‎ لا مؤشّرًا ولا مرجعًا، فإذا كانت ‎expr‎ مرجعًا أو ثابتةً، فسيُتجاهَل جزء المرجع، أمّا إذا كانت متغيّرة (volatile)، فسيُتجاهَل هذا أيضًا عند استنتاج نوع T. في المثال التالي: تُمرَّر param بالقيمة، وتكون x عددًا صحيحًا int، وcx تكون const int، بينما تكون rx مرجعًا إلى x كـ const int: template < typename T> void f(T param); int x = 27; const int cx = x; const int& rx = x; في بقية المثال أدناه، يكون كل من T و param نوعهما int. f(x); f(cx); f(rx); نوع الإعادة الزائد Trailing return type تجنّب تأهيل اسم نوع مُتشعِّب class ClassWithAReallyLongName { public: class Iterator { /*... */ }; Iterator end(); }; تعريف العضو ‎end‎ بنوع إعادة زائد (trailing return type): auto ClassWithAReallyLongName::end()->Iterator { return Iterator(); } تعريف العضو ‎end‎ بدون نوع إعادة زائد: ClassWithAReallyLongName::Iterator ClassWithAReallyLongName::end() { return Iterator(); } يُبحَثُ عن نوع الإعادة الزائد في نطاق الصنف (scope of the class)، بينما يُبحث عن نوع الإعادة البادئ (leading return type) في نطاق فضاء الاسم (namespace) المحيط، لذا قد يتطّلب ذلك إجراءَ تأهيل "مُكرّر" (redundant qualification). تعبيرات لامدا Lambda expressions لا يكون تعبير لامدا إلا نوع إعادة زائد فقط؛ إذ أنّ صيغة نوع الإعادة البادئ لا يمكن تطبيقها على دوالّ لامدا، لاحظ أنّه من غير الضروري غالبًا تحديد النوع المُعاد لأجل دالة لامدا. struct Base {}; struct Derived1: Base {}; struct Derived2: Base {}; auto lambda =[](bool b)->Base * { if (b) return new Derived1; else return new Derived2; }; // auto lambda = Base*[](bool b) { ... }; صيغة سيئة Typedef والأسماء البديلة للأنواع يمكن استخدام الكلمتين المفتاحيتين ‎typedef‎ و (منذ C++‎ 11)‏ ‎using‎ لإعطاء اسم جديد لنوع موجود. أساسيات صياغة typedef تصريح ‎typedef‎ يشبه التصريح عن متغيّر أو دالّة، غير أنّها تحتوي كلمة ‎typedef‎، ويؤدّي وجودها إلى التصريح عن نوع بدلاً من التصريح عن متغيّر أو دالّة. انظر المثال التالي: في السطر التالي، T نوعه int: int T; وهنا يكون T اسمًا بديلًا أو كُنية (alias) لـ int: typedef int T; نوع A "مصفوفة من 100 عدد صحيح: int A[100]; أما هنا فيكون A اسمًا بديلًا للنوع "مصفوفة من 100 عدد صحيح": typedef int A[100]; ونستطيع استخدام الاسم البديل للنوع بالتبادل مع الاسم الأصلي للنوع بمجرد تمام تعريف الأول. انظر المثال التالي حيث تكون S بُنية تحتوي مصفوفة من 100 عدد صحيح: typedef int A[100]; struct S { A data; }; لا تنشئ ‎typedef‎ نوعًا مختلفًا، بل تعطينا وسيلة أخرى للإشارة إلى نوع موجود سلفًا. struct S { int f(int); }; typedef int I; I S::f(I x) { return x; } استخدامات متقدّمة للكلمة المفتاحية typedef بما أن تصريحات typedof لها نفس بنية تصريح المتغيرات العادية والدوال، فيمكن استخدام ذلك لقراءة وكتابة تصريحات أعقد. انظر المثال التالي حيث يكون نوع f في السطر الأول مؤشر إلى دالة تأخذ عددا صحيحا وتعيد void، بينما يكون في السطر الثاني اسمًا بديلًا للمؤشر: void(*f)(int); typedef void(*f)(int); هذا مفيد بشكل خاص للبنيات ذات الصياغة المُربِكة، مثل المؤشّرات التي تشير إلى أعضاء غير ساكنة. انظر المثال التالي حيث يكون نوع pmf في السطر الأول مؤشرًا إلى دالة تابعة لـ Foo، يأخذ عددًا صحيحًا ويعيد void، بينما يكون في السطر الثاني اسمًا بديلًا للمؤشر: void(Foo:: *pmf)(int); typedef void(Foo:: *pmf)(int); صيغة تصريحات الدوال التالية صعبة التذكر حتى للمبرمجين ذوي الخبرة: void(Foo:: *Foo::f(const char *))(int); int(&g())[100]; ويمكن استخدام ‎typedef‎ لتسهيل قراءة الشيفرة كما في المثال التالي، حيث تكون pmf مؤشرًا إلى نوع دالة تابعة، و f هي دالة تابعة لـ Foo، و ra هي اختصار يعني "مرجع إلى مصفوفة" تتكون في حالتنا هنا من 100 عدد صحيح، وتعيد g مرجعًا إلى مصفوفة من 100 عدد صحيح أيضًا: typedef void(Foo::pmf)(int); pmf Foo::f(const char *); typedef int(&ra)[100]; ra g(); التصريح عن عدّة أنواع باستخدام typedef تُعدُّ الكلمة المفتاحية ‎typedef‎ مُحدِّدًا (specifier)، لذا فهي تُطبّق بشكل منفصل على كل مُصرِّح (declarator)، وعليه يشير كل اسم مُصرّح به إلى النوع الذي سيكون لذلك الاسم في غياب ‎typedef‎. انظر المثال التالي حيث يكون نوع x هو *int ونوع p هو ()(*)int، أما في السطر التالي فيكون x اسمًا بديلًا لـ *int، و p اسم بديل لـ ()(*)int: int *x, (*p)(); typedef int *x, (*p)(); التصريح عن الاسم البديل عبر using الإصدار ≥ C++‎ 11 صيغة ‎using‎ بسيطة للغاية، إذ يوضع الاسم المُراد تعريفه على الجانب الأيسر، بينما يوضع التعريف على الجانب الأيمن. using I = int; using A = int[100]; //مصفوفة من 100 عدد صحيح using FP = void(*)(int); // void يعيد int مؤشر إلى دالة من using MP = void(Foo:: *)(int); // void ويعيد int يأخذ Foo مؤشر إلى دالة تابعة من إنشاء اسم بديل للنوع باستخدام ‎using‎ له نفس تأثير إنشاء اسم بديل للنوع باستخدام ‎typedef‎، فما هي إلا صيغة بديلة لإنجاز نفس الشيء. وعلى عكس ‎typedef‎، فإنّ ‎using‎ قد تكون مُقولَبة، ويُستخدم مصطلح قالب الاسم البديل "alias template" للإشارة إلى "typedef المُقولبة" (template typedef) والمُنشأة عبر ‎using‎. نوع الإعادة الزائد Trailing return type تجنّب تأهيل اسم نوع مُتشعِّب class ClassWithAReallyLongName { public: class Iterator { /*... */ }; Iterator end(); }; تعريف العضو ‎end‎ بنوع إعادة زائد (trailing return type): auto ClassWithAReallyLongName::end()->Iterator { return Iterator(); } تعريف العضو ‎end‎ بدون نوع إعادة زائد: ClassWithAReallyLongName::Iterator ClassWithAReallyLongName::end() { return Iterator(); } يُبحَثُ عن نوع الإعادة الزائد في نطاق الصنف (scope of the class)، بينما يُبحث عن نوع الإعادة البادئ (leading return type) في نطاق فضاء الاسم (namespace) المحيط، لذا قد يتطّلب ذلك إجراءَ تأهيل "مُكرّر" (redundant qualification). تعبيرات لامدا Lambda expressions لا يكون تعبير لامدا إلا نوع إعادة زائد فقط؛ إذ أنّ صيغة نوع الإعادة البادئ لا يمكن تطبيقها على دوالّ لامدا، لاحظ أنّه من غير الضروري غالبًا تحديد النوع المُعاد لأجل دالة لامدا. struct Base {}; struct Derived1: Base {}; struct Derived2: Base {}; auto lambda =[](bool b)->Base * { if (b) return new Derived1; else return new Derived2; }; // auto lambda = Base*[](bool b) { ... }; صيغة سيئة محاذاة الأنواع جميع الأنواع في C++‎ لها محاذاة (alignment) تمثّل قيودًا على عناوين الذاكرة التي يمكن لكائنات تلك الأنواع أن تُنشأ فيها. ويكون عنوان في الذاكرة صالحًا لإنشاء كائن ما إذا كان العنوان قابلًا للقسمة على محاذاة ذلك الكائن. تساوي محاذاة الأنواع دائمًا قوةً للعدد 2 (بما في ذلك العدد 1، والذي يساوي 2 أسّ 0). التحكم في المحاذاة الإصدار ≥ C++‎ 11 يمكن استخدام الكلمة المفتاحية ‎alignas‎ لفرض محاذاة معيّنة على متغيّر أو حقل من صنف، أو تصريح صنف أو تعريفه، أو تصريح تعداد أو تعريفه. وتأتي في شكلين: alignas(x)‎ - حيث x تعبير ثابت، يمثّل محاذاة الكيان إن كانت مدعومة. alignas(T)‎ - حيث T يمثّل نوعًا، ويجعل محاذاة الكيان مساوية لمحاذاة النوع T، أي alignof(T)‎ إذا كانت مدعومة. ستُطبَّق المحاذاة الأكثر صرامة إذا تم تطبيق عددة محدِّدات على نفس الكيان. في المثال التالي، نضمن أن يكون للمخزن المؤقّت ‎buf‎ المحاذاة المناسبة لتخزين كائن ‎int‎ رغم أنّ نوع عناصره هو ‎unsigned char‎، والذي قد تكون محاذاته أضعف. alignas(int) unsigned char buf[sizeof(int)]; new(buf) int(42); لا يمكن استخدام ‎alignas‎ لجعل محاذاة نوع معيّن أصغر من المحاذاة الطبيعية التي كان سيحصل عليها بدونها، انظر المثال التالي حيث تُعد صيغة السطر الأول خاطئة إلا إن كانت محاذاة int هي 1 بايت، وكذلك في السطر الثاني تكون خاطئة إلا إن كانت محاذاة int تساوي محاذاة char أو أقل منها. alignas(1) int i; alignas(char) int j; يجب تمرير محاذاة صالحة لـ ‎alignas‎ عند إعطائها تعبيرًا ثابتًا صحيحًا، وينبغي للمحاذاة الصالحة أن تساوي دائمًا قوةً للعدد 2، ويجب أن تكون أكبر من الصفر. وتُلزَم المٌصرّفات بدعم جميع قيم المحاذاة الصالحة شرط ألًا تتجاوز محاذاة النوع ‎std::max_align_t‎، لكن من الممكن أن تدعم محاذاة أكبر من ذلك، إلا أن دعم تخصيص الذاكرة لمثل هذه الكائنات محدود، كما أنّ الحد الأعلى للمحاذاة يتعلّق بالتنفيذ. توفّر C++‎ 17 دعمًا مباشرًا في ‎operator new‎ لتخصيص الذاكرة للأنواع ذات المحاذاة الزائدة (over-aligned types). الاستعلام عن محاذاة نوع الإصدار ≥ C++‎ 11 يمكن الاستعلام عن محاذاة نوع معيّن باستخدام الكلمة المفتاحية ‎alignof‎ كمُعامل أحادي، وتكون لنتيجة تعبيرًا ثابتًا من النوع ‎std::size_t‎، مما يعني إمكانية تقييمه في وقت التصريف. #include <iostream> int main() { std::cout << "The alignment requirement of int is: " << alignof(int) << '\n'; } خرج محتمل: The alignment requirement of int is: 4 تعيد المحاذاةَ المطلوبة لنوع عناصر في مصفوفة في حال تطبيقها على تلك المصفوفة، أمّا في حال تطبيقها على نوع مرجع (reference type)، فستعيد محاذاة النوع الذي يشير إليه ذلك المرجع، إذ أن المراجع بحد ذاتها ليس لها محاذاة، لأنّها ليست كائنات. هذا الدرس جزء من سلسلة دروس عن C++‎. ترجمة -بتصرّف- للفصول التالي: Chapter 84: RTTI: Run-Time Type Information Chapter 89: Atomic Types Chapter 90: Type Erasure Chapter 91: Explicit type conversions Chapter 92: Unnamed types Chapter 93: Type Traits Chapter 94: Return Type Covariance Chapter 95: Layout of object types Chapter 96: Type Inference Chapter 97: Typedef and type aliases Chapter 98: type deduction Chapter 99: Trailing return type Chapter 100: Alignment من كتاب C++ Notes for Professionals
  23. قاعدة الصفر الإصدار ≥ C++‎ 11 عند الجمع بين مبادئ "قاعدة الخمسة" (Rule of Five) و RAII نحصل على "قاعدة الصفر" (Rule of Zero) ذات الواجهة الرشيقة، والتي قدمها مارتينيو فرنانديز لأول مرة، وتنصّ على أنّ أيّ مورِدٍ تجب إدارته ينبغي أن يكون في نوعه الخاص. ويجب أن يتبع ذلك النوع "قاعدة الخمسة"، لكن ليس على كلّ مستخدمي ذلك المورد أن يكتبوا التوابع الخمسة التي تتطلّبها "قاعدة الخمسة" (كما سنرى لاحقا)، إذ يمكنهم استخدام الإصدار الافتراضي ‎default‎ من تلك التوابع. وسننشئ في مثال قاعدة الثلاثة أدناه كائنًا لإدارة موارد cstrings باستخدام الصنف Person، انظر: class cstring { private: char* p; public: ~cstring() { delete [] p; } cstring(cstring const& ); cstring(cstring&& ); cstring& operator=(cstring const& ); cstring& operator=(cstring&& ); /* أعضاء آخرون */ }; يصبح الصنف ‎Person‎ أكثر بساطة بعد فصل الشيفرة: class Person { cstring name; int arg; public: ~Person() = default; Person(Person const &) = default; Person(Person &&) = default; Person &operator=(Person const &) = default; Person &operator=(Person &&) = default; /*أعضاء آخرون */ }; لا يلزم التصريح عن الأعضاء الخاصين في ‎Person‎ صراحة إذ سيتكفّل المُصرّف باعتماد الإصدار الافتراضي أو حذفها استنادًا إلى محتويات ‎Person‎. إليك مثالًا آخر عن قاعدة الصفر. struct Person { cstring name; int arg; }; إذا كان النوع ‎cstring‎ للنقل فقط (move-only type)، وكان يحتوي على عامل إنشاء/ إسناد ‎deleted‎، فسيكون ‎Person‎ تلقائيًا للنقل فقط أيضًا. قاعدة الخمسة Rule of Five الإصدار ≥ C++‎ 11 قدَّمت C++‎ 11 دالتين تابعتين جديدتين هما مُنشئ النقل (move constructor) وعامل إسناد النقل (operator move assignment)، وستجد أن نفس الأسباب التي قد تجعلك ترغب في اتّباع "قاعدة الثلاثة" في C++‎ 03 (انظر أدناه) ستجعلك تتّبع أيضًا "قاعدة الخمسة" في C++‎ 11: أنه إذا كان الصنف يتطلّب إحدى الدوال التابعة الخاصّة وكانت دلالات النقل (move semantics) مطلوبة، فالراجح أن الصنف سيتطّلب كلّ التوابع الخمسة، لكن اعلم أنّ عدم اتباع "قاعدة الخمسة" لا يُعدُّ خطأ عادةً طالما أنّك تتّبع قاعدة الثلاثة، لكنّه قد يضيّع عليك فرصة تحسين برنامجك. إذا لم يكن مُنشئ نقل أو مُعامل إسناد النقل متاحًا عندما يحتاجه المصرّف فسيَستخدم دلالات النسخ إن أمكن، هذا قد يؤدّي إلى إضعاف الكفاءة بسبب إجراء عمليات نسخ غير ضرورية. كذلك، لن تحتاج إلى التصريح عن مُنشئ نقل أو مُعامل إسناد إذا لم يكن الصنف بحاجة إلى دلالات النقل. انظر المثال التالي: class Person { char *name; int age; public: // مدمّر ~Person() { delete[] name; } // نفِّذ دلالات النَّسخ Person(Person const &other): name(new char[std::strlen(other.name) + 1]), age(other.age) { std::strcpy(name, other.name); } Person &operator=(Person const &other) { // استخدام أسلوب النسخ والمبادلة لتقديم عملية الإسناد Person copy(other); swap(*this, copy); return * this; } // نفذ دلالات النقل. اعلم أن الأفضل هو جعل عوامل النقل كـ noexcept، إذ يسمح ذلك بتنفيذ بعض التحسينات من قبل المكتبة القياسية عند استخدام الصنف داخل حاوية، نتابع المثال … Person(Person && that) noexcept: name(nullptr) // ضبط القيمة لنعلم أنها غير محددة , age(0) { swap(*this, that); } Person &operator=(Person && that) noexcept { swap(*this, that); return * this; } friend void swap(Person &lhs, Person &rhs) noexcept { std::swap(lhs.name, rhs.name); std::swap(lhs.age, rhs.age); } }; تستطيع -كخيار آخر- استبدال كل من مُعامل إسناد النسخ والنقل بمُعامل إسناد واحد يأخذ نسخة بالقيمة (by value) بدلاً من أخذها بالمرجع أو بالمرجع اليميني (rvalue reference)، وذلك لتسهيل استخدام تقنيات النسخ والمبادلة. Person &operator=(Person copy) { swap(*this, copy); return * this; } واعلم أن التوسّع من "قاعدة الثلاثة" إلى "قاعدة الخمسة" مهمّ لأسباب تتعلق بالأداء لكنّه في أغلب الحالات غير ضروري، ويضمن إضافة مُنشئ النسخ ومُعامل الإسناد أنّ نقل النوع لن يؤدّي إلى تسرّب الذاكرة -سيتحوّل إنشاء النقل إلى النسخ في هذه الحالة- لكنّه سيجري عمليات نسخ لم يتوقعها المُستدعي على الأرجح. قاعدة الثلاثة Rule of Three الإصدار ≤ C++‎ 03 تنصّ قاعدة الثلاثة على أنّه إن احتاج نوع معيّن أن يكون له مُنشئ نسخ مُعرَّف من قبل المستخدم (user-defined copy constructor)، أو مُعامل إسناد نسخ أو مُدمِّر، فيجب أن يتحوي على الثلاثة معًا. وسبب إنشاء هذه القاعدة هو أنّ الصنف الذي يحتاج أيًّا من تلك الوظائف الثلاثة سيحتاج أيضًا إلى إدارة الموارد (مقابض الملفات، الذاكرة المخصّصة ديناميكيًا، الخ)، لكن إدارة تلك المورد تحتاج دائمًا إلى تلك الوظائف الثلاث، وتتكفّل دوال النسخ بنسخ الموارد من كائن لآخر بينما يتكفّل المدمّر بتدمير المورد وفقًا لمبادئ RAII. يقدّم المثال التالي نوعًا يدير السلاسل النصية: class Person { char *name; int age; public: Person(char const *new_name, int new_age): name(new char[std::strlen(new_name) + 1]), age(new_age) { std::strcpy(name, new_name); }~Person() { delete[] name; } }; وبما أن ذاكرة ‎name‎ مُخصّصة في المنشئ، فسيُلغي المدمّر تخصيصها لتجنّب تسرّب الذاكرة. لكن ماذا سيحدث في حال نُسِخ الكائن؟ int main() { Person p1("foo", 11); Person p2 = p1; } أولاً، سيُنشَأ ‎p1‎، ثم يُنسَخ ‎p2‎ من ‎p1‎. لكن مُنشئ النسخ المُولَّد من C++‎ سينسخ كل مكوّن من مكوّنات النوع كما هو، ممّا يعني أنّ كلًّا من ‎p1.name‎ و ‎p2.name‎ سيشيران إلى نفس السلسلة النصّية. وعند انتهاء ‎main‎ ستُستدعى المدمّرات ابتداءً بمدمّر ‎p2‎ الذي سيحذف السلسلة النصّية. ثم سيُستدعى مدمّر ‎p1‎. لكنّ السلسلة النصية قد حُذِفت سلفًا. سينتج سلوك غير محدد عند استدعاء ‎delete‎ على ذاكرة حُذِفت فعلًا، ويجب توفير مُنشئ نسخ مناسب لتجنّب هذا. وإحدى طرق ذلك هي تطبيق نظام عدٍّ للمراجع (reference counted system)، حيث تتشارك مختلف نُسخ ‎Person‎ نفس البيانات النصية، ويُزاد عدّاد المرجع المشترك عند كلّ عملية نسخ، ثم يُنقِص المدمّر بعدها عدّاد المرجع، ولا يُحرِّر الذاكرة إلّا إذا كان العدّاد يساوي الصفر. يمكننا أيضًا تطبيق الدلالات القيمية (value semantics) وسلوك النسخ العميق (deep copying): Person(Person const &other): name(new char[std::strlen(other.name) + 1]), age(other.age) { std::strcpy(name, other.name); } Person &operator=(Person const &other) { // استخدام أسلوب النسخ والمبادلة لتطبيق معامل الإسناد Person copy(other); swap(copy); // *this و copy تُبادِلُ محتويات swap() افتراض أن return * this; } تطبيق مُعامل إسناد النسخ معقّد بسبب الحاجة إلى تحرير مخزن مؤقّت (buffer)، وتنشئ تقنية النسخ والمبادلة كائنًا مؤقّتًا يحتفظ بالمخزن المؤقّت، ثم يمنح تبديل محتويات ‎*this‎ و ‎copy‎ مِلكِية المخزن المؤقّت لـ ‎copy‎، وعند تدمير ‎copy‎ -عند عودة الدالّة- فسيُحرَّر المخزن المؤقّت الذي يملكه ‎*this‎. الوقاية من الإسناد الذاتي عندما تكتب عامل إسناد نسخٍ فيجب أن تدرك أنه يجب أن يظل عاملًا في حالة حدوث إسناد ذاتي، أي يجب أن يسمح بما يلي: SomeType t = ...; t = t; لا يحدث الإسناد الذاتي عادة بهذه الطريقة، وإنما يحدث في مسار دائري (circuitous route) في أنظمة الشيفرات، إذ يكون لموضع الإسناد (location of the assignment) مؤشّران أو مرجعان يشيران إلى كائن من النوع ‎Person‎، دون أن يدركا أنّهما في الحقيقة يمثّلان نفس الكائن. ويجب أن يُصمَّم أي عامل إسناد نسخ تكتبه للتعامل مع هذا الأمر، والطريقة المعتادة لفعل ذلك هي بتغليف كامل منطق الإسناد في عبارة شرطية على النحو التالي: SomeType &operator=(const SomeType &other) { if (this != &other) { // منطق الإسناد هنا } return * this; } ملاحظة: من المهم أخذ الإسناد الذاتي في الحسبان، والحرص على أنّ شيفرتك ستتصرّف بالشكل الصحيح عند حدوثه. وبما أن الإسناد الذاتي أمر نادر الحدوث، وقد يؤدّي تحسين الشيفرة خصّيصًا لمنعه إلى التشويش على الحالة الطبيعية، فقد يؤدّي ذلك إلى تقليل كفاءة الشيفرة لأنّ الحالة العادية أكثر شيوعًا (لذا كن حذرًا عند استخدامه). على سبيل المثال، الأسلوب العادي لتقديم مُعامل الإسناد هو أسلوب النسخ والمبادلة ، لكن التطبيق العادي لهذه التقنية لا يكلف نفسه عناء التحقق من الإسناد الذاتي -رغم أنّ الإسناد الذاتي مكلّف لأنّه ينطوي على عمليّة نسخ- والسبب هو أن الاحتياط أثبت أنه أكثر كلفة بكثير من الإسناد الذاتي بما أنه يحدث بكثرة. الإصدار ≥ C++‎ 11 كذلك يجب وقاية مُعاملات إسناد النقل من الإسناد الذاتي، لكن يبنى منطق عديد من هذه العوامل على ‎std::swap‎، والتي تستطيع التعامل مع التبديل من/إلى نفس الذاكرة بلا مشاكل. لذا إذا كان منطق إسناد النقل الخاص بك يتألّف حصرًا من سلسلة من عمليات التبديل فلن تحتاج إلى الاحتياط من الإسناد الذاتي. أما خلاف هذا فيجب عليك اتخاذ تدابير مماثلة على النحو الوارد أعلاه. هذا الدرس جزء من سلسلة دروس عن C++‎. ترجمة -بتصرّف- للفصل Chapter 82: The Rule of Three, Five, And Zero من كتاب C++ Notes for Professionals
  24. كيفية إنشاء خيط std::thread تُنشَأ الخيوط في C++‎ باستخدام الصنف std::thread، والخيط (thread) هو مسار تنفيذ منفصل أشبه بمساعد يساعدك على أداء مهمة فرعية أثناء إنجازك لمهمة أخرى، ثم يتوقف عند اكتمال تنفيذ الشيفرة في الخيط. يجب أن تمرر شيفرة ما للخيط عند إنشائه لينفذها، مثل: الدوالّ الحرّة (Free functions). الدوال التابعة. الكائنات الدالية (Functor). تعبيرات لامدا. تكون المعاملات المُمرَّرة: المعامل التفاصيل other تأخذ ملكية ‎other‎، بحيث أنّ ‎other‎ تفقد ملكيّة الخيط (thread) func دالّة لأجل استدعائها في خيط منفصل args وسائط لـ ‎func‎ table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; } هذا مثال على تمرير دالّة حرّة لتُنفّذ في خيط منفصل: #include <iostream> #include <thread> void foo(int a) { std::cout << a << '\n'; } int main() { // إنشاء وتنفيذ الخيط تكون foo هنا هي الدالة محل التنفيذ، و10 هو الوسيط الممرر إليها، وسينفَذ الخيط الآن بشكل منفصل ويُنتَظر هنا حتى تمام تنفيذه، انظر: std::thread thread(foo, 10); thread.join(); return 0; } هذا مثال على استخدام تابع ليُنفّذ في خيط منفصل: #include <iostream> #include <thread> class Bar { public: void foo(int a) { std::cout << a << '\n'; } }; int main() { Bar bar; // إنشاء وتنفيذ الخيط std::thread thread(&Bar::foo, &bar, 10); // Pass 10 to member function التابع سيُنفَّذ الآن بشكل منفصل، سننتظر هنا حتى تمام تنفيذ الخيط، فهذه عملية معطِّلة (Blocking)، نتابع: thread.join(); return 0; } مثال على استخدام كائن دالّي: #include <iostream> #include <thread> class Bar { public: void operator()(int a) { std::cout << a << '\n'; } }; int main() { Bar bar; // انشاء الخيط وتنفيذه std::thread thread(bar, 10); // مرر 10 إلى الكائن الدالي. سيُنفَّذ الكائن الدالي الآن بشكل منفصل، سننتظر هنا حتى تمام تنفيذ الخيط، فهذه عملية معطِّلة (Blocking)، نتابع: thread.join(); return 0; } مثال على تمرير تعبير لامدا: #include <iostream> #include <thread> int main() { auto lambda =[](int a) { std::cout << a << '\n'; }; // انشاء الخيط وتنفيذه std::thread thread(lambda, 10); // تمرير 10 إلى تعبير لامدا سيُنفَّذ تعبير لامدا الآن بشكل منفصل، وسننتظر اكتمال تنفيذ الخيط، فهي عملية معطِّلة، نتابع: thread.join(); return 0; } تمرير مرجع إلى خيط لا يمكنك تمرير مرجع -أو مرجع ثابت ‎const‎- مباشرةً إلى خيط، لأنّ الخيط سينسخها/ينقلها، بل استخدم ‎std::reference_wrapper‎: void foo(int & b) { b = 10; } int a = 1; std::thread thread { foo, std::ref(a) }; الآن، a ممرَّرة على شكل مرجع، تابع المثال: thread.join(); std::cout << a << '\n'; // 10 void bar(const ComplexObject &co) { co.doCalculations(); } ComplexObject object; std::thread thread { bar, std::cref(object) }; أيضًا، object ممرَّر الآن على شكل &const، تابع: thread.join(); std::cout << object.getResult() << '\n'; استخدام std::async بدلاً من std::thread تستطيع ‎std‎::async أن تنشئ خيوطًا رغم أنها أضعف من ‎std‎::thread، لكنّها تتميّر بأنّها أسهل في حال كنت تريد تنفيذ دالة بشكل غير متزامن (asynchronously). استدعاء دالّة بشكل غير متزامن #include <future> #include <iostream> unsigned int square(unsigned int i) { return i * i; } int main() { auto f = std::async (std::launch::async, square, 8); std::cout << "square currently running\n"; // square افعل شيئا ما أثناء تنفيذ std::cout << "result is " << f.get() << '\n'; // square الحصول على النتيجة من } أخطاء شائعة تعيد std::async كائن std::future يحتوي القيمة المُعادة التي ستحسُبها الدالّة، وعند تدمير ‎future‎ فإنّها تنتظر حتى يكتمل الخيط ممّا يجعل الشيفرة أحادية الخيوط (single threaded). يُمكن تجاهَل هذا السلوك إذا لم تكن بحاجة إلى القيمة المُعادة: std::async (std::launch::async, square, 5); في الشيفرة السابقة، انتهى تنفيذ الخيط لأن قيمة future قد دُمِّرت. تعمل std::async بدون سياسة إطلاق (launch policy)، لذا فإنّ التعبير ‎std::async(square, 5);‎ سيُصرَّف. عندئذ يقرر النظام إن كان سينشئ خيطًا أم لا. والفكرة أنّ النظام سيختار إنشاء خيط إن لم يكن عدد الخيوط قيد التنفيذ أكبر ممّا يمكنه التعامل معه. لكن عادة ما تختار التنفيذات (implementations) عدم إنشاء خيط في مثل هذه المواقف، لذا ستحتاج إلى إعادة تعريف هذا السلوك باستخدام ‎std::launch::async‎، التي تجبر النظام على إنشاء الخيط. أساسيات التزامن بين الخيوط يمكن تحقيق تزامن الخيوط باستخدام كائنات المزامنة (mutexes)، وتوفّر المكتبة القياسية العديد من أنواع كائنات المزامنة تلك لكن أبسطها هو ‎std::mutex‎ وسنتحدث عن تلك الكائنات بالتفصيل في القسم التالي. ولقفل كائن مزامنة ستحتاج إلى إنشاء قفل (lock) خاصّ به، وأبسط أنواع الأقفال هو ‎std::lock_guard‎: std::mutex m; void worker() { std::lock_guard<std::mutex > guard(m); // يحصل على قفلٍ على كائن المزامنة // الشيفرة المُزامَنة هنا } // سيُحرَّر كائن المزامنة عندما يخرج الدرع عن النطاق سيُقفل كائن المزامنة باستخدام ‎std::lock_guard‎ طول العمر الافتراضي لكائن القفل، وإن أردت التحكم في المناطق المقفلة يدويًا، فاستخدم ‎std::unique_lock‎: std::mutex m; void worker() { افتراضيًا، إنشاء unique_lock من كائن مزامنة سيقفل ذلك الكائن، ونستطيع إنشاء درع في حالة مفتوحة عبر تمرير std::defer_lock كوسيط ثاني ثم نقفل يدويًا فيما بعد، تابع المثال: std::unique_lock<std::mutex > guard(m, std::defer_lock); // لم يُقفَل كائن المزامنة بعد guard.lock(); // شيفرة خاصة guard.unlock(); // تحرير كائن المزامنة مجددا } كائنات المزامنة Mutexes كائنات المزامنة هي بنيات مزامنة بسيطة غير تكرارية (non-recursive) تُستخدَم لحماية البيانات التي يمكن الوصول إليها من خيوط متعددة (multiple threads). std::atomic_int temp { 0 }; std::mutex _mutex; std::thread t([ &]() { while (temp != -1) { std::this_thread::sleep_for(std::chrono::seconds(5)); std::unique_lock<std::mutex > lock(_mutex); temp = 0; } }); while (true) { std::this_thread::sleep_for(std::chrono::milliseconds(1)); std::unique_lock<std::mutex > lock(_mutex, std::try_to_lock); if (temp < INT_MAX) temp++; cout << temp << endl; } أنواع كائنات المزامنة توفّر الإصدارات C++1x عددًا من أصناف كائنات المزامنة: std::mutex - توفّر وظائف القفل الأساسية. std::timed_mutex - توفّر وظائف try_to_lock std::recursive_mutex - تتيح القفل التكراري من قِبل نفس الخيط std::shared_mutex و std::shared_timed_mutex - توفّران وظائف قفل مشتركة وحصرية الأقفال std::lock تستخدِم الأقفال std::lock خوارزميات لتجنب الشلل الوظيفي (deadlock) من أجل قفل كائنات المزامنة. وعند رفع اعتراض أثناء استدعاء لقَفل عدة كائنات فستفتح ‎std::lock‎ الكائنات المُقفلة قبل إعادة رفع الاعتراض. std::lock(_mutex1, _mutex2); الأقفال الحصرية (std::unique_lock) والأقفال المشتركة (std::shared_lock) والأقفال المُؤمّنة ( std::lock_guard) تُستخدم هذه الأقفال مع آلية RAII للحصول على أقفال المحاولة (try locks)، وأقفال المحاولة الموقوتة (timed try locks)، والأقفال التكرارية (recursive locks). std::unique_lock - تسمح بالملكية الحصرية لكائنات المزامنة. std::shared_lock - تسمح بالملكية المشتركة لكائنات المزامنة، إذ يمكن لعدّة خيوط أن تحتفظ بقفل مشتركtd::shared_locks خاصّ بكائن مزامنة مشترك std::shared_mutex. وقد أتيح منذ C++‎ 14 std::lock_guard - بديل خفيف للأقفال الحصرية std::unique_lock والأقفال المشتركة std::shared_lock. #include <unordered_map> #include <mutex> #include <shared_mutex> #include <thread> #include <string> #include <iostream> class PhoneBook { public: std::string getPhoneNo(const std::string &name) { std::shared_lock<std::shared_timed_mutex > l(_protect); auto it = _phonebook.find(name); if (it != _phonebook.end()) return (*it).second; return ""; } void addPhoneNo(const std::string &name, const std::string &phone) { std::unique_lock<std::shared_timed_mutex > l(_protect); _phonebook[name] = phone; } std::shared_timed_mutex _protect; std::unordered_map<std::string, std::string > _phonebook; }; استراتيجيات قفل الأصناف: std::try_to_lock و std::adopt_lock و std::defer_lock لدينا ثلاث استراتيجيات لتختار منها عند إنشاء قفل حصري: ‎std::try_to_lock‎ و std::defer_lock و std::adopt_lock: std::try_to_lock - تسمح بمحاولة القفل (trying a lock) بدون تعطيل: { std::atomic_int temp { 0 }; std::mutex _mutex; std::thread t([ &]() { while (temp != -1) { std::this_thread::sleep_for(std::chrono::seconds(5)); std::unique_lock<std::mutex > lock(_mutex, std::try_to_lock); if (lock.owns_lock()) { //افعل شيئًا temp = 0; } } }); while (true) { std::this_thread::sleep_for(std::chrono::seconds(1)); std::unique_lock<std::mutex > lock(_mutex, std::try_to_lock); if (lock.owns_lock()) { if (temp < INT_MAX) { ++temp; } std::cout << temp << std::endl; } } } std::defer_lock - تسمح بإنشاء بنية قفل دون الحصول على القفل. ذلك أنه عند قفل أكثر من كائن مزامنة، فهناك إمكانية لحدوث شلل وظيفي إذا حاولت دالتان الحصول على الأقفال في نفس الوقت: { std::unique_lock<std::mutex > lock1(_mutex1, std::defer_lock); std::unique_lock<std::mutex > lock2(_mutex2, std::defer_lock); lock1.lock() lock2.lock(); // شلل وظيفي هنا std::cout << "Locked! << std::endl; //... } يمكن الحصول على الأقفال وإصدارها بالترتيب المناسب مع الشيفرة التالية، بغض النظر عما يحدث في الدالة: { std::unique_lock<std::mutex > lock1(_mutex1, std::defer_lock); std::unique_lock<std::mutex > lock2(_mutex2, std::defer_lock); std::lock(lock1, lock2); // لن يحدث شلل وظيفي. std::cout << "Locked! << std::endl; //... } std::adopt_lock - لا تحاول القفل مرّة ثانية إذا كان الخيط المُستدعي يملك القفل حاليًا. { std::unique_lock<std::mutex > lock1(_mutex1, std::adopt_lock); std::unique_lock<std::mutex > lock2(_mutex2, std::adopt_lock); std::cout << "Locked! << std::endl; //... } تذكّر أنّ std::adopt_lock ليست بديلاً عن استخدام كائنات المزامنة التكرارية، فسيحرَّر كائن المزامنة عند خروج القفل عن النطاق. الأقفال النطاقية std::scoped_lock ‏(C++ 17) توفّر الأقفال النطاقية std::scoped_lock دلالات RAII لامتلاك كائن مزامنة أو أكثر، وتُستخدم مع خوارزميات تجنّب الشلل الوظيفي التي تستخدمها الأقفال العادية ‎std::lock‎. وعندما تُدمَّر ‎std::scoped_lock‎، فإن كائنات المزامنة تُحرّر بالترتيب العكسي لترتيب الحصول عليها. { std::scoped_lock lock { _mutex1, _mutex2 }; // افعل شيئا ما } كائنات المزامنة التكرارية Recursive Mutex تسمح كائنات المزامنة التكرارية لخيط ما بقفل أحد الموارد بدون حد معين، ولا توجد مبررات كثيرة لاستخدام هذه التقنية، لكن قد تحتاج بعض التنفيذات (implementations) المعقّدة إلى استدعاء نسخة مُحمّلة تحميلا زائدًا (overloaded) من دالّة دون تحرير القفل. انظر المثال التالي: std::atomic_int temp { 0 }; std::recursive_mutex _mutex; تطلق launch_deferred مهامًا غير متزامنة على نفس معرِّف الخيط، تابع … auto future1 = std::async ( std::launch::deferred, [ &]() { std::cout << std::this_thread::get_id() << std::endl; std::this_thread::sleep_for(std::chrono::seconds(3)); std::unique_lock<std::recursive_mutex > lock(_mutex); temp = 0; }); auto future2 = std::async ( std::launch::deferred, [ &]() { std::cout << std::this_thread::get_id() << std::endl; while (true) { std::this_thread::sleep_for(std::chrono::milliseconds(1)); std::unique_lock<std::recursive_mutex > lock(_mutex, std::try_to_lock); if (temp < INT_MAX) temp++; cout << temp << endl; } }); future1.get(); future2.get(); هياكل مزامنة الخيوط قد يتطلّب استعمال الخيوط أن تستخدم بعض تقنيات المزامنة إذا كانت الخيوط تتفاعل مع بعضها، وسنتحدث في هذا الموضوع عن عدد من الهياكل التي توفّرها المكتبة القياسية لحل هذه المشكلة. std::condition_variable_any و std::cv_status ‎std::condition_variable_any‎ هي تعميم لـ‎std::condition_variable‎، ويمكن أن تعمل مع أيّ نوع من الهياكل الأساسية القابلة للقفل (BasicLockable structure). وstd::cv_status كقيمة مُعادة من متغيّر شرطي يكون لها رمزا إعادة (return codes) محتملان: std::cv_status::no_timeout: إن لم تكن هناك مهلة (timeout)، وتمّ إشعارالمتغيّر الشرطي. std::cv_status::timeout: عند انتهاء مهلة المتغيّر الشرطي. الأقفال المشتركة std::shared_lock يمكن استخدام الأقفال المشتركة مع قفل حصريّ (unique lock) من أجل السماح بعدّة قارئات (readers)، لكن مع كاتبات (writers) حصرية. #include <unordered_map> #include <mutex> #include <shared_mutex> #include <thread> #include <string> #include <iostream> class PhoneBook { public: string getPhoneNo(const std::string &name) { shared_lock<shared_timed_mutex> r(_protect); auto it = _phonebook.find(name); if (it == _phonebook.end()) return (*it).second; return ""; } void addPhoneNo(const std::string &name, const std::string &phone) { unique_lock<shared_timed_mutex> w(_protect); _phonebook[name] = phone; } shared_timed_mutex _protect; unordered_map<string, string> _phonebook; }; std::call_once و std::once_flag تضمن std::call_once ألّا تُنفّذ دالّة معيّنة إلّا مرّة واحدة فقط من قبل الخيوط المتنافسة (competing threads). وتطلق خطأ نظامي std::system_error في حال حدث ذلك. كذلك فإن std::call_once تُستخدم مع ‎td::once_flag‎. #include <mutex> #include <iostream> std::once_flag flag; void do_something(){ std::call_once(flag, [](){std::cout << "Happens once" << std::endl;}); std::cout << "Happens every time" << std::endl; } قفل الكائنات لتحسين كفاءة الوصول قد ترغب في قفل الكائن بالكامل أثناء إجراء عمليات متعدّدة عليه، كأن تريد فحصه أو تعديله باستخدام المُكرّرات. وإن كنت بحاجة إلى استدعاء عدّة توابع، فمن الأفضل عمومًا قفله بالكامل بدلاً قفل التوابع فرديّا، انظر: class text_buffer { // لأجل تحسين القراءة والصيانة using mutex_type = std::shared_timed_mutex; using reading_lock = std::shared_lock<mutex_type> ; using updates_lock = std::unique_lock<mutex_type> ; public: يعيد هذا قفلًا نطاقيًا (scoped lock) تستطيع عدة قارئات أن تتشاركه مع استثناء الكاتبات في نفس الوقت، تابع المثال … [[nodiscard]] reading_lock lock_for_reading() const { return reading_lock(mtx); } يعيد هذا قفلًا نطاقيًا خاصًا بكاتب واحد، مع منع القارئات، تابع … [[nodiscard]] updates_lock lock_for_updates() { return updates_lock(mtx); } char* data() { return buf; } char const* data() const { return buf; } char* begin() { return buf; } char const* begin() const { return buf; } char* end() { return buf + sizeof(buf); } char const* end() const { return buf + sizeof(buf); } std::size_t size() const { return sizeof(buf); } private: char buf[1024]; mutable mutex_type mtx; // للكائنات الثابتة بأن تُقفَل mutable يسمح }; يسمح mutable في السطر الأخير الشيفرة أعلاه بأن تُقفَل الكائنات الثابتة، ويُقفل الكائن عند حساب المجموع (checksum) من أجل القراءة، وهذا سيفسح المجال أمام الخيوط الأخرى التي ترغب في القراءة من الكائن في نفس الوقت بأن تقرأ منه. std::size_t checksum(text_buffer const &buf) { std::size_t sum = 0xA44944A4; // قفل الكائن لأجل القراءة auto lock = buf.lock_for_reading(); for (auto c: buf) sum = (sum << 8) | (((unsigned char)((sum & 0xFF000000) >> 24)) ^ c); return sum; } ويؤدّي مسح الكائن إلى تحديث بياناته الداخلية، لذا يجب فعل ذلك باستخدام قفل حصري. void clear(text_buffer & buf) { auto lock = buf.lock_for_updates(); // قفل حصري std::fill(std::begin(buf), std::end(buf), '\0'); } يجب توخي الحذر دائمًا عند الحصول على أكثر من قفل، والحرص على الحصول على الأقفال بنفس الترتيب لجميع الخيوط. void transfer(text_buffer const &input, text_buffer &output) { auto lock1 = input.lock_for_reading(); auto lock2 = output.lock_for_updates(); std::copy(std::begin(input), std::end(input), std::begin(output)); } ملاحظة: من الأفضل إنجاز ذلك باستخدام std::deferred::lock ثمّ استدعاء std::lock متغير تقييد الوصول متغير تقييد الوصول (Semaphore) غير متاح حاليًا في C++‎، ولكن يمكن تنفيذه بسهولة باستخدام كائنات المزامنة والمتغيّرات الشرطية. هذا المثال مأخوذ من: C++0x has no semaphores? How to synchronize threads متغيرات تقييد الوصول في C++‎ 11 انظر المثال التوضيحي التالي: #include <mutex> #include <condition_variable> class Semaphore { public: Semaphore(int count_ = 0): count(count_) {} inline void notify(int tid) { std::unique_lock<std::mutex > lock(mtx); count++; cout << "thread " << tid << " notify" << endl; //أشعِر الخيط المنتظِر. cv.notify_one(); } inline void wait(int tid) { std::unique_lock<std::mutex > lock(mtx); while (count == 0) { cout << "thread " << tid << " wait" << endl; // notify انتظر كائن المزامنة إلى حين استدعاء cv.wait(lock); cout << "thread " << tid << " run" << endl; } count--; } private: std::mutex mtx; std::condition_variable cv; int count; }; مثال على استخدام متغير تقييد الوصول تضيف الدالّة التالية أربعة خيوط، تتنافس ثلاثة منها على متغير تقييد الوصول الذي يُضبط عدّاده عند القيمة 1. وسيستدعي الخيط الأبطأ ‎notify_one()‎، ممّا يسمح لأحد الخيوط المنتظِرة بالمتابعة. ونتيجة لهذا تبدأ ‎s1‎ على الفور، مما سيُبقي عدّاد متغير تقييد الوصول ‎count‎ دون القيمة 1، وستنتظر الخيوط الأخرى دورها في المتغيّر الشرطي حتى استدعاء notify()‎‎. int main() { Semaphore sem(1); thread s1([ &]() { while (true) { this_thread::sleep_for(std::chrono::seconds(5)); sem.wait(1); } }); thread s2([ &]() { while (true) { sem.wait(2); } }); thread s3([ &]() { while (true) { this_thread::sleep_for(std::chrono::milliseconds(600)); sem.wait(3); } }); thread s4([ &]() { while (true) { this_thread::sleep_for(std::chrono::seconds(5)); sem.notify(4); } }); s1.join(); s2.join(); s3.join(); s4.join(); ... } إنشاء ساحة خيوط بسيطة خيوط C++‎ 11 الأساسية منخفضة المستوى نسبيًا، لذا يمكن استخدامها لكتابة كائنات عالية المستوى مثل ساحة الخيوط (thread pool): الإصدار ≥ C++‎ 14 يشكل كائن المزامنة والمتغير الشرطي وكائن deque طابورًا من المهام آمن على الخيوط (thread-safe): struct tasks { std::mutex m; std::condition_variable v; … لاحظ أن <packaged_task<void تستطيع تخزين <packaged_task<R … : std::deque< std::packaged_task < void() >> work; // هذا سيمثل العمل الذي أنجزته الخيوط std::vector<std::future < void>> finished; (queue (lambda سيضيف لامدا إلى قائمة المهام التي سينفذها الخيط …: template < class F, class R = std::result_of_t < F &() >> std::future<R> queue(F && f) { // لتقسيم التنفيذ - packaged task - تغليف كائن الدالة في مهمة محزومة std::packaged_task < R() > p(std::forward<F> (f)); auto r = p.get_future(); // الحصول على القيمة من المهمة قيد تنفيذ { std::unique_lock<std::mutex > l(m); سنخزن المهمة <()R> على شكل <()void>، تابع: work.emplace_back(std::move(p)); } v.notify_one(); // إيقاظ الخيط ليعمل على المهمة return r; // إعادة النتيجة المستقبلية للمهمة } والآن، نبدأ عدد N من الخيوط في ساحة الخيوط، نتابع المثال: void start(std::size_t N = 1) { for (std::size_t i = 0; i < N; ++i) { كل الخيوط الآن غير متزامنة std::async، وتنفذ ()this->thread_task، تابع: finished.push_back( std::async ( std::launch::async, [this] { thread_task(); } ) ); } } تلغي ()abort كل المهام التي لم تنطلق بعد، وإخطار كل الخيوط العاملة أن تتوقف، وتنتظرهم حتى ينتهوا، تابع: void abort() { cancel_pending(); finish(); } تلغي ()cancel_pending المهام التي لم تنطلق بعد: void cancel_pending() { std::unique_lock<std::mutex > l(m); work.clear(); } هنا نرسل رسالة "stop the thread" إلى جميع الخيوط، ثم نتظرها، تابع: void finish() { { std::unique_lock<std::mutex > l(m); for (auto && unused: finished) { work.push_back( {}); } } v.notify_all(); finished.clear(); }~tasks() { finish(); } private: //: العمل الذي يقوم به الخيط قيد التنفيذ void thread_task() { while (true) { // سحب مهمة من الطابور std::packaged_task < void() > f; { std::unique_lock<std::mutex > l(m); if (work.empty()) { v.wait(l, [& ] { return !work.empty(); }); } f = std::move(work.front()); work.pop_front(); } // إذا كانت المهمة غير صالحة، فسيكون علينا إلغاؤها if (!f.valid()) return; // خلاف ذلك، ينبغي تنفيذ المهمة f(); } } }; تعيد الدالة التالية: tasks.queue( []{ return "hello world"s; } ) ‎‎قيمة من النوع std::future<std::string>‎، والتي ستساوي عند تنفيذ كائن المهام السلسلة النصية ‎hello world‎. كذلك يمكنك إنشاء الخيوط عن طريق تنفيذ ‎tasks.start(10)‎ (والتي تطلق 10 خيوط). إن سبب استخدام ‎packaged_task<void()>‎هو أنّه لا يوجد قالب صنف ‎std::function‎‏ مكافئ ومشطوب النوع (type-erased)، ولا يخزّن إلّا أنواع النقل فقط (move-only types). أيضًا، قد تكون كتابة نوع مخصّص أسرع من استخدام ‎packaged_task<void()>‎. انظر هذا المثال الحيّ على ذلك. الإصدار = C++‎ 11 في C++‎ 11، استبدل ‎result_of_t<blah>‎ بـ ‎typename result_of<blah>::type‎. التحقق من أنّ الخيط مضموم دائمًا عند استدعاء مدمَّر ‎std::thread‎، يجب استدعاء ‎join()‎ أو ‎detach()‎. وإذا لم يُضمّ (joined) الخيط أو يُفصل (detached)، فستُستدعى ‎std::terminate‎ افتراضيًا. يمكن تسهيل هذا عبر استخدام RAII: class thread_joiner { public: thread_joiner(std::thread t): t_(std::move(t)) {} ~thread_joiner() { if (t_.joinable()) { t_.join(); } } private: std::thread t_; } ثم يمكن كتابة ما يلي: void perform_work() { // إنجاز عمل ما } void t() { thread_joiner j { std::thread(perform_work) }; // تنفيذ بعض الحسابات أثناء تنفيذ الخيط } // يُضمّ الخيط هنا تلقائيا يوفّر هذا أيضًا أمان الاعتراضات (exception safety)؛ ذلك أنّه إذا أنشأنا الخيط بشكل طبيعي ثمّ تسبّب العمل المُنجَز في ‎t()‎ برفع اعتراض، فلن تُستدعى ‎join()‎ على خيطنا، ولن تكتمل العملية. إجراء عمليات على الخيط الحالي std::this_thread هي فضاء اسم (namespace) يحتوي بعض الدوالّ التي يمكن استخدامها لإجراء عمليات معيّنة على الخيط الحالي من الدالّة التي استُدعِي منها. الدالة الوصف get_id تعيد معرِّف الخيط. sleep_for تجعل الخيط ينام لفترة محددة. sleep_until تجعل الخيط ينام "حتى" وقت محدد. yield إعادة جدولة الخيوط العاملة وإعطاء الأولوية لخيوط أخرى. يمكنك الحصول على معُرّف الخيط الحالي باستخدام ‎std::this_thread::get_id‎، انظر: void foo() { // اطبع معرّف الخيط std::cout << std::this_thread::get_id() << '\n'; } std::thread thread { foo }; thread.join(); // 12556 طُبِع معرّف الخيط الآن، وسيكون شيئا يشبه foo(); // 2420 طُبِع معرّف الخيط الرئيسي الآن، وسيكون شيئا يشبه النوم لمدة 3 ثوانٍ باستخدام ‎std::this_thread::sleep_for‎: void foo() { std::this_thread::sleep_for(std::chrono::seconds(3)); } std::thread thread { foo }; foo.join(); std::cout << "Waited for 3 seconds!\n"; النوم إلى أن تنقضي 3 ساعات باستخدام ‎std::this_thread::sleep_until‎: void foo() { std::this_thread::sleep_until(std::chrono::system_clock::now() + std::chrono::hours(3)); } std::thread thread { foo }; thread.join(); std::cout << "We are now located 3 hours after the thread has been called\n"; منح الأولوية لخيوط أخرى باستخدام ‎std::this_thread::yield‎: void foo(int a) { for (int i = 0; i<al++i) std::this_thread::yield(); // ستأخذ خيوط أخرى الأولوية الآن، لأنّ هذا الخيط لا يفعل أي شيء مهم std::cout << "Hello World!\n"; } std::thread thread { foo, 10 }; thread.join(); استخدام المتغيرات الشرطية Using Condition Variables المتغيّر الشرطي (condition variable) هو كائن أوّلي (primitive) يُستخدم مع كائن مزامنة (mutex) لتنظيم الاتصالات بين الخيوط. ورغم أنّها ليست الطريقة الوحيدة لفعل ذلك ولا الأكثر فعالية إلا أنّها تتميّز بالبساطة والسهولة. و يمكن انتظار المتغيرات الشرطية ‎std::condition_variable‎ عبر ‎std::unique_lock<std::mutex>‎. فهذا يسمح للشيفرة بفحص الحالة المشتركة (shared state) بأمان قبل تقرير ما إذا كان يجب متابعة عملية الحصول (acquisition) على القفل أم لا. يستخدم المثال أدناه ‎std::thread‎ و ‎std::condition_variable‎ و ‎std::mutex‎. #include <condition_variable> #include <cstddef> #include <iostream> #include <mutex> #include <queue> #include <random> #include <thread> int main() { std::condition_variable cond; std::mutex mtx; std::queue<int> intq; bool stopped = false; std::thread producer { [ &]() { جهّز مولد الأعداد العشوائية، وسيدفع هذا المولد أعدادًا عشوائية إلى intq، تابع المثال: std::default_random_engine gen {}; std::uniform_int_distribution<int> dist {}; std::size_t count = 4006; while (count--) { لابد من القفل قبل تغيير الحالة التي يحميها كائن المزامنة والمتغير الشرطي condition_variable، تابع: std::lock_guard<std::mutex > L { mtx }; // وضع العدد العشوائي في الطابور intq.push(dist(gen)); cond.notify_one(); } الآن تم كل شيء، احصل على القفل وعين راية الإيقاف stopped ثم نبه المستخدم، تابع … : std::lock_guard<std::mutex > L { mtx }; std::cout << "Producer is done!" << std::endl; stopped = true; cond.notify_one(); } }; std::thread consumer { [ &]() { do { std::unique_lock<std::mutex > L { mtx }; cond.wait(L, [& ]() { // الاستحواذ على القفل في حال الانتهاء أو في حال لم يكن الطابور فارغا return stopped || !intq.empty(); }); // نحن نملك كائن المزامنة هنا // سحب العناصر من الطابور إلى أن يصبح فارغا while (!intq.empty()) { const auto val = intq.front(); intq.pop(); std::cout << "Consumer popped: " << val << std::endl; } if (stopped) { std::cout << "Consumer is done!" << std::endl; break; } } while (true); } }; consumer.join(); producer.join(); std::cout << "Example Completed!" << std::endl; return 0; } عمليات الخيوط Thread operations عندما يبدأ تنفيد خيط معيّن، فسيُنفّذ إلى أن يكتمل، لكن قد تحتاج أحيانًا إلى انتظار اكتمال تنفيذ خيط ما إن كنت تريد استخدام النتيجة التي يعيدها. على سبيل المثال: int n; std::thread thread { calculateSomething, std::ref(n) }; // افعل أشياء أخرى … نحن نحتاج n الآن، انتظر الخيط إلى أن ينتهي، إن لم يكن قد انتهى فعلًا، وستكون قيمة n بعدها هي النتيجة المحسوبة في خيط آخر، تابع: thread.join(); std::cout << n << '\n'; يمكنك أيضًا فصل (‎detach‎) الخيط، والسماح بأن يُنفّذ بحرّية: std::thread thread { doSomething }; //فصل الخيط، فنحن لا نريده بعد الآن thread.detach(); // سيتم إنهاء الخيط عند اكتمال تنفيذه، أو عند عودة الخيط الرئيسي التخزين المحلي للخيوط يمكن إنشاء خيوط مُخزّنة محليًا باستخدام الكلمة المفتاحية ‎thread_local‎، والمتغيّرات التي يُصرّح عنها بالمحدّد ‎thread_local‎ يُقال أنّ لها مدة تخزين خيطية (thread storage duration). كل خيط في البرنامج له نسخته الخاصّة من كل متغيّر محلي في الخيط (thread-local variable). سيُهيّأ متغيّر الخيط المحلي الموجود في نطاق دالة (محلّية) بمجرّد تمرير التحكّم إلى تعريفها. هذا المتغيّر سيكون ساكنًا ضمنيًا ما لم يُصرّح عنه عبر ‎extern‎. ستُهيّأ متغيّرات الخيط المحلي الموجودة في نطاق فضاء اسم أو نطاق صنف -غير محلي- عند بدء تنفيذ الخيط. تُدمّر متغيّرات الخيط المحلي عند اكتمال تنفيذ الخيط. لا يمكن لأعضاء صنف معيّن أن تكون محلية في الخيط (thread-local) إلا إن كانت ساكنة، وعندها ستكون هناك نسخة واحدة من ذلك المتغيّر لكل خيط، بدلاً من نسخة واحدة لكل زوج (نُسخة، خيط) [(thread, instance)]. انظر: void debug_counter() { thread_local int count = 0; Logger::log("This function has been called %d times by this thread", ++count); } إعادة إسناد كائنات الخيوط يمكننا إنشاء كائنات خيوط فارغة (empty thread objects)، وإسناد مهامّ معيّنة إليها لاحقًا. وإذا أسندت كائن خيط إلى خيط آخر نشط وقابل للضمّ ‎joinable‎، فستُستدعى ‎std::terminate‎ تلقائيًا قبل استبدال الخيط. انظر: #include <thread> void foo() { std::this_thread::sleep_for(std::chrono::seconds(3)); } // إنشاء 100 كائن خيطي فارغ std::thread executors[100]; // شيفرة هنا // إنشاء بعض الخيوط for (int i = 0; i < 100; i++) { // إذا لم يُسنَد خيط إلى هذا الكائن if (!executors[i].joinable()) executors[i] = std::thread(foo); } الآجال والوعود (Futures and Promises) تُستخدم الآجال (Futures) والوعود لنقل كائن من خيط إلى آخر. يُضبط كائن الوعود ‎std::promise‎ من قِبل الخيط الذي يولّد النتيجة. يُستخدم كائن ‎std::future‎ لاسترداد قيمة، أو التحقّق من إتاحة قيمة ما، أو لإيقاف التنفيذ إلى حين إتاحة القيمة. أصناف العمليات غير المتزامنة std::async : تنفذ عملية غير متزامنة. std::future : توفّر وصولًا إلى نتيجة عملية غير متزامنة. std::promise : تحزِم نتيجة العملية غير المتزامنة. std::packaged_task : تربط دالّة مع الوعد المرتبط بها في نوع القيمة المُعادة. ينشئ المثال التالي وعدًا لكي يستخدمه خيط آخر: { auto promise = std::promise<std::string > (); auto producer = std::thread([ &] { promise.set_value("Hello World"); }); auto future = promise.get_future(); auto consumer = std::thread([ &] { std::cout << future.get(); }); producer.join(); consumer.join(); } مثال مؤجل غير متزامن تقدّم الشيفرة التالية نسخة من ‎std::async‎، إلّا أنّها تتصرف كما لو كانت ‎async‎ تُستدعى عبر سياسة الإطلاق المؤجّلة ‎deferred‎. ليس لهذه الدالّة أيضًا سلوك الأجل (‎future‎) الخاص بـ ‎async‎، ويمكن تدمير القيمة المستقبلية (‎future‎) المُعادة قبل الحصول على قيمتها. template < typename F> auto async_deferred(F&& func)->std::future < decltype(func()) > { using result_type = decltype(func()); auto promise = std::promise<result_type> (); auto future = promise.get_future(); std::thread(std::bind([=](std::promise<result_type>& promise) { try { promise.set_value(func()); لاحظ أن هذا لن يعمل مع <std::promise <void إذ يحتاج برمجة القوالب الوصفية (meta-template programming)، تابع المثال … which is out of scope for this example. } catch (...) { promise.set_exception(std::current_exception()); } }, std::move(promise))).detach(); return future; } std::packaged_task و std::future تحزم std::packaged_task دالّة مع الوعد المرتبط بها في نوع القيمة المُعادة: template < typename F> auto async_deferred(F&& func)->std::future < decltype(func()) > { auto task = std::packaged_task < decltype(func())() > (std::forward<F> (func)); auto future = task.get_future(); std::thread(std::move(task)).detach(); return std::move(future); } يبدأ الخيط في العمل فورًا، ونستطيع فصله أو ضمّه في نهاية النطاق، وتكون النتيجة جاهزة عند انتهاء استدعاء الدالّة لـ std::thread finishes. لاحظ أنّ هذا يختلف قليلاً عن ‎std::async‎، إذ أنّه عند تدمير الأجل ‎std::future‎ المُعاد، فسيُعطّل (block) إلى أن ينتهي الخيط. std::future_error و std::future_errc إذا لم تُستوفى قيود الوعود (std::promise) والآجال (std::future)، فسيُطرَح استثناء من النوع std::future_error. وسيكون رمز الخطأ في الاستثناء من النوع std::future_errc، وستكون القيم على النحو التالي: enum class future_errc { broken_promise = /* لم تعُد المهمّة مُشتركة */, future_already_retrieved = /* تم استرداد القيمة المُعادة سلفا */, promise_already_satisfied = /* الإجابة خُزِّنت سلفا */, no_state = /* محاولة الدخول إلى وعد في حالة غير مشتركة */ }; انظر الأمثلة التوضيحية التالية: الوعود غير النشطة (Inactive promise) int test() { std::promise<int> pr; return 0; // ok تعيد } الوعود النشطة غير المستخدمة: int test() { std::promise<int> pr; auto fut = pr.get_future(); // تعطيل إلى أجل غير مسمى. return 0; } الاسترجاع المزدوج Double retrieval int test() { std::promise<int> pr; auto fut1 = pr.get_future(); try { auto fut2 = pr.get_future(); // future محاولة ثانية للحصول على return 0; } catch (const std::future_error& e) { cout << e.what() << endl; // Error: "The future has already been retrieved from the promise or packaged_task." return -1; } return fut2.get(); } تعيين قيمة الوعد مرتين: int test() { std::promise<int> pr; auto fut = pr.get_future(); try { std::promise<int> pr2(std::move(pr)); pr2.set_value(10); pr2.set_value(10); // محاولة ثانية لتعيين قيمة الوعد، وهذا سيؤدي إلى رفع اعتراض. } catch (const std::future_error& e) { cout << e.what() << endl; // Error: "The state of the promise has already been // set." return -1; } return fut.get(); } std::future و std::async تُستخدَم ‎std::async‎ في مثال الترتيب المتوازي التالي لإطلاق عدّة مهام merge_sort متوازية، وتُستخدَم std::future لانتظار النتائج ومزامنتها: #include <iostream> using namespace std; void merge(int low, int mid, int high, vector<int> &num) { vector<int> copy(num.size()); int h, i, j, k; h = low; i = low; j = mid + 1; while ((h <= mid) && (j <= high)) { if (num[h] <= num[j]) { copy[i] = num[h]; h++; } else { copy[i] = num[j]; j++; } i++; } if (h > mid) { for (k = j; k <= high; k++) { copy[i] = num[k]; i++; } } else { for (k = h; k <= mid; k++) { copy[i] = num[k]; i++; } } for (k = low; k <= high; k++) swap(num[k], copy[k]); } void merge_sort(int low, int high, vector<int> &num) { int mid; if (low < high) { mid = low + (high - low) / 2; auto future1 = std::async (std::launch::deferred, [& ]() { merge_sort(low, mid, num); }); auto future2 = std::async (std::launch::deferred, [& ]() { merge_sort(mid + 1, high, num); }); future1.get(); future2.get(); merge(low, mid, high, num); } } ملاحظة: في المثال أعلاه، تُطلَق ‎std::async‎ وفق سياسة ‎std::launch_deferred‎، وذلك لتجنّب إنشاء خيط جديد في كل استدعاء، فتُجرى في مثالنا السابق استدعاءات ‎std::async‎ دون ترتيب، إذ أنّها تُزامن في استدعاءات ‎std::future::get()‎. بالمقابل، تفرض std::launch_async إنشاء خَيط جديد في كل استدعاء. السياسة الافتراضية هي ‎std::launch::deferred| std::launch::async‎، ممّا يعني أن التقديم سيكون هو المسؤول عن تحديد سياسة إنشاء الخيوط الجديدة. التزامن عبر OpenMP يغطي هذا الموضوع أساسيات التزامن في C++‎ باستخدام OpenMP، والتوازي أو التزامن يعني تنفيذ الشيفرة في نفس الوقت. OpenMP: الأقسام المتوازية يوضّح هذا المثال أساسيات تنفيذ أقسام (sections) من الشيفرة بالتوازي. ستعمل ميزة OpenMP على كل المصرِّفات المدعومة دون الحاجة إلى تضمين مكتبات بما أنها مبنية مسبقًا في المصرفات، لكن قد ترغب في تضمين ‎omp.h‎ إذا كنت تريد استخدام ميزات الواجهة البرمجية لـ openMP. انظر المثال التوضيحي التالي: ستلمِّح تعليمة pragna للمصرف أن المحتوى الموجود داخل { } سينفَّذ في أقسام متوازية باستخدام OpenMP، وسيولِّد المصرِّف هذه الشيفرة من أجل تنفيذها بالتوازي ... std::cout << "begin "; #pragma omp parallel sections { وهنا تلمِّح تعليمة pragma للمصرف أن هذا القسم يمكن تنفيذه بالتوازي مع بقية الأقسام، وسينفَّذ كل قسم في خيط منفصل. سيولِّد المصرِّف هذه الشيفرة لتنفيذها بالتوازي. لاحظ الفرق بين كلمة "قسم" هنا، و"أقسام" في الجزء السابق أعلاه من المثال، نتابع ... #pragma omp section { std::cout << "hello " << std::endl; /** افعل شيئا ما **/ } #pragma omp section { std::cout << "world " << std::endl; /** افعل شيئا ما **/ } } // لن يُنفّذ هذا السطر حتى تنتهي كل الأقسام أعلاه std::cout << "end" << std::endl; الخرج: هناك نتيجتان محتمَلتان يمكن أن تنتُجا عن هذا المثال، وذلك اعتمادًا على نظام التشغيل والأجهزة المستخدمة. يوضّح الخرج أيضًا مشكلة حالة التسابق (race condition problem) التي قد تحدث نتيجة لمثل هذا التقديم. الخرج أ الخرج ب begin hello world end begin world hello end OpenMP: الأقسام المتوازية يوضّح هذا المثال كيفية تنفيذ أجزاء من الشيفرة بالتوازي: std::cout << "begin "; // بداية الأقسام المتوازية #pragma omp parallel sections { // تنفيذ الأقسام بالتوازي #pragma omp section { ... do something ... std::cout << "hello "; } #pragma omp section { ... افعل شيئا ما ... std::cout << "world "; } #pragma omp section { ... افعل شيئا ما ... std::cout << "forever "; } } // نهاية الأقسام std::cout << "end"; نظرًا لأنّ ترتيب التنفيذ غير مضمون، فقد ترى أيًّا من المخرجات التالية: begin hello world forever end begin world hello forever end begin hello forever world end begin forever hello world end OpenMP: التوازي في الحلقات يوضّح هذا المثال كيفية تقسيم حلقة إلى أجزاء متساوية وتنفيذها بشكل متوازي، سنقسم متجه العنصر إلى ()element.size و Thread Qty ثم نخصص نطاق كل خيط على حدة، انظر ... #pragma omp parallel for for (size_t i = 0; i < element.size(); ++i) element[i] = ... مثال على تخصيص 100 عنصر لكل خيط: خيط 1: 0~99. خيط 1: 100~199. خيط 1: 200~299. وهكذا .. تستمر العملية كلها عند إكمال الخيوط كلها للمهمة التي أوكلت إليها من الحلقة. يرجى توخي الحذر الشديد وعدم تعديل حجم المتجه المستخدم في حلقة for المتوازية لأنّ فهارس النطاق المُخصّص لا تُحدَّث تلقائيًا. OpenMP: التجميع / الاختزال المتوازي يوضّح هذا المثال مفهوم الاختزال (reduction) أو التجميع (gathering) باستخدام المتجهات (‎std::vector‎) و OpenMP. سنفترض أنّنا نحتاج عدّة خيوط لمساعدتنا على توليد مجموعة كبيرة من الأشياء، وقد استخدمنا ‎int‎ هنا للتبسيط، بيْد أنّه يمكن استبدالها بأنواع بيانات أخرى. سيكون هذا مفيدًا عندما تحتاج إلى دمج نتائج من المهام الفرعية (slaves) لتجنّب أخطاء الأجزاء (segement faults)، أو انتهاكات الوصول إلى الذاكرة، وفي نفس الوقت لا ترغب في استخدام المكتبات، أو مكتبات حاويات مخصّصة للمزامنة. // المتجه الرئيسي // نريد متجهًا يضم النتائج المجموعة من المهام الفرعية std::vector < int > Master; // تلميح للمصرّف لكي ينفّذ الكتلة { } بالتوازي في كل الخيوط المتاحة #pragma omp parallel { // في هذه المنطقة، يمكنك كتابة أي شيفرة تريد لكل خيط فرعي // في هذه الحالة، هي متجهة لتخزين كل نتائجها // ليس علينا أن نقلق من عدد الخيوط المُنشأة أو مما إذا كنا نريد تكرار هذا التصريح هنا std::vector < int > Slave; // أخبر المصرف هنا أن يستخدم هنا كل الخيوط المخصصة لهذا الحيز المتوازي لتنفيذ هذه الحلقة على مراحل // 1000000 / Thread Qty الفعلية هي الحِمل الفعلي هي // المصرّفَ ألا ينتظر انتهاء جميع المهام الفرعية nowait وتخبر الكلمة المفتاحية #pragma omp for nowait for (size_t i = 0; i < 1000000; ++i { /* افعل شيئا ما */ .... Slave.push_back(...); } // ستنفّذ المهام الفرعية التي تنهي هذا الجزء من العمل الخيوطَ واحدًا تلو الآخر // تضمن الأقسام الحرجة أنّ خيطا واحدا على الأكثر سينفِّذ الكتلة { } في كل مرة #pragma omp critical { // دمج المهمة الفرعية مع الرئيسية. // استخدم مكرِّرات النقل بدلًا من ذلك، وتجنب النسخ إلا إن كنت تريد استخدامه بعد هذا القسم. Master.insert(Master.end(), std::make_move_iterator(Slave.begin()), std::make_move_iterator(Slave.end())); } } هذا الدرس جزء من سلسلة دروس عن C++‎. ترجمة -بتصرّف- للفصول: Chapter 80: Threading Chapter 85: Mutexes Chapter 86: Recursive Mutex Chapter 87: Semaphore Chapter 88: Futures and Promises Chapter 131: Concurrency With OpenMP من كتاب C++ Notes for Professionals
  25. صار بالإمكان قولبة الأصناف والدوالّ والمتغيّرات في لغة ++C منذ C++‎ 14، والقالب هو شيفرة لها بعض المعاملات الحرّة (free parameters) التي ستُستبدَل فيما بعد بأصناف أو دوال أو متغيّرات حقيقية عند تحديد تلك المعاملات. وقد تكون المعاملات أنواعًا أو قيمًا أو قوالب بحد ذاتها، ومن أمثلة القوالب: المتجهات (‎std::vector‎)، التي تصبح أنواع حاويات حقيقية عند تحديد نوع العنصر، كما في ‎std::vector<int>‎. قالب صنف أولي Basic Class Template الفكرة الأساسية لقالب الصنف هي أنّ مُعامل القالب سيُستبدَل بنوع معيّن في وقت التصريف، ونتيجة لذلك يمكن استخدام نفس الصنف مع عدّة أنواع. يحدّد المستخدمُ النوعَ الذي سيُستخدَم عند التصريح عن متغيّر من ذلك الصنف، ولدينا ثلاثة أمثلة على ذلك: #include <iostream> using std::cout; template < typename T > // صنف بسيط لاحتواء عدد من أيّ نوع class Number { public: void setNum(T n); // ضبط حقل الصنف عند العدد المُعطى T plus1() const; // "follower" يعيد حقل الصنف private: T num; // حقل من الصنف }; template < typename T > // ضبط حقل الصنف عند العدد المُعطى void Number<T>::setNum(T n) { num = n; } template < typename T > // "follower" إعادة T Number<T>::plus1() const { return num + 1; } int main() { Number<int> anInt; // (في الصنف T ستستبدل int) إجراء اختبار مع عدد صحيح anInt.setNum(1); cout << "My integer + 1 is " << anInt.plus1() << "\n"; // 2 يطبع Number<double> aDouble; // double الاختبار بعدد من النوع aDouble.setNum(3.1415926535897); cout << "My double + 1 is " << aDouble.plus1() << "\n"; // يطبع 4.14159 Number<float> aFloat; // الاختبار بعدد عشري aFloat.setNum(1.4); cout << "My float + 1 is " << aFloat.plus1() << "\n"; // يطبع 2.4 return 0; } قوالب الدوال Function Templates يمكن تطبيق القولبة على الدوالّ (والهياكل التقليدية الأخرى)، انظر المثال التالي حيث تمثل T نوعًا مجهولًا، ويكون كلا الوسيطين من نفس النوع: template < typename T> void printSum(T add1, T add2) { std::cout << (add1 + add2) << std::endl; } يمكن استخدامها بنفس الطريقة التي تستخدم بها قوالب الهياكل (structure templates). printSum<int> (4, 5); printSum<float> (4.5 f, 8.9 f); يُستخدَم وسيط القالب في كلتا الحالتين لاستبدال أنواع المعاملات؛ وستعمل النتيجة بشكل مشابه لدوالّ C++‎ العادية، فإذا لم تتطابق المعاملات مع نوع القالب، فإنّ المٌصرّف سيطبّق التحويلات القياسية. إحدى الخصائص الإضافية لدوالّ القوالب -على عكس أصناف القوالب- هي أنّ المُصرّف يمكنه استنتاج معاملات القالب بناءً على المعاملات المُمرّرة إلى الدالّة. في الحالة التالية يكون كلا المعاملين من نوع int، وذلك يتيح للمصرف استنباط النوع، وتكون T مساوية لـ int: printSum(4, 5); في هذه الحالة يكون المعاملان من نوعين مختلفين، ويعجز المصرف عن استنتاج نوع T بسبب وجود تناقضات، ونتيجة لهذا يحدث خطأ في التصريف. printSum(5.0, 4); تتيح لنا هذه الميزة تبسيط الشيفرة عند الجمع بين بنيات ودوالّ القالب. يوجد نمط شائع في المكتبة القياسية يسمح لنا بجعل ‎template structure X‎ تستخدم دالّة مساعدة ‎make_X()‎. انظر المثال التالي الذي يوضح كيف يبدو نمط make_X: هيكل قالب مع نوع قالب واحد أو أكثر: template < typename T1, typename T2> struct MyPair { T1 first; T2 second; }; دالة make لها نوع لكل معاملات القوالب في هيكل القالب template < typename T1, typename T2> MyPair<T1, T2> make_MyPair(T1 t1, T2 t2) { return MyPair<T1, T2> { t1, t2 }; } كيف يساعد هذا؟ انظر ما يلي حيث يكون val1 و val2 من نفس النوع: auto val1 = MyPair<int, float> { 5, 8.7 }; // إنشاء الكائن يعرّف الأنواع صراحة auto val2 = make_MyPair(5, 8.7); // إنشاء الكائن باستخدام أنواع المعاملات ملاحظة: لم يُصمّم هذا لاختصار الشيفرة وإنّما صُمِّم لجعلها أكثر متانة إذ يسمح بتغيير الأنواع عن طريق تغيير الشيفرَة في مكان واحد وليس في مواقع متعدّدة. قوالب التعبير Expression templates قوالب التعابير هي تقنية تحسين (optimization) في وقت التصريف تُستخدم في الغالب في الحوسبة العلمية (scientific computing)، والغرض الرئيسي منها هو تجنّب إهدار الوقت وتحسين الحسابات باستخدام مسار واحد -عادةً عند إجراء عمليات على المجاميع العددية-. في البداية، تُقسَّم قوالب التعابير من أجل التحايل على الفوارق الناجمة عن التحميل الزائد عند تنفيذ أنواع المصفوفات (‎Array‎) أو المصفوفات المتعددة (‎Matrix‎). ويجب أن تفهم الهدف من قوالب التعبير أولًا قبل الغوص فيها، انظر الصنف Matrix الوارد في المثال أدناه: template < typename T, std::size_t COL, std::size_t ROW> class Matrix { public: using value_type = T; Matrix(): values(COL *ROW) {} static size_t cols() { return COL; } static size_t rows() { return ROW; } const T &operator()(size_t x, size_t y) const { return values[y *COL + x]; } T &operator()(size_t x, size_t y) { return values[y *COL + x]; } private: std::vector<T> values; }; template < typename T, std::size_t COL, std::size_t ROW> Matrix<T, COL, ROW> operator+(const Matrix<T, COL, ROW> &lhs, const Matrix<T, COL, ROW> &rhs) { Matrix<T, COL, ROW> result; for (size_t y = 0; y != lhs.rows(); ++y) { for (size_t x = 0; x != lhs.cols(); ++x) { result(x, y) = lhs(x, y) + rhs(x, y); } } return result; } يمكنك الآن كتابة تعبيرات Matrix على النحو: const std::size_t cols = 2000; const std::size_t rows = 1000; Matrix<double, cols, rows> a, b, c; // a, b &c هيئ for (std::size_t y = 0; y != rows; ++y) { for (std::size_t x = 0; x != cols; ++x) { a(x, y) = 1.0; b(x, y) = 2.0; c(x, y) = 3.0; } } Matrix<double, cols, rows> d = a + b + c; // d(x, y) = 6 كما هو مُوضّح أعلاه، فتستطيع توفير صيغة تحاكي الصيغة الرياضية المعتادة للمصفوفات عبر التحميل الزائد للعامل ‎operator+()‎، لكن التنفيذ السابق غير فعّال مقارنةً بالإصدارات المكافئة "المصنوعة يدويًا". ولفهم السبب، عليك مراعاة ما يحدث عند كتابة تعبير مثل ‎Matrix d = a + b‎ + c‎، يُنشر هذا التعبير في الواقع إلى التعبير ‎((a + b) + c)‎، أو ‎operator+(operator+(a, b), c)‎، أي تُنفّذ الحلقة الموجودة داخل operator+()‎ مرّتين، بينما كان من الممكن إجراؤها مرّة واحدة. هذا يؤدّي أيضًا إلى إنشاء كائنين مؤقّتين، مما يضعف الأداء أكثر. ويبدو أنّ المرونة التي حصلنا عليها والناتجة عن استخدام صياغة قريبة للصياغة المعمول بها في الرياضيات كانت على حساب الأداء. على سبيل المثال، تستطيع تنفيذ تجميع مصفوفةٍ بأسلوب أفضل بدون التحميل الزائد للعامل، وباستخدام مسار تمرير واحد: template < typename T, std::size_t COL, std::size_t ROW> Matrix<T, COL, ROW> add3(const Matrix<T, COL, ROW> &a, const Matrix<T, COL, ROW> &b, const Matrix<T, COL, ROW> &c) { Matrix<T, COL, ROW> result; for (size_t y = 0; y != ROW; ++y) { for (size_t x = 0; x != COL; ++x) { result(x, y) = a(x, y) + b(x, y) + c(x, y); } } return result; } لكنّ المثال السابق له عيوبه، لأنّه ينشئ واجهة أكثر تعقيدًا للصنف Matrix، إذ سيكون عليك اعتبار توابع من قبيل: ‎Matrix::add2()‎ و ‎Matrix::AddMultiply()‎. بدلاً من ذلك، دعنا نرجع خطوة إلى الوراء ونرى كيف يمكننا تكييف التحميل الزائد للمُعامل لأجل تحسين الأداء. تنبع المشكلة من حقيقة أنّ التعبير ‎Matrix d = a + b + c‎ يُقيَّم قبل إنشاء شجرة التعبير بأكملها، أي ما نريده حقًا هو تقييم ‎a + b + c‎ مرّة واحدة، وفقط عندما تحتاج إلى إسناد التعبير الناتج إلى ‎d‎. وتلك هي الفكرة الأساسية وراء قوالب التعبير: بدلاً من أن يُقيّم المعامل ‎operator+()‎ نتيجة إضافة نسختين من Matrix على الفور، فسيُعيد "قالبَ تعبير" لتقييمه مستقبلًا بعد الانتهاء من بناء شجرة التعبير بأكملها. على سبيل المثال، فيما يلي تنفيذ ممكن لقالب تعبير يتوافق مع جمع عنصرين من نوعين مختلفين: template < typename LHS, typename RHS> class MatrixSum { public: using value_type = typename LHS::value_type; MatrixSum(const LHS &lhs, const RHS &rhs): rhs(rhs), lhs(lhs) {} value_type operator()(int x, int y) const { return lhs(x, y) + rhs(x, y); } private: const LHS &lhs; const RHS &rhs; }; وهذه هي النسخة المُحدثة من ‎operator+()‎: template < typename LHS, typename RHS> MatrixSum<LHS, RHS> operator+(const LHS &lhs, const LHS &rhs) { return MatrixSum<LHS, RHS> (lhs, rhs); } كما ترى، لا يعيد ‎operator+()‎ "تقييمًا مُتسرّعًا" بعد الآن لنتيجَةِ إضافة نسختين من Matrix (والتي ستكون نسخة أخرى من Matrix)، ولكنه يعيد قالب تعبير يمثّل عملية الإضافة. ربما يجب أن تتذكّر أنّ التعبير لم يُقيَّم بعد، وإنما يخزّن مراجع إلى معاملاته وحسب. وفي الحقيقة، لا شيء يمنعك من إنشاء قالب تعبير ‎MatrixSum<>‎ كما يلي: MatrixSum<Matrix < double>, Matrix<double>> SumAB(a, b); يمكنك تقييم التعبير ‎d =‎‎a + ‎b‎ في مرحلة لاحقة، حين تحتاج فعليًا إلى نتيجة الجمع، كما يلي: for (std::size_t y = 0; y != a.rows(); ++y) { for (std::size_t x = 0; x != a.cols(); ++x) { d(x, y) = SumAB(x, y); } } كما ترى، هناك فائدة أخرى من استخدام قوالب التعبير وهي أنك ستتمكّن من تقييم مجموع ‎a‎ و ‎b‎ وإسناده إلى ‎d‎ مرّة واحد. أيضًا لا شيء يمنعك من الجمع بين عدّة قوالب تعبير، فقد يُنتج ‎a + b + c‎ قالب التعبير التالي: MatrixSum<MatrixSum<Matrix<double>, Matrix<double> >, Matrix<double>> SumABC(SumAB, c); وهنا، مرّة أخرى، يمكنك تقييم النتيجة النهائية مرّة واحدة: for (std::size_t y = 0; y != a.rows(); ++y) { for (std::size_t x = 0; x != a.cols(); ++x) { d(x, y) = SumABC(x, y); } } أخيرًا، آخر قطعة من اللغز هي توصيل قالب التعبير الخاص بك بالصنف ‎Matrix‎، من خلال تنفيذ العامل ‎Matrix::operator=()‎، الذي يأخذ قالب التعبير كوسيط ويقيِّمه في تمريرة واحدة كما فعلتَ "يدويًا" من قبل: template < typename T, std::size_t COL, std::size_t ROW> class Matrix { public: using value_type = T; Matrix(): values(COL *ROW) {} static size_t cols() { return COL; } static size_t rows() { return ROW; } const T &operator()(size_t x, size_t y) const { return values[y *COL + x]; } T &operator()(size_t x, size_t y) { return values[y *COL + x]; } template < typename E> Matrix<T, COL, ROW> &operator=(const E &expression) { for (std::size_t y = 0; y != rows(); ++y) { for (std::size_t x = 0; x != cols(); ++x) { values[y *COL + x] = expression(x, y); } } return * this; } private: std::vector<T> values; }; هياكل بيانات قالب متغاير الإصدار ≥ C++‎ 14 من المفيد أحيانًا تعريف أصناف أو بنيات متغايرة لا يُعرَّف عدد حقولها وأنواعها إلا في وقت التصريف، ويُعدُّ ‎std::tuple‎ أحد الأمثلة الأساسية على ذلك، لكن قد تحتاج أحيانًا إلى تعريف هياكلك المخصّصة. انظر المثال التالي الذي يعرّف بنية باستخدام التركيب (compounding) بدلاً من الوراثة كما هو الحال في ‎std::tuple‎. سنبدأ بالتعريف العام (الفارغ)، والذي يمكن أن ينفع أيضًا كحالة أوّلية (base-case) لإنهاء التكرارية في التخصيص اللاحق: template < typename...T > struct DataStructure {}; يسمح لنا هذا بتعريف بنية فارغة ‎DataStructure<> data‎، ولكنّ هذا غير مفيد حاليًا. بعد ذلك يأتي تخصيص الحالة التكرارية (recursive case specialisation): template < typename T, typename...Rest > struct DataStructure<T, Rest... > { DataStructure(const T &first, const Rest &...rest): first(first), rest(rest...) {} T first; DataStructure < Rest... > rest; }; أصبح المثال مناسبًا الآن لإنشاء هياكل بيانات عشوائية، مثل <DataStructure<int, float, std::string‎، و("data(1, 2.1, "hello. لاحظ أنّ هذا التخصيص يتطّلب وجود مُعامل قالب واحد على الأقل -وهو ‎T‎ أعلاه- دون إعارة اهتمام لخصوصيات الحُزمة ‎Rest‎، ويسمح إدراك وجود ‎T‎ بتعريف حقل ‎first‎. وتُحزَم بقية البيانات ذاتيًا على شكل ‎DataStructure‎ <‎Rest ...‎> ‎rest‎، ويهيّئ المنشئ كلا العُضوَين مع استدعاء مُنشئ ذاتي على العضو ‎rest‎. لفهم هذا بشكل أفضل، إليك المثال التالي: لنفترض أنّ لديك تصريحًا <‎DataStructure<int,‎ ‎float‎. في البداية يتطابق التصريح مع التخصيص، وذلك يؤدّي إلى بنية تحتوي الحقلينint first و DataStructure<float> rest. ويتطابق التعريف ‎rest‎ مرّة أخرى مع التخصيص إذ يُنشئ حقلين float first و DataStructure<> rest خاصّين به. أخيرًا، يُطابَق rest مع الحالة الأساسية (base-case)، ممّا ينتج عنه بنية فارغة. يمكنك تصور هذا على النحو التالي: DataStructure<int, float> - > int first -> DataStructure<float> rest -> float first - > DataStructure < > rest -> (empty) أصبح لدينا الآن بنية بيانات، ولكنّها ليست مفيدة حاليًا، إذ لا يمكننا الوصول بسهولة إلى العناصر الفردية للبيانات. على سبيل المثال، سيتعيّن علينا استخدام ‎data.rest.rest.first‎ للوصول إلى العضو الأخير في ‎DataStructure<int, float, std::string> data‎، وذلك صعب، لذا سنضيف تابع ‎get‎ إليها -مطلوب فقط في التخصيص، لأنّ بنية الحالة الأساسية لا تحتوي على بيانات أصلًا-: template < typename T, typename...Rest > struct DataStructure<T, Rest... > { ... template < size_t idx> auto get() { return GetHelper<idx, DataStructure<T, Rest... >>::get(*this); } ... }; كما ترى، فإنّ التابع ‎get‎ نفسه مُقولَب -هذه المرّة على فهرس العضو المطلوب (لذلك يمكن استخدامه على النحو ‎data.get<1>()‎، على غرار الصفوف ‎std::tuple‎-، ويتم العمل الفعلي بفضل دالّة ساكنة في الصنف المساعد ‎GetHelper‎. السبب في أنّنا لم نتمكن من تعريف الدالّة المطلوبة مباشرة في التابع ‎get‎ الخاص بـ ‎DataStructure‎ هو أنّنا (كما سنرى قريبًا) نحتاج إلى تخصيص ‎idx‎، وهذا مستحيل لأنّه لا يمكن تخصيص تابع القالب دون تخصيص قالب الصنف الحاوي. لاحظ أيضًا أنّ استخدام ‎auto‎ من نمط C++14 بسّط عملنا كثيرًا، إذ بدونها سيكون تعبير نوع القيمة المُعادة معقدًا. سنحتاج إلى تصريح مُسبق فارغ وتَخصيصَين في الصنف المساعد. أولا التصريح: template < size_t idx, typename T> struct GetHelper; في الحالة الأساسية (‎idx==‎ ‎0‎)، سنعيد العضو ‎first‎ فقط: template < typename T, typename...Rest > struct GetHelper<0, DataStructure<T, Rest... >> { static T get(DataStructure<T, Rest... > &data) { return data.first; } }; في حالة التكرارية، سنُنقِص قيمة ‎idx‎ ونستدعي ‎GetHelper‎على العضو ‎rest‎: template < size_t idx, typename T, typename...Rest > struct GetHelper<idx, DataStructure<T, Rest... >> { static auto get(DataStructure<T, Rest... > &data) { return GetHelper < idx - 1, DataStructure < Rest... >>::get(data.rest); } }; لنفترض أنّ لدينا ‎DataStructure<int, float> data‎ ونحتاج إلى ‎data.get<1>()‎، هذا سيَستدعي ‎GetHelper<1, DataStructure<int, float>>::get(data)‎ (التخصيص الثاني)، والذي سيستدعي بدوره ‎‎GetHelper<0, DataStructure<float>>::get(data.rest)‎، والتي تُعيد في النهاية (بحسب التخصيص الأوّل، إذ أنّ ‎idx‎ تساوي الآن 0) ‎data.rest.first‎. هذا كل شيء! في ما يلي الشيفرة الكاملة، مع مثال توضيحي في الدالة ‎main‎: #include <iostream> template < size_t idx, typename T> struct GetHelper; template < typename...T > struct DataStructure {}; template < typename T, typename...Rest > struct DataStructure<T, Rest... > { DataStructure(const T &first, const Rest &...rest): first(first), rest(rest...) {} T first; DataStructure < Rest... > rest; template < size_t idx> auto get() { return GetHelper<idx, DataStructure<T, Rest... >>::get(*this); } }; template < typename T, typename...Rest > struct GetHelper<0, DataStructure<T, Rest... >> { static T get(DataStructure<T, Rest... > &data) { return data.first; } }; template < size_t idx, typename T, typename...Rest > struct GetHelper<idx, DataStructure<T, Rest... >> { static auto get(DataStructure<T, Rest... > &data) { return GetHelper < idx - 1, DataStructure < Rest... >>::get(data.rest); } }; int main() { DataStructure<int, float, std::string > data(1, 2.1, "Hello"); std::cout << data.get<0> () << std::endl; std::cout << data.get<1> () << std::endl; std::cout << data.get<2> () << std::endl; return 0; } إعادة توجيه الوسائط يمكن أن يقبل القالبُ المراجعَ اليمينية (rvalue references) واليسارية (lvalue references) على السواء باستخدام مرجع إعادة توجيه (forwarding reference): template < typename T> void f(T && t); في هذه الحالة، سيُستنبَط النوع الحقيقي لـ ‎t‎ من السياق: struct X {}; X x; f(x); // f<X&>(x) تستدعي f(X()); // f < X>(x) تستدعي في الحالة الأولى، يُستنتَج النوع ‎T‎ كمرجع إلى X ‏(‎X&‎)، أما نوع ‎t‎ فهو مرجع يساري إلى X، بينما في الحالة الثانية يُستنتَج نوع ‎T‎ كـ ‎X‎، ونوع ‎t‎ كمرجع يميني إلى X ‏(‎X&&‎). ملاحظة: تجدر الإشارة إلى أنّه في الحالة الأولى، يكون ‎decltype(t)‎ و ‎T‎ متكافئان، وذلك على خلاف الحالة الثانية. ولأجل إعادة توجيه (forward)‏ ‎t‎ إلى دالّة أخرى بالشكل الصحيح، سواء كان مرجعًا يمينيا أو يساريا، فينبغي استخدام std::forward: template < typename T> void f(T && t) { g(std::forward<T> (t)); } يمكن استخدام "إعادة توجيه المراجع" (Forwarding references) مع القوالب المتغايرة (variadic templates): template < typename...Args > void f(Args && ...args) { g(std::forward<Args> (args)...); } ملاحظة: لا يمكن استخدام إعادة توجيه المراجع إلا مع معاملات القوالب، مثلًا في الشيفرة التالية، ‎v‎ هي مرجع يميني، وليست مرجع إعادة توجيه: #include <vector> template < typename T> void f(std::vector<T> && v); التخصيص الجزئي للقوالب على النقيض من التخصيص الكامل للقوالب، يسمح التخصيص الجزئي للقوالب بتقديم قالب مع بعض الوسائط الخاصّة بقالب آخر ثابت. ولا يُتاح التخصيص الجزئي للقوالب إلّا لأصناف وبنيات القالب: // حالة شائعة template < typename T, typename U> struct S { T t_val; U u_val; }; // int حالة خاصة حيث معامل القالب الأول مثبّت عند النوع template < typename V> struct S<int, V> { double another_value; int foo(double arg) { // افعل شيئا ما } }; كما هو مُوضّح أعلاه، قد تُقدِّم التخصيصات الجزئية للقوالب مجموعات مختلفة تمامًا من البيانات والدوالّ العضوية. عند استنساخ قالب مخصّص جزئيًا، فسيتمّ اختيار التخصيص الأنسب، مثلًا لنعرّف قالبًا مع تخصيصَين جزئِيين: template < typename T, typename U, typename V> struct S { static void foo() { std::cout << "General case\n"; } }; template < typename U, typename V> struct S<int, U, V> { static void foo() { std::cout << "T = int\n"; } }; template < typename V> struct S<int, double, V> { static void foo() { std::cout << "T = int, U = double\n"; } }; الاستدعاءات التالية: S<std::string, int, double>::foo(); S<int, float, std::string>::foo(); S<int, double, std::string>::foo(); سوف تَطبع الخرج التالي: General case T = int T = int, U = double لا يمُكن أن تُخصّص قوالب الدوالّ جزئيًا: template < typename T, typename U> void foo(T t, U u) { std::cout << "General case: " << t << " " << u << std::endl; } // حسنا template < > void foo<int, int> (int a1, int a2) { std::cout << "Two ints: " << a1 << " " << a2 << std::endl; } void invoke_foo() { foo(1, 2.1); // ==> "General case: 1 2.1" foo(1, 2); // =>> "Two ints: 1 2" } // Compilation error: partial function specialization is not allowed. template < typename U> void foo<std::string, U> (std::string t, U u) { std::cout << "General case: " << t << " " << u << std::endl; } تخصيص القوالب Template Specialization يمكنك تنفيذ نُسخ مُحدّدة من صنف أو تابع قالب. على سبيل المثال، إذا كان لديك: template < typename T> T sqrt(T t) { /*Some generic implementation */ } فيمكنك إذن كتابة: template < > int sqrt<int> (int i) { /* تنفيذ مُحسَّن للأعداد الصحيحة */ } سيحصل المستخدم الذي يكتُب ‎sqrt(4.0)‎ على التنفيذ العام، بينما يحصل من يكتب ‎sqrt(4)‎ على التنفيذ المُخصّص. كنى القوالب Alias templates الإصدار ≥ C++‎ 11 انظر المثال البسيط التالي: template < typename T > using pointer = T *; هذا التعريف يجعل ‎pointer<T>‎ كُنيةً (alias) لـ ‎T*‎. مثلًا، يكافئ السطر التالي ;int* p = new int pointer<int> p = new int; لا يمكن تخصيص كُنى القوالب، لكن يمكن جعلها تشير إلى نوع مُتشعِّب في بنية: template < typename T> struct nonconst_pointer_helper { typedef T * type; }; template < typename T> struct nonconst_pointer_helper < T const > { typedef T * type; }; template < typename T > using nonconst_pointer = nonconst_pointer_helper<T>::type; الاستنساخ الصريح سيؤدي تعريف الاستنساخ الصريح إلى إنشاء صنف أو دالة أو متغيّر حقيقي من القالب، كما سيُصرّح عنه قبل استخدامه. يمكن الإشارة إلى تلك النُسخة من وحدات الترجمة الأخرى، ويمكن استخدام ذلك لتجنّب تعريف قالب في ترويسة الملف إذا كان سيُستنُسخ مع مجموعة محدودة من الوسائط. مثلا: // print_string.h template < class T> void print_string(const T *str); // print_string.cpp #include "print_string.h" template void print_string(const char *); template void print_string(const wchar_t *); ونظرًالأنّ ‎print_string<char>‎ و ‎print_string<wchar_t>‎ مُستنسختان بشكل صريح في ‎print_string.cpp‎، فسيتمكّن الرابط (linker) من العثور عليهما رغم أنّ القالب ‎print_string‎ لم يُعرَّف في الترويسة، وإذا لم تكن هذه التصريحات الفورية حاضرة، فمن المحتمل حدوث خطأ في الرابط. تبيّن هذه الصفحة الأجنبية لماذا لا يمكن تقديم القوالب إلا في الترويسة. الإصدار ≥ C++‎ 11 إذا سُبِق تعريف الاستنساخ الصريح بالكلمة ‎extern‎، فسيتحوّل إلى تصريح عن استنساخ صريح. التصريح عن استنساخ صريح لتخصيص معيّن يمنع الاستنساخ الضمني لذلك التخصيص داخل وحدة الترجمة الحالية، لكن لا مانع أن يشير مرجع لذلك التخصيص -الذي كان من الممكن أن يتسبب في استنساخ ضمني- إلى تعريف استنساخ صريح في نفس وحدة الترجمة أو في غيرها. foo.h‎ # ifndef FOO_H #define FOO_H template < class T > void foo(T x) { // تنفيذ معقّد }# endif foo.cpp #include "foo.h" // تعريف صريح لاستنساخ الحالات الشائعة. template void foo(int); template void foo(double); main.cpp #include "foo.h" // لها تعريف استنساخ صريح foo.cpp نعلم أنّ extern template void foo(double); int main() { هنا تُستنسخ <foo<int، وذلك لا فائدة منه بما أن foo.cpp تقدم استنساخًا صريحًا سلفًا، نتابع: foo(42); وهنا، لن تُستنسخ <foo<double إذ تستخدم نسخة من <foo<double في foo.cpp بدلًا من ذلك، انظر بقية المثال: foo(3.14); } مُعامل قالب غير نوعي يُسمح لنا بالتصريح عن قيم التعبيرات الثابتة التي تفي بأحد المعايير التالية، وذلك خلا النوع كمعامِل قالب: نوع عددي صحيح أو تعداد (enumeration). مؤشّر إلى كائن أو مؤشّر إلى دالة. مرجع يساري إلى كائن أو مرجع يميني إلى دالّة. مؤشّر إلى عضو. std::nullptr_t. يمكن تحديد معاملات القالب غير النوعية بشكل صريح -مثل كل معاملات القوالب- أو اشتقاقها أو تحديد قيمها الافتراضية ضمنيًا عبر استنباط وسيط القالب. هذا مثال على استخدام مُعامل قالب غير نوعي، إذ سنمرر مصفوفة بالمرجع تتطلب حجمًا معينًا، ونحن نسمح بكل الأحجام باستخدام قالب size: #include <iostream> template < typename T, std::size_t size> std::size_t size_of(T(&anArray)[size]) { return size; } int main() { char anArrayOfChar[15]; std::cout << "anArrayOfChar: " << size_of(anArrayOfChar) << "\n"; int anArrayOfData[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; std::cout << "anArrayOfData: " << size_of(anArrayOfData) << "\n"; } انظر المثال التالي على استخدام معاملات القالب النوعية وغير النوعية بشكل صريح، إذ يكون int معامِلًا نوعيًا، أما 5 فلا: #include <array> int main() { std::array<int, 5> foo; } تعدّ معاملات القوالب غير النوعية إحدى طرق تحقيق عوديّة القوالب والبرمجة العليا. التصريح عن وسائط القوالب غير النوعية عبر auto كان عليك قبل الإصدار C++‎ 17 أن تحدد نوع معامِل القالب غير النوعي عند كتابته، لذلك كان من الشائع كتابة شيء من هذا القبيل: template < class T, T N> struct integral_constant { using type = T; static constexpr T value = N; }; using five = integral_constant<int, 5> ; ولكن بالنسبة للتعبيرات المعقّدة، فإنّ استخدام الصيغة أعلاه يتطّلب كتابة ‎decltype(expr), expr‎ عند استنساخ القوالب، والحلّ هو تبسيط هذا المنظور واستخدام ‎auto‎: الإصدار ≥ C++‎ 17 template < auto N> struct integral_constant { using type = decltype(N); static constexpr type value = N; }; using five = integral_constant<5> ; حاذف مخصّص للمؤشرات الحصرية unique_ptr من أمثلة استخدام وسائط القوالب غير النوعية هي الجمع بين تحسين الأساس الفارغ وحاذف مُخصَّص للمؤشّرات الحصرية ‎unique_ptr‎. وتختلف حاذفات الواجهات البرمجية للغة C في نوع القيمة المعادة، ولكن هذا لا يشغلنا، فكل ما نريد هو شيء يعمل مع كل الدوالّ: template < auto DeleteFn> struct FunctionDeleter { template < class T> void operator()(T *ptr) const { DeleteFn(ptr); } }; template<T, auto DeleteFn> using unique_ptr_deleter = std::unique_ptr<T, FunctionDeleter < DeleteFn>> ; والآن يمكنك استخدام أيّ مؤشّر دالة يقبل وسيطًا من النوع ‎T‎ كمُعامل قالب غير نوعي، بصرف النظر عن نوع القيمة المُعادة، ودون القلق من أيّ حِمل إضافي في الحجم: unique_ptr_deleter<std::FILE, std::fclose > p; معامِلات قالب القالب قد نود أحيانًا أن نمرّر إلى القالب نوعَ قالبٍ دون تحديد قيمه، وهنا يأتي دور معاملات قالب القالب. هذا مثال بسيط يوضّح مفهوم مُعاملات قالب القالب: template < class T> struct Tag1 {}; template < class T> struct Tag2 {}; template<template<class> class Tag> struct IntTag { typedef Tag<int> type; }; int main() { IntTag<Tag1>::type t; } الإصدار ≥ C++‎ 11 #include <vector> #include <iostream> template < class T, template < class... > class C, class U > C<T> cast_all(const C<U> &c) { C<T> result(c.begin(), c.end()); return result; } int main() { std::vector<float> vf = { 1.2, 2.6, 3.7 }; auto vi = cast_all<int> (vf); for (auto && i: vi) { std::cout << i << std::endl; } } القيم الافتراضية لمُعاملات القالب كما في حالة وسائط الدوال، يمكن أن يكون لمعاملات القالب قيمٌ افتراضية، ويجب التصريح عن معاملات القالب ذات القيمة الافتراضية في نهاية قائمة معاملات القالب، والهدف هو إتاحة حذف معاملات القالب ذات القيمة الافتراضية أثناء استنساخ القالب. هذا مثال بسيط يوضّح كيفية استخدام القيمة الافتراضية لمُعامل القالب: template < class T, size_t N = 10 > struct my_array { T arr[N]; }; int main() { /* N = 5 إهمال قيمة المعامل الافتراضية */ my_array<int, 5> a; /* 5 - a.arr اطبع طول */ std::cout << sizeof(a.arr) / sizeof(int) << std::endl; /* N = 10 المعامل الأخير محذوف */ my_array<int> b; /* 10 - a.arr اطبع طول*/ std::cout << sizeof(b.arr) / sizeof(int) << std::endl; } أنماط القوالب عجيبة التكرار CRTP أنماط القوالب عجيبة التكرار (Curiously Recurring Template Pattern)، أو CRTP اختصارًا، هي أنماط برمجية يكون من الممكن فيها أن يرث صنف من قالب صنف، بحيث يكون ذلك الصنف نفسه أحد معامِلات القالب. وتُستخدم CRTP عادة لتوفير تعددية الأشكال الساكنة (static polymorphism) في C++‎. وتُعدّ CRTP بديلًا ممتازًا وساكنًا (static) للدوالّ الوهمية والوراثة التقليدية، إذ يمكن استخدامها لتحديد خصائص الأنواع في وقت التصريف، ويقوم مبدأ عملها على جعل قالب صنف أساسي (base class template) يأخذ صنفًا مشتقًا منه كأحد معاملات القالب خاصته، وهذا يسمح بإجراء تحويل ساكن ‎static_cast‎ للمؤشّر ‎this‎ الخاص بالصنف الأساسي لكي يشير إلى الصنف المشتق. بالطبع، هذا يعني أنّه سيتوجّب استخدام صنف CRTP دائمًا كصنف أساسي (base class) لصنف آخر، ويجب أن يمرِّر الصنف المشتق نفسه إلى الصنف الأساسي. الإصدار ≥ C++‎ 14 لنفترض أنّ لديك مجموعة من الحاويات التي تدعم الدالّتين ‎begin()‎ و ‎end()‎، وتتطّلب المكتبة القياسية للحاويات المزيد من الدوالّ. يمكننا هنا أن نصمم صنف CRTP أساسي يوفّر مثل تلك الدولب استنادًا إلى ‎begin()‎ و ‎end()‎ فقط: #include <iterator> template < typename Sub> class Container { private: تعيد ()self مرجعًا إلى النوع المشتق، نتابع المثال: Sub &self() { return * static_cast<Sub*> (this); } Sub const &self() const { return * static_cast< Sub const*> (this); } public: decltype(auto) front() { return* self().begin(); } decltype(auto) back() { return *std::prev(self().end()); } decltype(auto) size() const { return std::distance(self().begin(), self().end()); } decltype(auto) operator[](std::size_t i) { return *std::next(self().begin(), i); } }; يوفّر الصنف أعلاه الدوالّ ‎front()‎ و ‎back()‎ و ‎size()‎ و ‎operator[]‎ لأي صنف فرعي يوفر الدالتين ‎begin()‎ و ‎end()‎. في المثال التالي، يمكن للمصفوفات البسيطة المخصّصة ديناميكيًا أن تكون صنفًا فرعيًا: #include <memory> // مصفوفة مخصّصة ديناميكيا template < typename T> class DynArray: public Container<DynArray < T>> { public: using Base = Container<DynArray < T>> ; DynArray(std::size_t size): size_ { size }, data_ { std::make_unique < T[] > (size_) } {} T* begin() { return data_.get(); } const T* begin() const { return data_.get(); } T* end() { return data_.get() + size_; } const T* end() const { return data_.get() + size_; } private: std::size_t size_; std::unique_ptr < T[] > data_; }; يمكن الآن لمستخدمي الصنف ‎DynArray‎ استخدام الواجهات التي يوفّرها الصنف الأساسي CRTP بسهولة على النحو التالي: DynArray<int> arr(10); arr.front() = 2; arr[2] = 5; assert(arr.size() == 10); فائدة النمط: يتجنّب هذا النمط استدعاءات الدوالّ الوهمية في وقت التشغيل، والتي تسعى لاجتياز التسلسل الهرمي للوراثة، ويعتمد بدلًا من ذلك على التحويلات الساكنة (static casts): DynArray<int> arr(10); DynArray<int>::Base &base = arr; base.begin(); // لا استدعاءات وهمية ويسمح التحويل الساكن الوحيد داخل الدالّة ‎begin()‎ في الصنف الأساسي ‎Container<DynArray<int>>‎ للمٌصرّف بتحسين الشيفرة بشكل كبير، إذ لن يحدث أي تنقيب في الجدول الوهمي (virtual table) في وقت التشغيل. عيوب النمط: نظرًا لأنّ الصنف الأساسي مُقوْلَب ويختلف من مصفوفة ‎DynArray‎ إلى أخرى، فلا يمكن تخزين المؤشّرات التي تشير إلى أصنافها الأساسية في مصفوفة متجانسة كما نفعل عمومًا مع الوراثة العادية التي لا يعتمد فيها الصنف الأساسي على الصنف المشتق: class A {}; class B: public A {}; A *a = new B; استخدام أنماط CRTP لتجنّب تكرار الشيفرة يوضّح المثال التالي كيفية استخدام CRTP لتجنّب تكرار الشيفرة: struct IShape { virtual~IShape() = default; virtual void accept(IShapeVisitor &) const = 0; }; struct Circle: IShape { يجب على كل شكل أن يقدم هذا التابع بنفس الطريقة … ، نتابع المثال: void accept(IShapeVisitor & visitor) const override { visitor.visit(*this); } // ... }; struct Square: IShape { بالمثل هنا، يجب على كل شكل أن يقدم هذا التابع بنفس الطريقة، نتابع: void accept(IShapeVisitor & visitor) const override { visitor.visit(*this); } // ... }; يحتاج كل نوع فرعي من ‎IShape‎ إلى تنفيذ نفس الدالّة بنفس الطريقة، وهذا قد يأخذ الكثير من الوقت. الحل البديل هو تنفيذ نوع جديد في الهرمية الوراثية يتكفّل بفعل ذلك نيابة عنّا: template < class Derived> struct IShapeAcceptor: IShape { void accept(IShapeVisitor & visitor) const override { visitor.visit(*static_cast< Derived const*> (this)); } }; والآن، يكفي أن ترث الأشكال من المتقبِّل (acceptor): struct Circle: IShapeAcceptor < Circle> { Circle(const Point &center, double radius): center(center), radius(radius) {} Point center; double radius; }; struct Square: IShapeAcceptor < Square> { Square(const Point &topLeft, double sideLength): topLeft(topLeft), sideLength(sideLength) {} Point topLeft; double sideLength; }; لم تعد هناك حاجة الآن إلى تكرار الشيفرة. هذا الدرس جزء من سلسلة دروس عن C++‎. ترجمة -بتصرّف- للفصل Chapter 78: Expression templates والفصل Chapter 79: Curiously Recurring Template Pattern (CRTP)‎ من كتاب C++ Notes for Professionals
×
×
  • أضف...