المحتوى عن 'سلسلة ++c للمحترفين'.



مزيد من الخيارات

  • ابحث بالكلمات المفتاحية

    أضف وسومًا وافصل بينها بفواصل ","
  • ابحث باسم الكاتب

نوع المُحتوى


التصنيفات

  • الإدارة والقيادة
  • التخطيط وسير العمل
  • التمويل
  • فريق العمل
  • دراسة حالات
  • التعامل مع العملاء
  • التعهيد الخارجي
  • السلوك التنظيمي في المؤسسات
  • عالم الأعمال
  • التجارة الإلكترونية
  • نصائح وإرشادات
  • مقالات ريادة أعمال عامة

التصنيفات

  • PHP
    • Laravel
    • ووردبريس
  • جافاسكريبت
    • Node.js
    • jQuery
    • AngularJS
    • Cordova
    • React
    • Vue.js
  • HTML
    • HTML5
  • CSS
  • SQL
  • لغة C#‎
  • لغة C++‎
  • لغة C
  • بايثون
    • Flask
    • Django
  • لغة روبي
    • Sass
    • إطار عمل Bootstrap
    • إطار العمل Ruby on Rails
  • لغة Go
  • لغة جافا
  • لغة Kotlin
  • برمجة أندرويد
  • لغة Swift
  • لغة R
  • لغة TypeScript
  • ‎.NET
    • ASP.NET
  • الذكاء الاصطناعي
  • صناعة الألعاب
    • Unity3D
    • منصة Xamarin
  • سير العمل
    • Git
  • سهولة الوصول
  • مقالات برمجة عامة

التصنيفات

  • تجربة المستخدم
  • الرسوميات
    • إنكسكيب
    • أدوبي إليستريتور
    • كوريل درو
  • التصميم الجرافيكي
    • أدوبي فوتوشوب
    • أدوبي إن ديزاين
    • جيمب
  • التصميم ثلاثي الأبعاد
    • 3Ds Max
    • Blender
  • نصائح وإرشادات
  • مقالات تصميم عامة

التصنيفات

  • خوادم
    • الويب HTTP
    • قواعد البيانات
    • البريد الإلكتروني
    • DNS
    • Samba
  • الحوسبة السّحابية
    • Docker
  • إدارة الإعدادات والنّشر
    • Chef
    • Puppet
    • Ansible
  • لينكس
  • FreeBSD
  • حماية
    • الجدران النارية
    • VPN
    • SSH
  • مقالات DevOps عامة

التصنيفات

  • التسويق بالأداء
    • أدوات تحليل الزوار
  • تهيئة محركات البحث SEO
  • الشبكات الاجتماعية
  • التسويق بالبريد الالكتروني
  • التسويق الضمني
  • التسويق بالرسائل النصية القصيرة
  • استسراع النمو
  • المبيعات
  • تجارب ونصائح
  • مبادئ علم التسويق

التصنيفات

  • إدارة مالية
  • الإنتاجية
  • تجارب
  • مشاريع جانبية
  • التعامل مع العملاء
  • الحفاظ على الصحة
  • التسويق الذاتي
  • مقالات عمل حر عامة

التصنيفات

  • الإنتاجية وسير العمل
    • مايكروسوفت أوفيس
    • ليبر أوفيس
    • جوجل درايف
    • شيربوينت
    • Evernote
    • Trello
  • تطبيقات الويب
    • ووردبريس
    • ماجنتو
  • أندرويد
  • iOS
  • macOS
  • ويندوز

التصنيفات

  • شهادات سيسكو
    • CCNA
  • شهادات مايكروسوفت
  • شهادات Amazon Web Services
  • شهادات ريدهات
    • RHCSA
  • شهادات CompTIA
  • مقالات عامة

أسئلة وأجوبة

  • الأقسام
    • أسئلة ريادة الأعمال
    • أسئلة العمل الحر
    • أسئلة التسويق والمبيعات
    • أسئلة البرمجة
    • أسئلة التصميم
    • أسئلة DevOps
    • أسئلة البرامج والتطبيقات
    • أسئلة الشهادات المتخصصة

التصنيفات

  • ريادة الأعمال
  • العمل الحر
  • التسويق والمبيعات
  • البرمجة
  • التصميم
  • DevOps

تمّ العثور على 55 نتائج

  1. ‎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
  2. نماذج الذاكرة إن حاوَلَت عدّة خيوط الوصول إلى نفس الموضع من الذاكرة، فستدخل في تسابق على البيانات (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
  3. القيم الثنائية مصنفة النوع المُعرّفة من المستخدم (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
  4. سنستعرض في هذا الدرس بعض أنماط التصميم الشهيرة في 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
  5. إعادة التوجيه التامة (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
  6. ينصّ المعيار على ضرورة نسخ الكائنات أو نقلها في بعض المواضع من أجل تهيئتها، وإهمال النسخ (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
  7. تمكّن الكلمة المفتاحية ‎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
  8. دلالات النقل هي وسيلة لنقل كائن إلى آخر في 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
  9. ما المقصود بالسلوك غير المعرَّف (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
  10. كلمة 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
  11. ما هي تعابير لامدا؟ توفّر دوال لامدا طريقة موجزة لإنشاء كائنات دوالّ بسيطة، وتعبير لامدا هو قيمة يمينية خالصة (prvalue) تُنتجُ كائنَ تغليف (closure)، والذي يتصرف ككائن دالة. نشأ الاسم "تعبير لامدا" (lambda expression) عن علم حسابيات اللامدا (lambda calculus)، وهو شكل رياضي اختُرِع في الثلاثينيات من قِبل Alonzo Church للإجابة عن بعض الأسئلة المتعلقة بالمنطق والحوسبيّات (computability)، وقد شكّلت حسابيات لامدا أساس لغة LISP، وهي لغة برمجة وظيفية. مقارنة بحسابيات لامدا و LISP، فإنّّ تعبيرات لامدا في C++‎ تشترك معها في أنّها غير مسمّاة، وأنّها تلتقط المتغيّرات الموجودة في السياق المحيط، لكنّها تفتقر إلى القدرة على العمل على الدوال أو إعادتها. وغالبًا ما تُستخدَم دوال لامدا كوسائط للدوالّ التي تأخذ كائنًا قابلاً للاستدعاء، وذلك غالبًا أبسط من إنشاء دالة مُسمّاة، والتي لن تُستخدَم إلا عند تمريرها كوسيط، وتعبيرات لامدا مفضّلة في مثل هذه الحالات، لأنّها تسمح بتعريف الدوّال ضمنيًا. يتألف تعبير لامدا عادة من ثلاثة أجزاء: قائمة الالتقاط ‎[]‎، وقائمة المعاملات الاختيارية ‎()‎، والمتن ‎{}‎، وكلها يمكن أن تكون فارغة: []() {} // تعبير لامدا فارغ لا يعيد أي شيء. قائمة الالتقاط (Capture list) تمثّل ‎[]‎ قائمة الالتقاط. لا يمكن الوصول إلى متغيّرات النطاق المحيط عبر تعبير لامدا افتراضيًا، ويكون المتغير متاحًا بمجرد التقاطه داخل تعبير لامدا سواء كنسخة أو مرجع، وتصبح المتغيرات الملتقطة جزءًا من لامدا، ولا يلزم تمريرها عند استدعاء لامدا، على عكس وسائط الدوال. int a = 0; // تعريف متغير عددي صحيح auto f =[]() { return a * 9; }; // Error: 'a' cannot be accessed auto f =[a]() { return a * 9; }; // بالقيمة 'a' تم التقاط auto f =[& a]() { return a++; }; // بالمرجع 'a' تم التقاط // a ملاحظة: المبرمج ليس مسؤولا عن ضمان ألا تُدمَّر // قبل استدعاء لامدا auto b = f(); // من دالة الالتقاط ولم تُمرّر هنا a استدعاء دالة لامدا، تم أخذ قائمة المعاملات تمثّل ‎()‎ قائمة المعاملات، وهي تماثل قائمة المعاملات في الدوال العادية، ويمكن حذف الأقواس إذا لم يأخذ تعبير لامدا أيّ وسائط، إلا إذا كنت بحاجة إلى جعل تعبير لامدا قابلًا للتغيير ‎mutable‎. يُعدُّ تعبيرا لامدا التاليان متكافئين: auto call_foo =[x]() { x.foo(); }; auto call_foo2 =[x] { x.foo(); }; الإصدار ≥ C++‎ 14 تستطيع قائمةُ المعاملات أن تستخدم النوعَ النائب (placeholder type)‏ ‎auto‎ بدلاً من الأنواع الفعلية، وهذا يجعل الوسيط يتصرّف كمُعامل قالب لقالب دالة. تعبيرات لامدا التالية متكافئة: auto sort_cpp11 =[](std::vector<T>::const_reference lhs, std::vector<T>::const_reference rhs) { return lhs < rhs; }; auto sort_cpp14 =[](const auto &lhs, const auto &rhs) { return lhs < rhs; }; متن الدالة القوسان المعقُوصان ‎{}‎ يحتويان على متن تعبير لامدا، ويُماثل متن الدوال العادية. استدعاء دالة لامدا يعيد تعبير لامدا مغلِّفًا أو غلافًا (closure)، ويمكن استدعاؤه باستخدام الصيغة ‎operator()‎ (كما هو الحال مع الدوال العادية): int multiplier = 5; auto timesFive =[multiplier](int a) { return a * multiplier; }; std::out << timesFive(2); // 10 multiplier = 15; std::out << timesFive(2); // 2 *5 == 10 نوع القيمة المعادة افتراضيًا، يُستنبَط نوع القيمة المُعادة لتعبير لامدا تلقائيًا. []() { return true; }; في هذه الحالة، نوع القيمة المُعادة هو ‎bool‎، يمكنك أيضًا تعريف نوع القيمة المُعادة يدويًا باستخدام الصياغة التالية: []()->bool { return true; }; دوال لامدا القابلة للتغيير (Mutable Lambda) الكائنات المُلتقطة بالقيمة في دوال لامدا تكون افتراضيًا غير قابلة للتغيير (immutable)، وذلك لأنّ المُعامل ‎operator()‎ الخاصّ بالكائن المغلّف المُنشأ ثابت (‎const‎) افتراضيًا. في المثال التالي، سيفشل التصريف لأن ++C ستحاول محاكاة حالة تعبير لامدا. auto func = [c = 0](){++c; std::cout << c;}; يمكن السماح بالتعديل باستخدام الكلمة المفتاحية ‎mutable‎، والتي تجعل المُعامل ‎operator()‎ غير ثابت ‎const‎: auto func =[c = 0]() mutable { ++c; std::cout << c; }; إذا اسُتخدِم مع نوع القيمة المُعادة، فينبغي وضع ‎mutable‎ قبله. auto func =[c = 0]() mutable->int { ++c; std::cout << c; return c; }; انظر المثال التالي لتوضيح فائدة لامدا: الإصدار < C++‎ 11 // كائن دالي عام لأجل المقارنة struct islessthan { islessthan(int threshold): _threshold(threshold) {} bool operator()(int value) const { return value < _threshold; } private: int _threshold; }; // التصريح عن متجهة const int arr[] = { 1, 2, 3, 4, 5 }; std::vector<int> vec(arr, arr + 5); // إيجاد عدد أصغر من مُدخَل مُعطى - على افتراض أنه سيكون مُدخلا إلى دالة int threshold = 10; std::vector<int>::iterator it = std::find_if(vec.begin(), vec.end(), islessthan(threshold)); الإصدار ≥ C++‎ 11 // التصريح عن متجهة std::vector<int> vec { 1, 2, 3, 4, 5 }; // إيجاد عدد أصغر من مُدخَل مُعطى - على افتراض أنه سيكون مُدخلا إلى دالة int threshold = 10; auto it = std::find_if(vec.begin(), vec.end(), [threshold](int value) { return value < threshold; }); يمكننا تلخيص ما سبق بالجدول التالي: 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; } المعامل التفاصيل default-capture الالتقاط الافتراضي يحدّد كيفيّة التقاط جميع المتغيّرات غير المدرجة، قد يكون عبر ‎=‎ (التقاطًا بالقيمة) أو عبر ‎&‎ (التقاطا بالمرجع). وفي حال حذفها، سيتعذّر الوصول إلى المتغيّرات غير المدرجة ضمن متن لامدا. كما يجب أن يسبق default- capture المعاملُ capture-list. capture-list لائحة الالتقاط يحدّد كيف يمكن الوصول إلى المتغيّرات المحلية داخل متن لامدا، تُلتقط المتغيّرات التي ليست لها سابقة (prefix) بالقيمة (by value) بينما تُلتقط المتغيّرات المسبوقة بـ ‎&‎ بالمرجع. يمكن استخدام ‎this‎ في توابع الأصناف لجعل جميع أعضاء الكائن الذي يشير إليه في المتناول عبر المرجع. ولا يمكن الوصول إلى المتغيّرات غير المدرجة في القائمة إلا إذا كانت القائمة مسبوقة بـ default-capture. argument-list قائمة الوسائط يحدّد وسائط دالّة لامدا. mutable (اختياري) عادة ما تكون المتغيّرات الملتقطة بالقيمة ثابتة (‎const‎). وستكون غير ثابتة عند تحديد ‎mutable‎. وسيُحتفظ بالتغييرات على تلك المتغيّرات بين الاستدعاءات. throw-specification رفع الاعتراضات (اختياري) يحدّد سلوك رفع الاعتراضات في دالّة لامدا، مثل: ‎noexcept‎ أو ‎throw‎‏ (std::exception). attributes السمات (اختياري) سمات دالّة لامدا. على سبيل المثال، إذا كان متن لامدا يلقي دائمًا اعتراضًا، فيمكن استخدام ‎[[noreturn]]‎. نوع القيمة المعادة (اختياري) يحدّد نوع القيمة المُعادة من دالّة لامدا. وهي ضرورية في الحالات التي لا يكون بمقدور المٌصرّف فيها تخمين نوع القيمة المعادة. lambda-body متن لامدا الكتلة التي تحتوي تنفيذ لامدا. تحديد نوع القيمة المُعادة بالنسبة لدوال لامدا التي تحتوي عبارة return واحدة فقط، أو عدّة عبارات return من نفس النوع، فيمكن للمصرّف أن يستنتج نوع القيمة المُعادة. انظر المثال التالي الذي يعيد قيمة بوليانية لأن "10 < value" هي قيمة بوليانية. auto l =[](int value) { return value > 10; } بالنسبة لدوال لامدا التي لها عدّة عبارات return من أنواع مختلفة، فلا يمكن للمصرّف أن يستنتج نوع القيمة المُعادة: // error: return types must match if lambda has unspecified return type auto l =[](int value) { if (value < 10) { return 1; } else { return 1.5; } }; في هذه الحالة، يجب عليك تحديد نوع القيمة المُعادة بشكل صريح: // 'double' حُدِّد نوع القيمة المعادة بـ auto l =[](int value)->double { if (value < 10) { return 1; } else { return 1.5; } }; تطابق هذه القواعدُ قواعدَ استنتاج نوع ‎auto‎، ولا تعيد دوال لامدا التي لم يُصرّح بنوع قيمتها المُعادة صراحة مراجعًا أبدًا، لذا إذا كنت تودّ إعادة نوع مرجعي (reference type)، فيجب تحديده بشكل صريح: تعيد copy في المثال التالي قيمة من نوع &X لذا تنسخ المدخلات: auto copy = [](X& x) { return x; }; لا يحدث نسخ لأن ref في المثال التالي ستعيد قيمة من نوع &X: auto ref = [](X& x) -> X& { return x; }; الالتقاط بالقيمة Capture by value إن حدّدتَ اسم المتغيّر في قائمة الالتقاط فإنّّ تعبير لامدا سيلتقطُه بالقيمة، هذا يعني أنّ نوع المغلِّف المُنشأ لتعبير لامدا سيخزّن نسخة من المتغيّر، وهذا يتطّلب أيضًا أن يكون نوع المتغيّر قابلاً للنسخ: int a = 0; [a]() { return a; // بالقيمة 'a' تم التقاط }; الإصدار < C++‎ 14 auto p = std::unique_ptr<T>(...); [p]() { // Compile error; `unique_ptr` is not copy-constructible return p->createWidget(); }; من C++‎ 14 وصاعدًا، من الممكن تهيئة المتغيّرات على الفور، وهذا يسمح بالتقاط "أنواع النقل فقط" في لامدا. الإصدار ≥ C++‎ 14 auto p = std::make_unique<T> (...); [p = std::move(p)]() { return p->createWidget(); }; رغم أنّ دوال لامدا يمكن أن تلتقط المتغيّرات بالقيمة عندما تُمرّر باسمائها، إلّا أنّ تلك المتغيّرات لا يمكن تعديلها في متن لامدا افتراضيًا، وذلك لأنّ نوع المغلّف يضع متن لامدا في تصريح من الشكل ‎operator() const‎. وتُطبَّق ‎const‎ على محاولات الوصول إلى المتغيّرات العضوية (member variables) في نوع المغلّف، والمتغيّرات المُلتقطة التي هي أعضاء في المغلّف (على خلاف المتوقّع): int a = 0; [a]() { a = 2; // غير جائز decltype(a) a1 = 1; a1 = 2; // جائز }; لإزالة ‎const‎، يجب عليك وضع الكلمة المفتاحية ‎mutable‎ في تعبير لامدا: int a = 0; [a]() mutable { a = 2; // 'a' يمكن الآن تعديل return a; }; نظرًا لأنّ ‎a‎ التُقِطت بالقيمة، فإنّّ أيّ تعديلات تُجرى عن طريق استدعاء تعبير لامدا لن تؤثر على ‎a‎، ولقد نُسخِت قيمة ‎a‎ في تعبير لامدا عند إنشائه، لذلك فإنّ نسخة ‎a‎ في تعبير لامدا مختلفة عن المتغيّر ‎a‎ الخارجي. int a = 5 ; auto plus5Val = [a] (void) { return a + 5 ; } ; auto plus5Ref = [&a] (void) {return a + 5 ; } ; a = 7 ; std::cout << a << ", value " << plus5Val() << ", reference " << plus5Ref() ; // "7, value 10, reference 12" تعبيرات لامدا الذاتية الاستداعاء Recursive lambdas لنفترض أنّنا نرغب في كتابة خوارزمية اقليدس للقاسم المشترك الأكبر ‎gcd()‎ على شكل تعبير لامدا. أولا، سنكتبها على شكل دالة: int gcd(int a, int b) { return b == 0 ? a : gcd(b, a%b); } تعبيرات لامدا لا يمكن أن تكون ذاتية الاستدعاء، لأنها غير قادرة على استدعاء نفسها، فلامدا ليس لها اسم، واستخدام ‎this‎ داخل متن لامدا إنّما يشير إلى ‎this‎ المُلتقَط (بافتراض أنّ تعبير لامدا مُنشأ في متن دالة تابعة، وإلا فسيُطلق خطأ)، إذن كيف نحلّ هذه المشكلة؟ استخدام std::function يمكننا جعل تعبير لامدا يحصل على مرجع إلى كائن ‎std::function‎ لم يُنشأ بعد: std::function<int(int, int)> gcd = [&](int a, int b){ return b == 0 ? a : gcd(b, a%b); }; هذا سيعمل بدون مشاكل، ولكن ينبغي ألّا تستخدم هذه الطريقة إلّا لُمامًا، إذ أنّها بطيئة (لأنّنا نستخدم محو الأنواع - type erasure - بدلاً من استدعاء الدالّة المباشرة)، كما أنّها هشّة (نسخ ‎gcd‎ أو إعادة ‎gcd‎ سيكسر الشيفرة لأنّ تعبير لامدا يشير إلى الكائن الأصلي)، ولن تعمل مع دوال لامدا العامة. استخدام مؤشرين ذكيين auto gcd_self = std::make_shared<std::unique_ptr< std::function<int(int, int)> >>(); *gcd_self = std::make_unique<std::function<int(int, int)>>( [gcd_self](int a, int b){ return b == 0 ? a : (**gcd_self)(b, a%b); }; }; هذا يضيف الكثير من الأعمال غير المباشرة (ما يؤدّي إلى زيادة الحِمل)، ولكن يمكن نسخه/إعادته، كما أنّ جميع النسخ تشترك في الحالة، وهذا يتيح لك إعادة تعبير لامدا، وهذا الحل أفضل من الحل المذكور أعلاه. استخدام Y-combinator باستخدام بِنية صغيرة مُساعِدة، يمكننا حل جميع هذه المشاكل: template < class F> struct y_combinator { F f; // لامدا ستُخزَّن هنا // مسبق operator() عامل template < class...Args > decltype(auto) operator()(Args && ...args) const { ستمرر نفسها هنا إلى f ثم تمرر الوسائط، ويجب أن تأخذ لامدا الوسيط الأول كـ auto&& recurse أو نحوه، نتابع المثال: return f(*this, std::forward<Args> (args)...); } }; // :دالة مساعِدة تستنبط نوع لامدا template < class F> y_combinator<std::decay_t < F>> make_y_combinator(F && f) { return { std::forward<F> (f) }; } // ( `make_` هناك حلول أفضل من C++17 تذكر أنّ في ) يمكننا تنفيذ ‎gcd‎ على النحو التالي: auto gcd = make_y_combinator( [](auto&& gcd, int a, int b){ return b == 0 ? a : gcd(b, a%b); } ); ‎y_combinator‎ هو أحد مفاهيم حسابيّات لامدا، ويتيح لك العمل بالذاتية دون الحاجة إلى تسمية الدالة، وهذا هو بالضبط ما ينقصنا في دوال لامدا. تستطيع إنشاء تعبير لامدا يأخذ "recurse" كوسيط أوّل، وعندما تريد استخدام الذاتية، يمكنك تمرير الوسائط إلى recurse. ويعيد ‎y_combinator‎ بعد ذلك كائن دالة يستدعي تلك الدالّة مع وسائطها، ولكن بكائن "recurse" المناسب (أي ‎y_combinator‎ نفسه) كوسيط أوّل، ويوجِّه بقية الوسائط التي مرّرتها إلى ‎y_combinator‎ إلى تعبير لامدا على النحو التالي: auto foo = make_y_combinator( [&](auto&& recurse, some arguments) { { // اكتب شيفرة تعالج بعض المعاملات }); استدع recurse مع بعض الوسائط الأخرى عند الحاجة لتنفيذ الذاتية (Recursion)، وكذلك نحصل على الذاتية في لامدا دون أيّ قيود أو حِمل كبير. الالتقاط الافتراضي Default capture افتراضيًا، لا يمكن الوصول إلى المتغيّرات المحلية التي لم تُحدَّد بشكل صريح في قائمة الالتقاط من داخل متن تعبير لامدا، بيْد أنّه من الممكن ضمنيًا التقاط المتغيّرات المُسمّاة من قبل متن لامدا: int a = 1; int b = 2; // التقاط افتراضي بالقيمة [=]() { return a + b; }; // بالقيمة a و b تم التقاط // الالتقاط الافتراضي بالمرجع [&]() { return a + b; }; // بالمرجع a و b ما يزال بالإمكان القيام بالتقاط صريحٍ بجانب الالتقاط الافتراضي الضِّمني، سيعيد الالتقاط الصريح تعريف الالتقاط الافتراضي: int a = 0; int b = 1; [=, &b]() { a = 2; b = 2; }; في الشيفرة السابقة: لا تجوز a = 2 لأن a ملتقطَة بالقيمة، وتعبير لامدا لا يقبل التغيير، أي ليس mutable، بينما تجوز b = 2 لأن b ملتقَطة بالمرجع. دوال لامدا في الأصناف والتقاط this تُعدُّ تعبيرات لامدا الذي تم تقييمها في تابع ما صديقة للصّنف الذي ينتمي إليه ذلك التابع ضمنيًا: class Foo { private: int i; public: Foo(int val): i(val) {} // تعريف دالة تابعة void Test() { auto lamb = [](Foo &foo, int val) { // (private) تعديل متغير عضو خاص foo.i = val; }; يُسمح لـ lamb أن تصل إلى عضو خاص لأنها صديقة لـ Foo، نتابع المثال: lamb(*this, 30); } }; مثل هذه التعبيرات ليست صديقة لهذا الصنف فحسب، بل لها نفس إمكانيات الوصول التي يتمتّع بها الصنف الذي صُرِّح عنها فيه. كذلك يمكن لدَوال لامدا التقاط المؤشّر ‎this‎، والذي يمثّل نُسخة الكائن الذي استُدعِيت الدالّة الخارجية عليه، عن طريق إضافة ‎this‎ إلى قائمة الالتقاط، انظر المثال التالي: class Foo { private: int i; public: Foo(int val): i(val) {} void Test() { // بالقيمة this التقاط المؤشر auto lamb =[this](int val) { i = val; }; lamb(30); } }; عند التقاط ‎this‎، يمكن لتعبير لامدا استخدام أسماء أعضاء الصنف الذي يحتويه كما لو كان في صنفه الحاوي، لذلك تُطبّق ‎this->‎ ضمنيًا على أولئك الأعضاء. يجب الانتباه إلى أنّ ‎this‎ تُلتقط بالقيمة، ولكن ليس بقيمة النوع، بل تُلتقَط بقيمة ‎this‎، وهو مؤشّر، لهذا فإنّ تعبير لامدا لا يملك ‎this‎، وإذا تجاوز عُمرُ تعبير لامدا عمرَ الكائن الذي أنشأه، فقد يصبح غير صالح. هذا يعني أيضًا أنّ لامدا يمكنها تعديل ‎this‎ حتى لو لم تكن قابلة للتغيير (‎mutable‎)، ذلك أنّ المؤشّر هو الذي يكون ثابتًا ‎const‎ وليس الكائن الذي يؤشّر إليه، إلا إن كان التابع الخارجي دالة ثابتة ‎const‎. أيضًا، تذكّر أنّ كتل الالتقاط الافتراضية ‎[=]‎ و ‎[&]‎ ستلتقط ‎this‎ ضمنيًا، وكلاهما سيلتقطَانه بقيمة المؤشّر، وفي الحقيقة، فمن الخطأ تحديد ‎this‎ في قائمة الالتقاط عند إعطاء قيمة افتراضية. الإصدار ≥ C++‎ 17 وتستطيع دوال لامدا التقاط نسخة من كائن ‎this‎ المُنشأ في وقت إنشاء تعبير لامدا، وذلك عن طريق إضافة ‎*this‎ إلى قائمة الالتقاط: class Foo { private: int i; public: Foo(int val): i(val) {} void Test() { // التقاط نسخة من الكائن المُعطى من قبل المؤشر auto lamb =[*this](int val) mutable { i = val; }; lamb(30); // this->i لا تغيّر } }; الالتقاط بالمرجع Capture by reference إذا وضعت السابقة ‎&‎ قبل اسم متغيّر محلي، فسيُلتقط المتغيّر بالمرجع، هذا يعني نظريًا أنّ نوع مُغلِّفِ لامدا سيكون له متغيّر مرجعي يُهيَّأُ كمرجع، وسيشير ذلك المرجع إلى المتغيّر المقابل الموجود خارج نطاق تعبير لامدا، وأيّ استخدام للمتغيّر في متن لامدا سوف يشير إلى المتغيّر الأصلي: // 'a' التصريح عن المتغير int a = 0; // بالمرجع 'a' التصريح عن لامدا تلتقط auto set =[& a]() { a = 1; }; set(); assert(a == 1); الكلمة المفتاحية ‎mutable‎ ليست مطلوبة لأنّ ‎a‎ نفسها ليست ثابتة. والالتقاط بالمرجع يعني أنّ لامدا يجب ألا تخرج عن نطاق المتغيّرات التي تلتقطها، لذلك يمكنك استدعاء الدوال التي تأخذ دالّة، ولكن لا تستدع دالّة تخزّن لامدا خارج نطاق مراجعك، وكذلك لا تُعِد تعبير لامدا. تعابير لامدا العامة النوع Generic lambdas الإصدار ≥ C++‎ 14 يمكن أن تأخذ دوال لامدا وسائط من أيّ نوع، هذا يسمح لهذه الدوال أن تكون أكثر عمومية: auto twice =[](auto x) { return x + x; }; int i = twice(2); // i == 4 std::string s = twice("hello"); // s == "hellohello" طُبِّق هذا في C++‎ عبر جعل ‎operator()‎ تزيد تحميل دالّة قالب (template function)، انظر النوع التالي الذي له سلوك مكافئ لسلوك مُغلِّف لامدا أعلاه: struct _unique_lambda_type { template < typename T> auto operator()(T x) const { return x + x; } }; ليس بالضرورة أن تكون كل المعاملات عامة في تعبير لامدا غير المحدد أو العام (Generic lambda): [](auto x, int y) { return x + y; } هنا، تُستنبَط ‎x‎ بناءً على وسيط الدالّة الأوّل، بينما سيكون ‎y‎ دائمًا عددًا صحيحًا (‎int‎)، وقد تأخذ دوال لامدا العامَّة الوسائط بالمرجع أيضًا، وذلك باستخدام القواعد المعتادة لـ ‎auto‎ و ‎&‎، أما إن أُخذ وسيط عام كـ ‎auto&&‎، فسيكون مرجعًا أماميًا (forwarding reference) يشير إلى الوسيط المُمرّر، وليس مرجعًا يمينِيّا (rvalue reference): auto lamb1 = [](int &&x) {return x + 5;}; auto lamb2 = [](auto &&x) {return x + 5;}; int x = 10; lamb1(x); lamb2(x); في الشيفرة السابقة، لا تجوز (lamb1(x لوجوب استخدام std::move(x)‎ لأجل معامِلات &&int، بينما تجوز (lamb1(x لأن نوع x يُستنتج على أنه &int. كذلك يمكن أن تكون دوالّ لامدا متغايرة (variadic)، وأن تعيد توجيه وسائطها: auto lam = [](auto&&... args){return f(std::forward<decltype(args)>(args)...);}; أو: auto lam = [](auto&&... args){return f(decltype(args)(args)...);}; والتي لن تعمل "بالشكل الصحيح" إلّا مع المتغيّرات من نوع ‎auto&&‎. أيضًا، أحد الأسباب القوية لاستخدام دوال لامدا العامّة هو البنية اللغوية للزيارة (visiting syntax)، انظر: boost::variant<int, double> value; apply_visitor(value, [&](auto&& e){ std::cout << e; }); هنا، قمنا هنا بالزيارة بأسلوب متعدد الأشكال، لكن في السياقات الأخرى فلا تهم أسماء النوع المُمرّرة: mutex_wrapped<std::ostream&> os = std::cout; os.write([&](auto&& os){ os << "hello world\n"; }); لا فائدة من تكرار النوع ‎std::ostream&‎ هنا؛ كأنك تذكر نوع المتغيّر في كل مرّة تستخدمه، ولقد أنشأنا هنا زائرًا غير متعدد الأشكال، كما استخدمنا ‎auto‎ هنا لنفس سبب استخدام ‎auto‎ في حلقة ‎for(:)‎. استخدام دوال لامدا لفك حزم المعاملات المضمنة الإصدار ≥ C++‎ 14 كان فكّ حزم المعامِلات (Parameter pack unpacking) يتطّلب كتابة دالّة مساعدة في كل مرّة تريد القيام بذلك. مثلًا: template<std::size_t...Is > void print_indexes(std::index_sequence < Is... >) { using discard = int[]; (void) discard { 0, ((void)( std::cout << Is << '\n' // هي ثابتة في وقت التصريف Is هنا ), 0)... }; } template<std::size_t I> void print_indexes_upto() { return print_indexes(std::make_index_sequence < I> {}); } يريد ‎print_indexes_upto‎ إنشاء وفكّ حزمة معاملات من الفهارس، ولفعل ذلك، يجب استدعاء دالّة مساعدة. يجب أن تنشئ دالة مساعدة مخصصة في كل مرة تريد فك حزمة معامِلات أنشأتها، ويمكن تجنب ذلك هنا باستخدام دوال لامدا، إذ يمكنك فكّ الحِزَم باستخدام دالة لامدا على النحو التالي: template<std::size_t I> using index_t = std::integral_constant<std::size_t, I> ; template<std::size_t I> constexpr index_t<I> index {}; template < class = void, std::size_t...Is > auto index_over(std::index_sequence < Is... >) { return[](auto && f) { using discard = int[]; (void) discard { 0, (void( f(index < Is>) ), 0)... }; }; } template<std::size_t N> auto index_over(index_t<N> = {}) { return index_over(std::make_index_sequence < N> {}); } الإصدار ≥ C++‎ 17 يمكن تبسيط ‎index_over()‎ باستخدام التعبيرات المطوية على النحو التالي: template<class=void, std::size_t...Is> auto index_over( std::index_sequence<Is...> ) { return [](auto&& f){ ((void)(f(index<Is>)), ...); }; } بعد ذلك، يمكنك استخدام هذا لتجنّب الحاجة إلى فكّ حزم المعاملات يدويًا عبر تحميل زائد ثانٍ في الشيفرات الأخرى، فذلك سيتيح لك فكّ حزم المعاملات بشكل مضمّن (inline): template < class Tup, class F> void for_each_tuple_element(Tup&& tup, F&& f) { using T = std::remove_reference_t<Tup> ; using std::tuple_size; auto from_zero_to_N = index_over<tuple_size < T> {} > (); from_zero_to_N( [&](auto i) { using std::get; f(get<i> (std::forward<Tup> (tup))); } ); } نوع ‎auto i‎ المُمرّر إلى دالة لامدا عبر ‎index_over‎ هو ‎std::integral_constant<std::size_t, ???>‎ الذي يحتوي على تحويل ‎constexpr‎ إلى std::size_t لا يعتمد على حالة this، وعليه نستطيع استخدامه كثابت في وقت التصريف، مثلا عندما نمرّره إلى ‎std::get<i>‎ أعلاه. سنعيد الآن كتابة المثال أعلاه: template<std::size_t I> void print_indexes_upto() { index_over(index < I>)([](auto i) { std::cout << i << '\n'; // هي ثابتة في وقت التصريف i هنا }); } صار المثال أقصر بكثير. انظر إن شئت هذا المثال الحيّ على ذلك. الالتقاطات المعممة النوع Generalized capture الإصدار ≥ C++‎ 14 تستطيع دوال لامدا التقاط التعبيرات وليس المتغيّرات فقط، هذا يسمح لدوال لامدا بتخزين أنواع النقل فقط (move-only types): auto p = std::make_unique<T> (...); auto lamb =[p = std::move(p)]() { p->SomeFunc(); }; هذا ينقل المتغيّر ‎p‎ الخارجي إلى متغيّر لامدا المُلتقط ، ويسمى أيضًا ‎p‎، وتمتلك ‎lamb‎ الآن الذاكرة المخصّصة لـ ‎make_unique‎. وبما أن التغليف (closure) يحتوي على نوع غير قابل للنسخ، فذلك يعني أنّ ‎lamb‎ ستكون غير قابلة للنسخ، لكن ستكون قابلة للنقل رغم هذا: auto lamb_copy = lamb; // غير جائز auto lamb_move = std::move(lamb); // جائز الآن أصبحت ‎lamb_move‎ تملك الذاكرة، لاحظ أنّ ‎std::function<>‎ تتطّلب أن تكون القيم المخزّنة قابلة للنسخ، يمكنك كتابة دالة خاصّة بك تتطّلب النقل فقط أو وضع لامدا في غلاف مؤشّر مشترك ‎shared_ptr‎: auto shared_lambda = [](auto&& f){ return [spf = std::make_shared<std::decay_t<decltype(f)>>(decltype(f)(f))] (auto&&...args)->decltype(auto) { return (*spf)(decltype(args)(args)...); }; }; auto lamb_shared = shared_lambda(std::move(lamb_move)); هنا أخذنا دالة لامدا للنقل فقط ووضعنا حالتها في مؤشّر مشترك، ثم ستُعاد دالة لامدا قابلة للنسخ، ثم التخزين في ‎std::function‎ أو نحو ذلك. يستخدم الالتقاط المُعمّم استنباط النوع ‎auto‎ لاستنباط نوع المتغيّر، وسيصرّح عن هذه الالتقاطات على أنّها قيم افتراضيًا، لكن يمكن أن تكون مراجع أيضًا: int a = 0; auto lamb = [&v = a](int add) // مختلفان `a` و `v` تذكّر أنّ اسمَي { v += add; // `a` تعدي }; lamb(20); // ستصبح 20 `a` يستطيع الالتقاط المعمَّم أن يلتقط تعبيرًا عشوائيًا لكن لا يلزمه التقاط متغيرات خارجية: auto lamb =[p = std::make_unique<T> (...)]() { p->SomeFunc(); } هذا مفيد في إعطاء دوال لامدا قيمًا عشوائية يمكنها الاحتفاظ بها وربّما تعديلها دون الحاجة إلى التصريح عنها خارجيًا، بالطبع، هذا لا يكون مفيدًا إلّا إذا كنت تنوي الوصول إلى تلك المتغيّرات بعد أن تكمل لامدا عملها. التحويل إلى مؤشر دالة إذا كانت قائمة الالتقاط الخاصّة بلَامدا فارغة، فستُحوَّل لامدا ضمنيا إلى مؤشّر دالة يأخذ نفس الوسائط ويعيد نوع القيمة المُعادة نفسه: auto sorter =[](int lhs, int rhs)->bool { return lhs < rhs; }; using func_ptr = bool(*)(int, int); func_ptr sorter_func = sorter; // تحويل ضمني يمكن أيضًا فرض هذا التحويل باستخدام عامل "+" الأحادي: func_ptr sorter_func2 = +sorter; // فرض التحويل الضمني سيكون سلوك استدعاء مؤشّر الدالّة هذا مكافئًا لاستدعاء ‎operator()‎ على لامدا، فلا يعتمد مؤشّر الدالّة على وجود مغلّف لامدا، لذلك قد يستمرّ حتى بعد انتهاء مغلّف لامدا. هذه الميزة مفيدة عند استخدام دوال لامدا مع الواجهات البرمجية التي تتعامل مع مؤشّرات الدوال، بدلاً من كائنات دوال C++‎. الإصدار ≥ C++‎ 14 يمكن أيضًا تحويل دالة لامدا عامة لها قائمة التقاط فارغة إلى مؤشّر دالّة، وإذا لزم الأمر سيُستخدَم استنباط وسيط القالب (template argument deduction) لاختيار التخصيص الصحيح. auto sorter =[](auto lhs, auto rhs) { return lhs < rhs; }; using func_ptr = bool(*)(int, int); func_ptr sorter_func = sorter; // int استنتاج // لكن السطر التالي غامض // func_ptr sorter_func2 = +sorter; ترقية دوال لامدا إلى C++‎ 03 باستخدام الكائنات الدالية (functor) دوال لامدا في C++‎ هي اختصارات مفيدة توفّر صياغة مختصرة لكتابة الدوال، ويمكن الحصول على وظيفة مشابهة في C++‎ 03 (وإن كانت مُطولًة) عن طريق تحويل دالّة لامدا إلى كائن دالّي: // هذه بعض الأنواع struct T1 { int dummy; }; struct T2 { int dummy; }; struct R { int dummy; }; هذه الشيفرة تستخدم دالة لامدا،هذا يعني أنها تستلزم C++11، نتابع المثال: R use_lambda(T1 val, T2 ref) { هنا، استعمل auto لأن النوع المُعاد من دالة لامدا مجهول، نتابع: auto lambda =[val, &ref](int arg1, int arg2)->R { /* متن لامدا */ return R(); }; return lambda(12, 27); } // C++03 صنف الكائن الدالي - صالح في class Functor { // قائمة الالتقاط T1 val; T2 &ref; public: // المنشئ inline Functor(T1 val, T2 &ref): val(val), ref(ref) {} // متن الكائن الدالي R operator()(int arg1, int arg2) const { /* متن لامدا */ return R(); } }; هذا يكافئ use_lambda لكنه يستخدم كائنًا دالّيًا، وهو صالح في C++03: R use_functor(T1 val, T2 ref) { Functor functor(val, ref); return functor(12, 27); } int main() { T1 t1; T2 t2; use_functor(t1, t2); use_lambda(t1, t2); return 0; } إذا كانت دالّة لامدا قابلة للتغيير (‎mutable‎)، فاجعل مُعامل الاستدعاء الخاص بالكائن الدالّي غير ثابت، أي: R operator()(int arg1, int arg2) /*non-const*/ { /* متن لامدا */ return R(); } هذا الدرس جزء من سلسلة دروس عن C++‎. ترجمة -بتصرّف- للفصل Chapter 73: Lambdas من كتاب C++ Notes for Professionals
  12. آلية 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
  13. قاعدة الصفر الإصدار ≥ 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
  14. كيفية إنشاء خيط 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‎، ممّا يعني أن التقديم سيكون هو المسؤول عن تحديد سياسة إنشاء الخيوط الجديدة. هذا الدرس جزء من سلسلة دروس عن C++‎. ترجمة -بتصرّف- للفصول: Chapter 80: Threading Chapter 85: Mutexes Chapter 86: Recursive Mutex Chapter 87: Semaphore Chapter 88: Futures and Promises من كتاب C++ Notes for Professionals
  15. صار بالإمكان قولبة الأصناف والدوالّ والمتغيّرات في لغة ++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
  16. معالج C الأولي هو محلّل/مبدّل نصوص يُشغَّل قبل التصريف الفعلي للشيفرة، ويُستخدم لتوسيع وتيسير استخدام لغة C (وكذلك C++‎ لاحقًا)، ويمكن استخدامه من أجل: تضمين ملفّات أخرى باستخدام ‎#include‎. تعريف شيفرة جامعة (macro)، لاستبدال النص باستخدام ‎#define‎ التصريف الشرطي باستخدام ‎#if‎ ‎#ifdef‎. توجيه شيفرة معيّنة لمنصّة أو مٌصرّف معيّن (امتداد للتصريف الشرطي) قيود التضمين قد تُضمّن ترويسة من قبل ترويسة أخرى. لذلك، فالملفّات المصدرية التي تتضمّن عدّة ترويسات قد تتضمّن بعض الترويسات أكثر من مرّة بشكل غير مباشر. وإن كانت إحدى الترويسات المُضمّنة أكثر من مرّة تحتوي على تعريفات، فإنّّ المٌصرّف -بعد المعالجة الأوّلية- سيرصد انتهاكًا لقاعدة التعريف الواحد (الفقرة 3.2 من المعيار C++‎ لعام 2003) ومن ثم يحدث خطأ في التصريف. يمكن منع التضمين المتعدّد باستخدام دروع التضمين "include guards"، وتُعرَف أيضًا بدروع الترويسة، أو دروع الشيفرة الجامعة (macro guards). وتُنفَّذ هذه الدروع باستخدام مُوجِّهات المُعالج الأوّلي ‎#define‎ و ‎#ifndef‎ و ‎#endif‎. انظر: // Foo.h #ifndef FOO_H_INCLUDED #define FOO_H_INCLUDED class Foo // تعريف صنف { }; #endif الميزة الرئيسية لاستخدام دروع التضمين أنّها تعمل مع جميع المُصرّفات المتوافقة مع المعايير والمُعالجات الأوّلية. من ناحية أخرى، قد تخلق دروع التضمين بعض المشاكل للمطورين، لوجوب التأكد أنّ وحدات الشيفرة الجامعة فريدة في جميع الترويسات المُستخدمة في المشروع، خاصة إذا استَخدَمت ترويستين (أو أكثر) ‎FOO_H_INCLUDED‎ كدروع تضمين، فإنّّ أولى تلك الترويستين المُضمّنتين في وحدة تصريف ستمنع تضمين الترويسات الأخرى. هذه التحديات تظهر خاصّة إذا كان المشروع يستخدم مكتبات الطرف الثالث، والتي تحتوي على ترويسات تشترك في استخدام دروع تضمين. من الضروري أيضًا التأكد من أنّ وحدات الشيفرة الجامعة المستخدمة في دروع التضمين لا تتعارض مع وحدات الشيفرة الجامعة الأخرى المُعرَّفة في الترويسة. معظم تنفيذات C++‎ تدعم المُوجِّه ‎#pragma once‎ الذي يحرص على عدم تضمين الملفّ إلّا مرّة واحدة فقط في كل عمليّة تصريف، وهو مُوجِّه قياسي، لكنه ليس جزءًا من أيّ معيار من معايير ISO C++‎. مثلا: // Foo.h #pragma once class Foo {}; في الشيفرة السابقة: بينما تُجنِّب ‎#pragma once‎ بعض المشاكل المرتبطة بدروع التضمين، فإنّ ‎#pragma‎ -حسب المعيار- هي بطبيعتها خُطَّاف خاص بالمٌصرّف (compiler-specific hook)، وستُتجاهل بصمت من قبل المٌصرّفات التي لا تدعمها. حمل المشاريع (أي porting) التي تستخدم ‎#pragma once‎ إلى المٌصرّفات التي لا تدعمها ليس بالأمر الهيّن. هناك عدد من إرشادات التشفير ومعايير C++‎ التي توصي بعدم استخدام أيّ مُعالج أوّلي إلا من أجل تضمين ملفات الترويسة ‎#include‎، أو لأجل وضع دروع التضمين في الترويسات. المنطق الشرطي والتعامل مع تعدد المنصات باختصار، يتمحور منطق المعالجة الأوّلية الشرطية حول التحكم في جعل منطق الشيفرة متاحًا للتصريف أو غير متاح باستخدام تعاريف الشيفرات الجامعة. هذه ثلاث حالات استخدام أساسية: عدّة إصدارات من تطبيق (مثلًا: إصدار للتنقيح، وآخر للإطلاق، وآخر للاختبار، وآخر للتحسين) التصريفات متعدّدة المنصات (cross-platform compiles) - نفس الشيفرة المصدرية، مع تصريفات لعدة منصات استخدام نفس الشيفرة لأجل عدة إصدارات من التطبيق (مثلًا: إصدار Basic و Premium و Pro من البرنامج) - مع ميزات مختلفة قليلاً. مثال أ: مقاربة متعددة المنصات لإزالة الملفّات: #ifdef _WIN32 #include <windows.h > // وبقية ملفات نظام ويندوز #endif #include <cstdio> bool remove_file(const std::string &path) { #ifdef _WIN32 return DeleteFile(path.c_str()); #elif defined(_POSIX_VERSION) || defined(__unix__) return (0 == remove(path.c_str())); #elif defined(__APPLE__) //TODO: دالة مُخصّصة مع نافذة حوار للترخيص NSAPI التحقق مما إذا كان لـ return (0 == remove(path.c_str())); #else #error "This platform is not supported" #endif } مثال ب: إتاحة إمكانية التسجيل الإضافي لأجل التنقيح: void s_PrintAppStateOnUserPrompt() { std::cout << "--------BEGIN-DUMP---------------\n" << AppState::Instance()->Settings().ToString() << "\n" #if ( 1 == TESTING_MODE ) // الخصوصية: لا نريد المعلومات الشخصية للمستخدم إلا عند الاختبار << ListToString(AppState::UndoStack()->GetActionNames()) << AppState::Instance()->CrntDocument().Name() << AppState::Instance()->CrntDocument().SignatureSHA() << "\n" #endif << "--------END-DUMP---------------\n" } مثال ج: توفير ميزة مدفوعة (premium feature) في منتج منفصل (ملاحظة: هذا المثال توضيحي. من الأفضل إتاحة الميزة دون الحاجة إلى إعادة تثبيت التطبيق) void MainWindow::OnProcessButtonClick() { #ifndef _PREMIUM CreatePurchaseDialog("Buy App Premium", "This feature is available for our App Premium users. Click the Buy button to purchase the Premium version at our website"); return; #endif //… الميزات الفعلية هنا } بعض الطرق الشائعة: تعريف رمز في وقت الاستدعاء (invocation): يمكن استدعاء المُعالج الأوّلي برموز مسبقة (مع تهيئة اختيارية). على سبيل المثال (‎gcc -E‎ يعمل فقط على المُعالج الأوّلي): gcc - E - DOPTIMISE_FOR_OS_X - DTESTING_MODE = 1 Sample.cpp يُعالَج Sample.cpp كما لو أنّ ‎#define OPTIMISE_FOR_OS_X‎ و ‎#define TESTING_MODE 1‎ مُضافان إلى أعلى Sample.cpp. التأكد أن شيفرة جامعة ما معرَّفة: إذا لم تكن الشيفرة الجامعة مُعرّفة وقورنت قيمتها أو تم التحقق منها، فسيفترض المعالج الأوّلي دائمًا أنّ القيمة ستساوي ‎0‎. وهناك عدّة طرق للتعامل مع هذا، إحداها هي افتراض أنّ الإعدادات الافتراضية تُمثَّل بالعدد 0، ويجب إجراء أيّ تغييرات لازمة بشكل صريح (على سبيل المثال ENABLEEXTRADEBUGGING = 0 افتراضيًا، مع الإسناد -DENABLEEXTRADEBUGGING = 1 لإعادة التعريف). هناك طريقة أخرى، وهي جعل جميع التعاريف والقيم الافتراضية صريحة، ويمكن تحقيق ذلك بدمج المُوجِّهين ‎#ifndef‎ و ‎#error‎: #ifndef (ENABLE_EXTRA_DEBUGGING) // إن لم تكن مضافة سلفا DefaultDefines.h برجاء إضافة # error "ENABLE_EXTRA_DEBUGGING is not defined" #else # if ( 1 == ENABLE_EXTRA_DEBUGGING ) //code # endif #endif وحدات الشيفرة الجامعة التوليدية X-macros هي تقنية اصطلاحية لتكرار توليد بنيات برمجية في وقت التصريف، وتتكون الشيفرات الجامعة التوليدية من جزأين: القائمة، وتنفيذ القائمة. هذا مثال توضيحي: #define LIST\ X(dog)\ X(cat)\ X(racoon) // class Animal { // public: // void say(); // }; #define X(name) Animal name; LIST #undef X int main() {#define X(name) name.say(); LIST #undef X return 0; } هذا المثال سيُوسَّع من قبل المعالج الأوّلي إلى ما يلي: Animal dog; Animal cat; Animal racoon; int main() { dog.say(); cat.say(); racoon.say(); return 0; } عندما تصبح القوائم كبيرة (أكثر من 100 عنصر)، فإنّ هذه التقنية تساعد على تجنّب الإكثار من النسخ واللصق. وإذا أردت تجنّب التعريف غير الضروري لـ ‎X‎ قبل استخدام ‎LIST‎، فيمكنك تمرير اسم شيفرة جامعة كوسيط أيضًا: #define LIST(MACRO)\ MACRO(dog)\ MACRO(cat)\ MACRO(racoon) الآن، يمكنك تعريف الشيفرة الجامعة الذي يجب استخدامها عند توسيع القائمة، كما يلي: #define FORWARD_DECLARE_ANIMAL(name) Animal name; LIST(FORWARD_DECLARE_ANIMAL) إذا وجب على كل استدعاء لـ ‎MACRO‎ أن يأخذ معاملات إضافية ثابتة بالنسبة للقائمة، فيمكن استخدام وحدات شيفرات جامعة متغيّرة (variadic macros): // Visual studio #define EXPAND(x) x #define LIST(MACRO, ...)\ EXPAND(MACRO(dog, __VA_ARGS__))\ EXPAND(MACRO(cat, __VA_ARGS__))\ EXPAND(MACRO(racoon, __VA_ARGS__)) يُمرَّر الوسيط الأولى عبر ‎LIST‎، بينما تُوفّر الوسائط الأخرى عبر المستخدم عند استدعاء ‎LIST‎. مثلا، الشيفرة التالية: #define FORWARD_DECLARE(name, type, prefix) type prefix## name; LIST(FORWARD_DECLARE, Animal, anim_) LIST(FORWARD_DECLARE, Object, obj_) سوف تتوسع على النحو التالي: Animal anim_dog; Animal anim_cat; Animal anim_racoon; Object obj_dog; Object obj_cat; Object obj_racoon; الشيفرات الجامعة Macros تُصنّف وحدات الشيفرة الجامعة إلى مجموعتين رئيسيتين: وحدات الشيفرة الجامعة المشابهة للكائنات (object-like macros)، ووحدات الشيفرة الجامعة المشابهة للدوالّ (function-like macros). تُعامَل وحدات الشيفرة الجامعة كأداة لتعويض مقطع (token) معيّن من الشيفرة أثناء عملية التصريف، هذا يعني أنّه يمكن تجريد المقاطع الكبيرة (أو المكرّرة) من الشيفرة عبر شيفرة جامعة. انظر: // هذه شيفرة جامعة تشبه الكائنات #define PI 3.14159265358979 هذه شيفرة جامعة تشبه الدوال، لاحظ أنه يُمكننا استخدام الشيفرَات الجامعة المُعرّفة مسبقًا في تعريفات شيفرة جامعة أخرى. ولا يعرف المصرِّف أيّ نوع يتعامل معه، لذلك يفضل استخدام الدوال المضمّنة: #define AREA(r) (PI *(r) *(r)) // يمكن استخدامها على النحو التالي double pi_macro = PI; double area_macro = AREA(4.6); تستخدم مكتبة Qt هذه التقنية لإنشاء نظام وصفي للكائنات (meta-object system) عن طريق مطالبة المستخدم بتعريف الشيفرة الجامعة Q_OBJECT في رأس الصنف الذي يوسّع QObject. وتُكتب أسماء وحدات الشيفرة الجامعة في العادة بأحرف كبيرة لتسهيل تمييزها عن الشيفرات العادية. هذا ليس شرطًا لكنّ يستحسنه كثير من المبرمجين. عند مصادفة شيفرة جامعة مشابهة للكائنات فإنها تُوسّع كعملية لصق-نسخ بسيطة، مع استبدال اسم الشيفرَة الجامعة بتعريفها. أمّا الشيفرات الجامعة المشابهة للدوال، فيُوسّع كلّ من اسمها ومعاملاتها. double pi_squared = PI * PI; double pi_squared = 3.14159265358979 * 3.14159265358979; double area = AREA(5); double area = (3.14159265358979 *(5) *(5)) وغالبًا ما تُوضع معاملات الشيفرة الجامعة الشبيهة بالدالّة (function-like macro parameters) بسبب هذا ضمن الأقواس، كما في ‎AREA()‎ أعلاه، وذلك لمنع أيّ أخطاء قد تحدث أثناء توسيع الشيفرة الجامعة، خاصة الأخطاء التي قد تنتج عن مُعاملات الشيفرة الجامعة التي تتألّف من عدة قيم. #define BAD_AREA(r) PI *r *r double bad_area = BAD_AREA(5 + 1.6); // ما يراه المصرف: double bad_area = 3.14159265358979 * 5 + 1.6 * 5 + 1.6; double good_area = AREA(5 + 1.6); // ما يراه المصرف: double good_area = (3.14159265358979 *(5 + 1.6) *(5 + 1.6)); لاحظ أيضًا أنّه يجب توخي الحذر بخصوص المعاملات المُمرّرة إلى وحدات الشيفرة الجامعة بسبب هذا التوسيع البسيط، وذلك لمنع الآثار الجانبية غير المتوقعة. وإذا عُدِّل المُعامل أثناء التقييم فسيُعُدَّل في كل مرّة يُستخدَم في الشيفرة الجامعة الموسَّعة، ونحن لا نريد ذلك عادة. يبقى هذا صحيحًا حتى لو كانت الشيفرة الجامعة تُحيط المعاملات بالأقواس لمنع كسر الشيفرة بسبب التوسيع. int oops = 5; double incremental_damage = AREA(oops++); // ما يراه المصرف: double incremental_damage = (3.14159265358979*(oops++)*(oops++)); بالإضافة إلى ذلك، لا توفّر وحدات الشيفرة الجامعة أيّ حماية من أخطاء الأنواع، ممّا يصعّب فهم الأخطاء الناتجة عن عدم تطابق الأنواع. ولمّا كان المبرمجون ينهون السطر بفاصلة منقوطة في العادة، فغالبًا ما تُصمَّم وحدات الشيفرة الجامعة المراد استخدامها كسطور مستقلة "لابتلاع" الفاصلة المنقوطة، هذا يمنع أيّ أخطاء غير مقصودة ناتجة عن وجود فاصلة منقوطة إضافية. #define IF_BREAKER(Func) Func(); if (some_condition) // Oops. IF_BREAKER(some_func); else std::cout << "I am accidentally an orphan." << std::endl; في هذا المثال، كسرت الفاصلة المنقوطة المزدوجة غير المقصودة كتلة ‎if...else‎، ومنعت المٌصرّف من مطابقة ‎else‎ بـ ‎if‎. ولكي نمنع هذا، تُحذَف الفاصلة المنقوطة من تعريف الشيفرة الجامعة، ممّا يتسبب في "ابتلاع" الفاصلة المنقوطة مباشرةً بعد أيّ استخدام لها. #define IF_FIXER(Func) Func() if (some_condition) IF_FIXER(some_func); else std::cout << "Hooray! I work again!" << std::endl; يسمح ترك الفاصلة المنقوطة الزائدة أيضًا باستخدام الشيفرة الجامعة دون إنهاء التعليمة الحالية، وذلك يمكن أن يكون مفيدًا. #define DO_SOMETHING(Func, Param) Func(Param, 2) // ... some_function(DO_SOMETHING(some_func, 3), DO_SOMETHING(some_func, 42)); عادة، ينتهي تعريف الشيفرة الجامعة في نهاية السطر، فإذا احتاجت الشيفرة الجامعة إلى أن تمتدّ على عدّة أسطر، فيمكن استخدام شرطة عكسية \ في نهاية السطر للإشارة إلى ذلك. ويجب أن تكون هذه المشروطة العكسية هي الحرف الأخير من السطر، فذلك يُخطر المُعالج الأوّلي بوجوب ضمّ السطر التالي إلى السطر الحالي، ومعاملتهُما كسطرٍ واحد. يمكن استخدام هذا عدّة مرات على التوالي. #define TEXT "I \ am \ many \ lines." // ... std::cout << TEXT << std::endl; // I am many lines. هذا مفيد بشكل خاص في وحدات الشيفرة الجامعة المعقّدة الشبيهة بالدوالّ، والتي قد تحتاج إلى الامتداد على عدّة أسطر. #define CREATE_OUTPUT_AND_DELETE(Str) \ std::string* tmp = new std::string(Str); \ std::cout << *tmp << std::endl; \ delete tmp; // ... CREATE_OUTPUT_AND_DELETE("There's no real need for this to use 'new'.") أمّا بخصوص وحدات الشيفرة الجامعة المعقدة الشبيهة بالدوالّ، فقد يكون من المفيد منحها نطاقًا خاصًّا بها لمنع التداخلات في الأسماء أو التسبب في تدمير الكائنات في نهاية الشيفرة الجامعة، وذلك على غرار الدوالّ الفعلية. المقاربة الشائعة لهذا هي استخدام الحلقة do while 0، حيث تُوضع الشيفرة الجامعة في كتلة التكرار. لا تُتبع هذه الكتلة عادة بفاصلة منقوطة، ممّا يسمح لها بابتلاع واحدة. #define DO_STUFF(Type, Param, ReturnVar) do {\ Type temp(some_setup_values);\ ReturnVar = temp.process(Param);\ } while (0) int x; DO_STUFF(MyClass, 41153.7, x); int x; do { MyClass temp(some_setup_values); x = temp.process(41153.7); } while (0); هناك أيضًا وحدات شيفرة جامعة متغايرة (variadic)، والتي تأخذ عددًا متغيّرا من الوسائط، ثم توسّعها جميعًا بدلاً من المُعامل الخاص ‎__VA_ARGS__‎. #define VARIADIC(Param, ...) Param(__VA_ARGS__) VARIADIC(printf, "%d", 8); // ما يراه المصرِّف: printf("%d", 8); لاحظ أنّه يمكن وضع ‎__VA_ARGS__‎ أثناء التوسيع في أيّ مكان في التعريف، وسيُوسّع بشكل صحيح. #define VARIADIC2(POne, PTwo, PThree, ...) POne(PThree, __VA_ARGS__, PTwo) VARIADIC2(some_func, 3, 8, 6, 9); some_func(8, 6, 9, 3); في حالة المعامِل المتغير الذي ليس له وسائط، فإنّ المٌصرّفات تتعامل مع الفاصلة الزائدة بشكل مختلف، حيث تبتلع بعض المُصرّفات -مثل Visual Studio- الفاصلة بصمت بدون أيّ صيغة خاصّة، فيما ستطلّب منك مُصرّفات أخرى -مثل GCC- وضع ‎##‎ مباشرة قبل ‎__VA_ARGS__‎. ويكون من الحكمة بسبب هذا أن تضع تعريف وحدات الشيفرة الجامعة التي تأخذ عددًا متغيّرا من الوسائط في عبارة شرطية إذا كنت حريصًا على قابلية الشيفرة للعمل في أكثر من مكان. في المثال التالي: COMPILER هو شيفرة جامعة تحدد المصرِّف المستخدم. #if COMPILER == "VS" #define VARIADIC3(Name, Param, ...) Name(Param, __VA_ARGS__) #elif COMPILER == "GCC" #define VARIADIC3(Name, Param, ...) Name(Param, ##__VA_ARGS__) #endif /* COMPILER */ الشيفرات الجامعة المُعرّفة مسبقًا (Predefined macros) الشيفرة الجامعة المُعرّفة مسبقًا هي تلك التي يعرّفها المٌصرّف -على النقيض من تلك التي يعرّفها المستخدمون في الملف المصدري-، ويجب ألّا يُعاد تعريف تلك الشيفرات الجامعة أو إلغاء تعريفها من قِبل المستخدم. الشيفرات الجامعة التالية مُعرّفة مسبقًا وفقًا لمعيار C++‎: ‎__LINE__‎ - تحتوي على رقم السطر الذي تُسُتخدم فيه هذه الشيفرة الجامعة، ويمكن تغييرها عبر المُوجِّه ‎‎#line. ‎__FILE__‎ - تحتوي على اسم الملفّ الذي تُستخدم فيه هذه الشيفرة الجامعة في، ويمكن تغييرها عبر المُوجِّه ‎‎#line. ‎__DATE__‎ - تحتوي تاريخ تصريف الملف (بتنسيق "Mmm dd yyyy")، ويتم تنسيق Mmm كما لو كانت مُعادة من قبل ‎std::asctime()‎. ‎__TIME__‎ - تحتوي زمن تصريف الملف وفق التنسيق "HH: MM: SS". ‎__cplusplus‎ - تُعرَّف من قبل مصرّف C++‎ أثناء تصريف ملفات C++‎، وقيمتها هي الإصدار القياسي الذي يتوافق مع المٌصرّف، أي ‎199711L‎ لـ C++‎ 98 و C++‎ 03، و ‎201103L‎ لـ C++‎ 11، و ‎201402L‎ لمعيار C++‎ 14. الإصدار ≥ C++‎ 11 ‎__STDC_HOSTED__‎ - تُعطى القيمة ‎1‎ إذا كان التطبيق مُستضافًا (hosted)، أو ‎0‎ إذا كان قائما بذاته (freestanding). الإصدار ≥ C++‎ 17 ‎__STDCPP_DEFAULT_NEW_ALIGNMENT__‎ - تحتوي على size_t حرفية، والتي تمثّل المحاذاة المستخدمة لإجراء استدعاء للعامل ‎operator new‎ غير المدرِك للمحاذاة. بالإضافة إلى ذلك، يُسمح للشيفرة الجامعة التالية أن تُعرَّف مسبقًا من قبل التنفيذات، لكنّها غير إلزامية: ‎__STDC__‎ - يعتمد معناها على التنفيذ، ولا تٌعرَّف عادة إلّا عند تصريف ملف في لغة C للدلالة على الامتثال الكامل مع معيار C القياسي، أو قد لا تُنفّد إذا قرّر المٌصرّف عدم دعم هذه الشيفرة الجامعة. الإصدار ≥ C++‎ 11 ‎__STDC_VERSION__‎ - يعتمد معناها على التنفيذ، وعادة ما تساوي قيمتها إصدارَ لغة C، على نحو مشابه لـ ‎__cplusplus‎ في إصدار C++‎، أو قد لا تُعرَّف، إذا قرر المٌصرّف عدم دعم هذه الشيفرة الجامعة. ‎__STDC_MB_MIGHT_NEQ_WC__‎ - تُعطى القيمة ‎1‎، إذا كان ممكنًا لقيم الترميز الضيق لمجموعة المحارف الأساسية، ألا تساوي قيم نظرائها الواسعة، مثلًا في حال كان ‎(uintmax_t)'x' != (uintmax_t)L'x'‎. ‎__STDC_ISO_10646__‎ - تُعرّف في حال كان wchar_t مُرمّزًا بترميز اليونيكود (Unicode)، وتُوسّع إلى عدد صحيح على شكل ‎yyyymmL‎ إشارةً إلى دعم آخر مُراجَعة لليونيكود. ‎__STDCPP_STRICT_POINTER_SAFETY__‎ - تُعطى القيمة ‎1‎ إذا كان للتنفيذ نظام أمان صارم للمؤشّرات (strict pointer safety) ‎__STDCPP_THREADS__‎ - تُعطى القيمة ‎1‎ إذا أمكن احتواء البرنامج على أكثر من خيط (thread) واحد للتنفيذ، ينطبق هذا على التنفيذ المستقل (freestanding implementation) أما التنفيذات المستضافة فيمكن أن يكون لها أكثر من خيط واحد. ربما تجب الإشارة إلى ‎__func__‎، وهي ليست شيفرة جامعة ولكنّها متغيِّر محلّي لدالّة مُعرّفة مُسبقا (predefined function-local variable). يحتوي هذا المتغير على اسم الدالّة التي تُستخدَم فيها على شكل مصفوفة حروف ساكنة وفق تنسيق مُعرّف من قِبل التنفيذ. ويمكن أن يكون للمٌصرّفات مجموعة خاصّة بها من وحدات الشيفرة الجامعة المسبقة، وذلك إضافة إلى وحدات الشيفرة الجامعة القياسية المُعرّفة مسبقًا، ارجع إن شئت إلى توثيق المٌصرّف للاطلاع عليها. على سبيل المثال: gcc Microsoft Visual C++‎‎ clang Intel C++ Compiler توجد بعض تلك الشيفرات الجامعة للاستعلام عن دعم بعض الميزات فقط: #ifdef __cplusplus // C++ في حال صُرِّفت عبر مصرّف extern "C" { //C يجب أن تُزخرَف شيفرة // هنا C ترويسة مكتبة } #endif البعض الآخر مفيدة جدًا للتنقيح: الإصدار ≥ C++‎ 11 bool success = doSomething( /*some arguments*/ ); if( !success ){ std::cerr << "ERROR: doSomething() failed on line " << __LINE__ - 2 << " in function " << __func__ << "()" << " in file " << __FILE__ << std::endl; } والبعض الآخر للتحكم في الإصدار: int main(int argc, char *argv[]) { if( argc == 2 && std::string( argv[1] ) == "-v" ){ std::cout << "Hello World program\n" << "v 1.1\n" // عليك تحديث هذا يدويا << "compiled: " << __DATE__ << ' ' << __TIME__ // هذا يُحدّث تلقائيا << std::endl; } else { std::cout << "Hello World!\n"; } } عمليات المعالجة الأولية يُستخدم المُعامل ‎#‎ أو مُعامل التنصيص (stringizing operator) لتحويل مُعامل شيفرة جامعة إلى سلسلة نصية، ولا يمكن استخدامه إلّا مع شيفرة جامعة ذات وسائط. انظر المثال التالي حيث يحول المعالج الأولي المعامِلَ x إلى السلسلة النصية المجردة x: #define PRINT(x) printf(#x "\n") PRINT(This line will be converted to string by preprocessor); printf("This line will be converted to string by preprocessor" "\n"); يضمّ المُصرّف سلسلتين نصّيتين إلى بعضهما، وسيكون الوسيط ‎printf()‎ النهائي عبارة عن سلسلة نصية مجردة تنتهي بمحرف السطر الجديد. وسيتجاهل المُعالج الأوّلي المسافات البيضاء الموجودة قبل أو بعد وسيط الشيفرة الجامعة، لذلك ستطبع الشيفرة التالية نفس النتيجة. PRINT(This line will be converted to string by preprocessor); إذا تطلب مُعامل السلسلة النصية المجردة تسلسل تهريب (escape sequence)، كما هو الحال قبل علامة الاقتباس المزدوجة، فسيُدرَج تلقائيًا عبر المعالج الأوّلي. PRINT(This "line" will be converted to "string" by preprocessor); printf("This \"line\" will be converted to \"string\" by preprocessor""\n"); يُستخدم العامل ‎##‎ أو عامل لصق القِطع (Token pasting operator) لضمّ سلسلتين نصّيتين أو مقطعَين خاصين بشيفرة جامعة. انظر المثال التالي حيث يدمج المعالجُ الأولي المتغيرَ مع x: #define PRINT(x) printf("variable" #x " = %d", variable##x) int variableY = 15; PRINT(Y); printf("variable""Y"" = %d", variableY); وسنحصل على الناتج التالي: variableY = 15 ‎‎ #pragma once تدعم معظم تطبيقات C++‎ المُوجِّه ‎#pragma once‎ الذي يتحقّق من أنّ الملفّ لن يُضمَّن إلّا مرّة واحدة في كل عملية تصريف، وهو ليس جزءًا من أيّ معيار من معايير ISO C++‎. على سبيل المثال: // Foo.h #pragma once class Foo {}; بينما تتجنب ‎#pragma once‎ بعض المشاكل المرتبطة بدروع التضمين (include guards) التي ذكرناها آنفًا، فإنّ ‎#pragma‎ -حسب تعريفها في المعايير- هي خطّاف خاصّ بالمٌصرّف (compiler-specific hook)، وسيُتجاهَل بصمت من قبل المٌصرّفات التي لا تدعمه. ويجب تعديل المشاريع التي تستخدم ‎#pragma once‎ لتكون متوافقة مع المعايير، وقد يؤدّي ‎#pragma once‎ إلى تسريع كبير لعملية التصريف في بعض المُصرِّفات -خاصّة تلك التي تستخدم الترويسات المُصرّفة مسبقًا-. وبالمثل، تسرّع بعض المُعالجات الأوّلية عملية التصريف من خلال التحقق من الترويسات المُتضمّنة التي تستخدم الدروع. قد تزيد الفائدة من استخدام كل من ‎#pragma once‎ والدروع معًا أو تنقص بحسب التنفيذ ووقت التصريف. وقد كان يوصى بدمج ‎#pragma once‎ مع دُروع التضمين عند كتابة تطبيقات MFC على ويندوز، وكانت تُنشأ بواسطة الأدوات ‎add class‎ و ‎add dialog‎ و ‎add windows‎ الخاصة بـ Visual Studio، لذا من الشائع أن تجدها في تطبيقات C++‎ الموجّهة لويندوز. رسائل خطأ المُعالج الأوّلي يمكن توليد أخطاء التصريف باستخدام المعالج الأوّلي، هذا مفيد لعدد من الأسباب: إخطار المستخدم في حال كان على منصّة أو مٌصرّف غير مدعوم. إعادة خطأ في حال كان الإصدار أقل من gcc 3.0.0. #if __GNUC__ < 3 #error "This code requires gcc > 3.0.0" #endif إعادة خطأ إن كان التصريف على حاسوب Apple. #ifdef __APPLE__ #error "Apple products are not supported in this release" #endif هذا الدرس جزء من سلسلة مقالات عن C++‎. ترجمة -بتصرّف- للفصل Chapter 75: Preprocessor من كتاب C++ Notes for Professionals
  17. تُسنَد فئات القيمة إلى تعبيرات C++‎ بناءً على نتائج تلك التعبيرات، ويمكن لهذه الفئات أن تؤثّر على تحليل التحميل الزائد (overload resolution) للدوالّ في C++‎، كما تحدّد خاصّيتين مهمَّتين ومنفصلتين حول التعابير، تحدد الأول منهما إذا كان للتعبير هوية (identity)، أي إذا كان يشير إلى كائن له اسمُ متغيّرٍ (variable name)، حتى لو لم يكن اسم المتغيّر مُضمَّنًا في التعبير. والثانية هي إذا كان يجوز النقل ضمنيًا (implicitly move) من قيمة التعبير، أو بشكل أدقّ، إذا كان التعبير سيرتبط بأنواع المعاملات ذات القيمة اليمينية (r-value parameter types) عند استخدامه كمُعامل دالة أم لا. تُعرّف C++‎ ثلاث فئات تمثّل مجموعة من هذه الخصائص: lvalue - تعبير يساري (تعبيرات ذات هوّية، ولكن لا يمكن النقل منها). xvalue - تعبير مرتحل (تعبيرات ذات هوّية ويمكن النقل منها). prvalue - تعبير يميني خالص (تعبيرات بدون هوية ويمكن النقل منها). لا تحتوي C++‎ على تعبيرات ليس لها هوية ولا يمكن النقل منها. من ناحية أخرى، تعرّف C++‎ فئتي قيمَة أخريين، كل منهما تعتمد حصرًا على إحدى هذه الخصائص: glvalue - تعبير يساري مُعمّم النوع (تعبيرات ذات هوية) rvalue - تعبير يميني (تعبيرات يمكن النقل منها). ويمكن استخدام هذه كمجموعات لتصنيف الفئات السابقة. انظر هذا الرسم للتوضيح: القيم اليمينيّة rvalue التعبير اليمينيّ rvalue هو أيّ تعبير يمكن نقله ضمنيًا بغض النظر عما إذا كانت له هوية. وبتعبير أدق، يمكن استخدام التعبيرات اليمينيّة كوسائط للدوال التي تأخذ مُعاملات من النوع ‎T &&‎ ( يمثّل ‎T‎ نوع التعبير ‎expr‎)، والتعبيرات اليمينية هي وحدها التي يمكن تمريرها كوسائط لمعاملات مثل هذه الدوالّ. أما في حال استخدام تعبير غير يميني فإنّّ تحليل التحميل الزائد سيختار أيّ دالّة لا تستخدم مرجع يمينيًا (rvalue reference)، وسيطلق خطأً في حال تعذّر العثور عليها. تشمل فئة التعبيرات اليمينية حصرًا جميع تعبيرات xvalue و prvalue، وتحوّل دالّة المكتبة القياسية ‎std::move‎ تعبيرًا غير يمينيّ بشكل صريح إلى تعبير يميني، إذ تحوّل التعبير إلى تعبير من الفئة xvalue، فحتى لو كان التعبير يمينيا خالصًا ناقص الهوية (identity-less prvalue) من قبل، فإنّه سيكتسب هويةً -اسم معامِل الدالة- عبر تمريره كمُعامل إلى ‎std::move‎، ويصبح من الفئة xvalue. انظر المثال التالي: std::string str("init"); //1 std::string test1(str); //2 std::string test2(std::move(str)); //3 str = std::string("new value"); //4 std::string &&str_ref = std::move(str); //5 std::string test3(str_ref); //6 يأخذ منشئ السلاسل النصية مُعاملًا واحدًا من النوع std::string‎&&‎‏‏‎، ويُطلق على هذا المنشئ عادة "منشئ النقل" (move constructor)، لكن فئة القيمة الخاصة بـ ‎str‎ ليست يمينيّة (بل يساريّة)، لذا لا يمكن استدعاء التحميل الزائد للمنشئ. وبدلاً من ذلك يُستدعي التحميل الزائد لـ ‎const std::string&‎، أي مُنشئ النسخ. تتغير الأمور في السطر الثالث حيث تكون القيمة المُعادة من ‎std::move‎ هي ‎T&&‎، إذ يمثّل ‎T‎ النوع الأساسي للمُعامِل المُمرّر، لذا فإنّّ ‎std::move(str)‎ تُعيد ‎std::string&&‎. واستدعاء الدالة الذي تكون قيمته المعادة مرجعًا يمينيًا (rvalue reference) يُعدُّ تعبيرًا يمينيًا، وتحديدًا من فئة xvalue، لذا قد تستدعي منشئَ النقلِ للسلسلة النصية std::string. بعد ذلك، أي بعد السطر الثالث، يكون النقل من str قد تم، وصارت محتويات str غير محددة. يمرّر السطر 4 عنصرًا مؤقّتًا إلى مُعامل الإسناد الخاص بـ ‎std::string‎، الذي له تحميل زائد يأخذ std::string&&‎، ويكون التعبير std::string("new value")‎ تعبيرًا يمينيًا (على وجه التحديد من الفئة prvalue)، لذا قد يستدعي التحميل الزائد. وعليه سيُنقل العنصر المؤقّت إلى ‎str‎ مع استبدال المحتويات غير المُحدّدة بمحتويات محدّدة. ينشئ السطر 5 مرجعًا يمينيًا مُسمّى (named rvalue reference) يحمل الاسم ‎str_ref‎ ويشير إلى ‎str‎، وهنا تصبح فئات القيمة مُربكة. فرغم أن ‎str_ref‎ ستكون مرجعًا يمينيًّا يشير إلى ‎std::string‎، إلا أن فئة قيمة التعبير ‎str_ref‎ ليست يمينيّة بل يسارية، ولهذا لا يمكن للمرء استدعاء مُنشئ النقل الخاص بـ ‎std::string‎ مع التعبير ‎str_ref‎. ولنفس السبب فإن السطر 6 ينسخ قيمة ‎str‎ إلى ‎test3‎. وإذا أردنا نقله، سيتعيّن علينا توظيف ‎std::move‎ مرّة أخرى. القيمة المرتحلة xvalue التعابير من فئة القيمة المرتحلة xvalue (أو eXpiring value) هي تعابير ذات هوية تمثّل كائنات يمكن النقل منها ضمنيًا. وفكرتها بشكل عام هي أنّ الكائنات التي تمثّلها هذه التعبيرات هي على وشك أن تُدمَّر، وبالتالي فإنّ النقل منها ضمنيًا سيكون مقبولًا. انظر الأمثلة التالية على ذلك: struct X { int n; }; extern X x; 4; // prvalue: ليس لديها هوية x; // lvalue x.n; // lvalue std::move(x); // xvalue std::forward<X&>(x); // lvalue X { 4 }; // prvalue: ليس لها هوية X { 4 }.n; // xvalue: ليس لها هوية وتشير إلى موارد يمكن إعادة استخدامها تعبيرات القيمة اليمينية الخالصة prvalue تعبير القيمة اليمينيّة الخالصة prvalue (أو pure-rvalue) هي تعبيرات تفتقر إلى الهوية، ويُستخدم تقييمها عادة لتهيئة كائن يمكن نقله ضمنيًا. هذه بعض الأمثلة على ذلك: التعبيرات التي تمثّل كائنات مؤقّتة، مثل ‎std::string("123")‎. استدعاء دالة لا تعيد مرجعًا. كائن حرفي (باستثناء السلاسل النصية الحرفية - إذ أنها يسارية)، مثل ‎1‎ أو ‎true‎ أو ‎0.5f‎ أو ‎'a'‎. تعبيرات لامدا. لا يمكن تطبيق معامل العنونة (addressof operator) المُضمّن (‎&‎) على هذه التعبيرات. تعبيرات القيمة اليسارية lvalue التعبيرات اليسارية lvalue هي تعبيرات ذات هويّة لكن لا يمكن النقل منها ضمنيًا، وتشمل التعبيرات التي تتألّف من اسم متغيّر واسم دالّة والتعبيرات التي تستخدمها مُعاملات التحصيل (dereference) المُضمّنة والتعبيرات التي تشير إلى مراجع يسارية. تكون القيمة اليسارية عادة مجرّد اسم، لكن يمكن أن تأتي بأشكال أخرى أيضًا: struct X { … }; X x; // قيمة يسارية x X* px = &x; // px is an lvalue *px = X {}; // قيمة يمينيّة خالصة X{} هي قيمة يسارية، و *px X* foo_ptr(); // قيمة يمينيّة خالصة foo_ptr() X &foo_ref(); // قيمة يسارية foo_ref() في حين أنّ معظم الكائنات الحرفية (على سبيل المثال ‎4‎، ‎'x'‎) هي تعبيرات يمينية خالصة، إلا أن السلاسل النصية الحرفية يسارية. تعبيرات القيم اليسارية المُعمّمة glvalue تعابير القيم اليسارية المُعمّمة glvalue (أو "generalized lvalue") هي التعابير التي ليس لها هوية بغض النظر عما إذا كان يمكن النقل منها أم لا. وتشمل هذه الفئة تعبيرات القيم اليسارية (التعبيرات التي لها هوية ولكن لا يمكن النقل منها)، وتعبيرات القيمة المرتحلة xvalues (التعبيرات التي لها هوية، ويمكن النقل منها). بالمقابل، فهي لا تشمل القيم اليمينيةّ الخالصة prvalues (تعبيرات بدون هوية). وإذا كان لتعبيرٍ ما اسم، فإنّّه يساريّ معمَّم glvalue: struct X { int n; }; X foo(); X x; x; std::move(x); foo(); X {}; X {}.n; في الشيفرة السابقة: x له اسم، وعليه يكون يساريًا معمَّمًا. (std::move(x له اسم بما أننا ننقل من x، وعليه يكون قيمة يسارية معمَّمة glvalue لكن بما أننا نستطيع النقل منها فتكون قيمة مرتحلة وليست يسارية. ()foo ليس له اسم، فهو يميني خالص، وليس يساريا معمَّمًا. {} X قيمة مؤقتة ليس لها اسم، لذا فالتعبير يميني خالص، وليس يساريا معمَّمًا. X {}.n له اسم، لذا فهو يساري معمم، كما يمكن النقل منه، لذا فهو مرتحل وليس يساريًا معمَّمًا. هذا الدرس جزء من سلسلة دروس عن C++‎. ترجمة -بتصرّف- للفصل Chapter 74: Value Categories من كتاب C++ Notes for Professionals
  18. إمساك الاعتراضات Catching exceptions تُستخدَم الكتلة ‎try/catch‎ لإمساك الاعتراضات إذ توضع في القسم ‎try‎ الشيفراتُ التي يُشتبه في أنّها قد ترفع اعتراضًا، فيما تتكفّل الشيفرة الموضوعة في الكتلة ‎catch‎ بمعالجة الاعتراض حال رفعه. #include <iostream> #include <string> #include <stdexcept> int main() { std::string str("foo"); try { str.at(10); // std::out_of_range محاولة الدخول إلى العنصر قد تؤدي إلى رفع } catch (const std::out_of_range &e) { // وتحتوي رسالة توضيحية std::exception موروثة من what() std::cout << e.what(); } } يمكن استخدام عدة كتل ‎catch‎ للتعامل مع أكثر من نوع ٍمن الاعتراضات، وفي حال استخدام عدة عبارات ‎catch‎ فإنّّ آلية معالجة الاعتراضات ستحاول مطابقتها بحسب ترتيب ظهورها في الشيفرة: std::string str("foo"); try { str.reserve(2); // std::length_error محاولة تخصيص سعة زائدة قد تؤدي إلى رفع str.at(10); // std::out_of_range محاولة الدخول إلى هذا العنصر سترفع } catch (const std::length_error &e) { std::cout << e.what(); } catch (const std::out_of_range &e) { std::cout << e.what(); } يمكن إمساك أصناف الاعتراضات المشتقّة من صنف أساسي باستخدام عبارة ‎catch‎ واحدة مخصّصة للصنف الأساسي. ويمكن استبدال العبارتين ‎catch‎ الخاصتين بالاستثنائَين ‎std::length_error‎ و std::out_of_range في المثال أعلاه بعبارة ‎catch‎ واحدة موجّهة للاعتراض std:exception: std::string str("foo"); try { str.reserve(2); // std::length_error محاولة تخصيص سِعة زائدة قد تؤدي إلى رفع str.at(10); // std::out_of_range محاولة الدخول إلى هذا العنصر سترفع } catch (const std::exception &e) { std::cout << e.what(); } ونظرًا لأنّ عبارات ‎catch‎ تُختبَر بالترتيب، فتأكد من كتابة عبارات catch الأكثر تخصيصًا أولاً، وإلا فقد لا تُستدعى شيفرة الاعتراض المخصوصة أبدًا: try { /* شيفرة ترفع اعتراضً */ } catch (const std::exception &e) { /* std::exception معالجة كل الاعتراضات من النوع */ } catch (const std::runtime_error &e) { } لن تُنفَّذ الكتلة الأخيرة في الشيفرة السابقة لأن std::runtime_error ترث من std::exception، وقد أُمسكت كل اعتراضات std::exception من قِبل تعليمة catch التي سبقتها. هناك حلّ آخر، وهو استخدام عبارة catch تعالج كل الاعتراضات، على النحو التالي: try { throw 10; } catch (...) { std::cout << "caught an exception"; } إعادة رفع اعتراض قد ترغب أحيانًا أن تفعل شيئًا بالاعتراض الذي أمسكته، مثل كتابته في سجل الأخطاء، أو طباعة تحذير، ثمّ إعادة رفعه إلى النطاق الأعلى لكي يُعالج هناك. وهذا ممكن، إذ يمكنك إعادة رفع أيّ اعتراض تمسكه، انظر: try { ... // شيفرة هنا } catch (const SomeException &e) { std::cout << "caught an exception"; throw; } يؤدّي استخدام ‎throw;‎ بدون وسائط إلى إعادة رفع الاعتراض الممسوك حاليًا. الإصدار ≥ C++‎ 11 لإعادة رفع اعتراض ‎std::exception_ptr‎ سبقت إدارته، توفّر مكتبة C++‎ القياسية دالّة ‎rethrow_exception‎ يمكن استخدامها عبر تضمين ترويسة في البرنامج. #include <iostream> #include <string> #include <exception> #include <stdexcept> void handle_eptr(std::exception_ptr eptr) // لا حرج في التمرير بالقيمة { try { if (eptr) { std::rethrow_exception(eptr); } } catch (const std::exception &e) { std::cout << "Caught exception \"" << e.what() << "\"\n"; } } int main() { std::exception_ptr eptr; try { std::string().at(1); // std::out_of_range ًسيولّد هذا اعتراض } catch (...) { eptr = std::current_exception(); // إمساك } handle_eptr(eptr); } // eptr هنا عند تدمير std::out_of_range سيُستدعى مدمّر أفضل تطبيق: رفع الاعتراضات بالقيمة، وإمساكها بالمراجع الثابتة بشكل عام، يُوصى برفع الاعتراض بالقيمة (بدلاً من رفعه بالمؤشّر)، ولكن يوصى بإمساكه بالمراجع الثابتة. try { // throw new std::runtime_error("Error!"); // لا تفعل هذا // سينشئ هذا كائن اعتراض في الكومة، وسيتطلّب منك أن تمسك المؤشر وتدير الذاكرة بنفسك // وهذا قد يُسبِّب تسرّب الذاكرة throw std::runtime_error("Error!"); } catch (const std::runtime_error &e) { std::cout << e.what() << std::endl; } أحد الأسباب التي تجعل الإمساك بالمرجع أفضل، هو أنّه يلغي الحاجة إلى إعادة بناء الكائن عند نقله إلى كتلة catch (أو عند نشره إلى كتل catch الأخرى)، كما يتيح الإمساك بالمرجع معالجة الاعتراضات بأسلوب تعدد الأشكال، ويتجنّب تشريح (slicing) الكائنات، لكن إن كنت تريد إعادة رفع اعتراض (مثل ‎throw e;‎، انظر المثال أدناه)، فلا يزال بإمكانك تشريح الكائن لأنّ تعليمة ‎throw e;‎ تنشئ نسخة من الاعتراض من النّوع المُصرّح عنه: #include <iostream> struct BaseException { virtual const char *what() const { return "BaseException"; } }; struct DerivedException: BaseException { // اختيارية هنا "virtual" الكلمة المفتاحية virtual const char *what() const { return "DerivedException"; } }; int main(int argc, char **argv) { try { try { throw DerivedException(); } catch (const BaseException &e) { std::cout << "First catch block: " << e.what() << std::endl; // ==> First catch block: DerivedException throw e; سيغير ذلك الاعتراض من DerivedException إلى BaseException، تابع: } } catch (const BaseException &e) { std::cout << "Second catch block: " << e.what() << std::endl; // ==> Second catch block: BaseException } return 0; } إذا كنت متأكدًا من أنك لن تفعل أيّ شيء يؤدي إلى تغيير الاعتراض (مثل إضافة معلومات أو تعديل رسالة الخطأ المرفقة بالاعتراض)، فإنّّ إمساك الاعتراض بمرجع ثابت سيسمح للمٌصرّف بإجراء بعض التحسينات، ولكن هذا لن يلغي بالضرورة الحاجة إلى تشريح الكائن (كما هو مُوضّح في المثال أعلاه). تحذير: احذر من رفع اعتراضات غير مقصودة في كتلة ‎catch‎، خاصّة تلك المتعلّقة منها بتخصيص ذاكرةٍ أو موارد إضافية، فمثلًا قد يؤدّي إنشاء ‎logic_error‎ أو ‎runtime_error‎ أو الأصناف الفرعية المشتقّة منهما إلى رفع ‎bad_alloc‎ نتيجة نفاد الذاكرة عند نسخ السلسلة النصّية الخاصة بالاعتراض، وقد ترفع مجاري الدخل/الخرج اعتراضً أثناء تسجيل الأخطاء (logging) باستخدام أقنعة الاعتراضات (exception masks) المرتبطة بها. الاعتراضات المخصصة ينبغي ألّا ترفع قيمًا خامًا (raw values) كاعتراضات، بل استخدم أحد أصناف الاعتراضات القياسية أو اصنع واحدًا خاصًّا بك، ويُعدّ إنشاء صنف اعتراض موروث من ‎std::exception‎ أسلوبًا مستحسنًا. في المثال التالي، انظر صنف اعتراض مخصّص يرث مباشرةً من ‎std::exception‎: #include <exception> class Except: virtual public std::exception { protected: int error_number; ///< Error number int error_offset; ///< Error offset std::string error_message; ///< Error message public: /**Constructor (C++ STL string, int, int). * @param msg The error message * @param err_num Error number * @param err_off Error offset */ explicit Except(const std::string &msg, int err_num, int err_off): error_number(err_num), error_offset(err_off), error_message(msg) {} المدمر: يكون وهميًا (Virtual) من أجل السماح بالتصنيف الفرعي، تابع: virtual~Except() throw () {} نعيد مؤشرًا يشير إلى وصف الخطأ الثابت، ومؤشرًا يشير إلى *const char، وتحتوي الذاكرة الأساسية على الكائن Except، ويجب ألا يحاول المستدعون أن يحرروا الذاكرة. انظر: virtual const char *what() const throw () { return error_message.c_str(); } /**Returns error number. * @return #error_number */ virtual int getErrorNumber() const throw () { return error_number; } /**Returns error offset. *@return #error_offset */ virtual int getErrorOffset() const throw () { return error_offset; } }; هذا مثال تطبيقي: try { throw (Except("Couldn't do what you were expecting", -12, -34)); } catch (const Except &e) { std::cout << e.what() << "\nError number: " << e.getErrorNumber() << "\nError offset: " << e.getErrorOffset(); } بهذه الطريقة فإنّك لا ترفع رسالة خطأ وحسب، ولكن ترفع أيضًا بعض القيم الأخرى التي توضّح طبيعة الخطأ بالضبط، وهكذا يصبح التعامل مع الأخطاء أسهل وأيسر. يوجد صنف اعتراض يتيح لك معالجة رسائل الخطأ بسهولة، وهو ‎std::runtime_error‎، تستطيع الاشتقاق من هذا الصنف على النحو التالي: #include <stdexcept> class Except: virtual public std::runtime_error { protected: int error_number; ///< Error number int error_offset; ///< Error offset public: /**Constructor (C++ STL string, int, int). * @param msg The error message * @param err_num Error number * @param err_off Error offset */ explicit Except(const std::string &msg, int err_num, int err_off): std::runtime_error(msg) { error_number = err_num; error_offset = err_off; } /** المدمر * للسماح بالاشتقاق Virtual الكلمة المفتاحية */ virtual~Except() throw () {} /**Returns error number. * @return #error_number */ virtual int getErrorNumber() const throw () { return error_number; } /**Returns error offset. *@return #error_offset */ virtual int getErrorOffset() const throw () { return error_offset; } }; لاحظ أنّك لم تُعِدْ تعريف الدالّة ‎what()‎ من الصنف الأساسي (‎std::runtime_error‎)، أي أنّنا سنستخدم إصدار الصنف الأساسي من ‎what()‎، لكن لا شيء يمنعك من إعادة تعريفها إن أردت. std::uncaught_exceptions الإصدار ≥ C++‎ 17 تقدّم C++‎ 17 النوع ‎int std::uncaught_exceptions()‎ (ليحلّ محلّ النوع ‎bool std::uncaught_exception()‎ المحدود) لمعرّفة عدد الاعتراضات غير الممسوكة حاليًا، يتيح هذا للأصناف معرفة ما إذا كان الاعتراض قد دُمِّر أثناء فكّ المكدّس (stack unwinding) أم لا. #include <exception> #include <string> #include <iostream> // تطبيق التغيير عند التدمير: // في حال رفع اعتراض: Rollback. // غير ذلك: Commit. class Transaction { public: Transaction(const std::string &s): message(s) {} Transaction(const Transaction &) = delete; Transaction &operator=(const Transaction &) = delete; void Commit() { std::cout << message << ": Commit\n"; } void RollBack() noexcept(true) { std::cout << message << ": Rollback\n"; } // ... ~Transaction() { if (uncaughtExceptionCount == std::uncaught_exceptions()) { Commit(); // قد يُطرَح اعتراض } else { // فك المكدّس الحالي RollBack(); } } private: std::string message; int uncaughtExceptionCount = std::uncaught_exceptions(); }; class Foo { public: ~Foo() { try { Transaction transaction("In ~Foo"); // حتى لو كان هناك اعتراض غير ممسوك commit الاعتماد //... } catch (const std::exception &e) { std::cerr << "exception/~Foo:" << e.what() << std::endl; } } }; int main() { try { Transaction transaction("In main"); // RollBack (تراجع) Foo foo; // يعتمد المعاملة ~Foo //... throw std::runtime_error("Error"); } catch (const std::exception &e) { std::cerr << "exception/main:" << e.what() << std::endl; } } الناتج: In~Foo: Commit In main: Rollback exception / main: Error استخدام كتلة try في الدوال العادية إليك المثال التوضيحي التالي: void function_with_try_block() try { // شيفرة هنا } catch (...) { // شيفرة هنا } والذي يكافئ: void function_with_try_block() { try { // شيفرة هنا } catch (...) { // شيفرة هنا } } لاحظ أنّه بالنسبة للمنشئات والمدمّرات فإنّّ السلوك يكون مختلفًا لأنّ كتلة catch تعيد رفع اعتراض على أيّ حال (الاعتراض الممسوك في حال لم يكن هناك رفع لاعتراض آخر في جسم كتلة catch). ويُسمح للدّالّة ‎main‎ أن تحتوي على كتلة try مثل أيّ دالّة أخرى، بيْد أنّ كتلة try في الدالة ‎main‎ لن تمسك الاعتراضات التي تحدث أثناء إنشاء المتغيّرات الساكنة غير المحلية، أو عند تدمير أيّ متغيّر ساكن، لكن بدلاً من ذلك تُستدعى ‎std::terminate‎. الاعتراض المتداخل الإصدار ≥ C++‎ 11 أثناء معالجة الاعتراض، يمكن إمساك اعتراض عام من دالّة ذات مستوى منخفض (مثل خطأ في نظام الملفّات أو خطأ في نقل البيانات) ورفع اعتراض عالي المستوى أكثر تخصيصًا يشير إلى أنّه قد تعذّر تقديم بعض العمليات عالية المستوى (مثل عدم القدرة على نشر صورة على الويب). يسمح ذلك بالاستجابة للمشاكل الخاصّة بالعمليات عالية المستوى، كما يسمح للمبرمج بإيجاد مكان وقوع الاعتراض في التطبيق، الجانب السلبي في هذا الحل هو أنّ مكدّس الاستدعاءات (callstack) الخاص بالاعتراض سيُقتطَع، وسيُفقد الاعتراض الأصلي، ويفرض هذا على المطوّرين تضمين نصّ الاعتراض الأصلي يدويًا في آخر اعتراض مُنشأ. تهدف الاعتراضات المتداخلة std::nested_exception إلى حل هذه المشكلة عن طريق إرفاق اعتراض منخفض المستوى، والذي يصف سبب رفع الاعتراض، باعتراض عالي المستوى، والذي يصف ما يعنيه الاعتراضُ في هذه الحالة بالذات. يسمح الاعتراض المتداخل بتداخل الاعتراضات بفضل std::throw_with_nested: #include <stdexcept> #include <exception> #include <string> #include <fstream> #include <iostream> struct MyException { MyException(const std::string &message): message(message) {} std::string message; }; void print_current_exception(int level) { try { throw; } catch (const std::exception &e) { std::cerr << std::string(level, ' ') << "exception: " << e.what() << '\n'; } catch (const MyException &e) { std::cerr << std::string(level, ' ') << "MyException: " << e.message << '\n'; } catch (...) { std::cerr << "Unkown exception\n"; } } void print_current_exception_with_nested(int level = 0) { try { throw; } catch (...) { print_current_exception(level); } try { throw; } catch (const std::nested_exception &nested) { try { nested.rethrow_nested(); } catch (...) { print_current_exception_with_nested(level + 1); // (Recursion) ذاتية } } catch (...) { //فارغ // (Recursion) إنهاء الذاتية } } // دالة بسيطة تمسك اعتراضً وتغلّفه في اعتراض متداخل void open_file(const std::string &s) { try { std::ifstream file(s); file.exceptions(std::ios_base::failbit); } catch (...) { std::throw_with_nested(MyException { "Couldn't open " + s }); } } // دالة بسيطة تمسك اعتراضً وتغلّفه في اعتراض متداخل void run() { try { open_file("nonexistent.file"); } catch (...) { std::throw_with_nested(std::runtime_error("run() failed")); } } // تشغيل الدالة أعلاه وطباعة الاعتراض الممسوك int main() { try { run(); } catch (...) { print_current_exception_with_nested(); } } الخرج المحتمل: exception: run() failed MyException: Couldn 't open nonexistent.file exception: basic_ios::clear إذا كنت تعمل حصرًا مع الاعتراضات الموروثة من ‎std::exception‎، فهناك إمكانية لتبسيط الشيفرة أكثر. استخدام كتلة Try في المنشِئات الطريقة التالية هي الوحيدة لإمساك اعتراض في قائمة مهيئ: struct A: public B { A() try: B(), foo(1), bar(2) { // جسم المنشئ } catch (...) { تُمسَك الاعتراضات الناجمة عن قائمة المهيئ والمنشئ هنا، وسيعاد رفع الاعتراض الممسوك عند عدم رفع أي اعتراض، نتابع المثال: } private: Foo foo; Bar bar; }; استخدام كتلة Try في المدمّرات انظر المثال التوضيحي التالي: struct A { ~A() noexcept(false) try { // جسم المدمّر } catch (...) { // الاعتراضات الناجمة عن جسم المدمّر تُمسَك هنا // في حال عدم رفع أيّ اعتراض هنا، فسيُعاد رفع الاعتراض الممسوك } }; رغم أنّه يمكن رفع اعتراض من المدمّر، إلّا أنّه ينبغي التزام الحذر، ذلك أنّه في حال رفع اعتراض في مدمّر مُستدعًى أثناء فكّ المكدّس، فإنّّ ذلك سيتسبّب في استدعاء ‎std::terminate‎. هذا الدرس جزء من سلسلة دروس عن C++‎. ترجمة -بتصرّف- للفصل Chapter 72: Exceptions من كتاب C++ Notes for Professionals
  19. حجم الأنواع العددية الصحيحة الأنواع التالية هي أنواع عددية صحيحة: ‎char‎ الأنواع العددية الصحيحة المُؤشّرة Signed integer types الأنواع العددية الصحيحة غير المُؤشّرة Unsigned integer types ‎char16_t‎ و ‎char32_t‎ ‎bool‎ ‎wchar_t‎ باستثناء ‎sizeof(char)‎ و ‎sizeof(signed char)‎ و ‎sizeof(unsigned char)‎، الموجودة بين الفقرة ‎‎ 3.9.1.1 [basic.fundamental/1] ‎‎ و‎‎الفقرة 5.3.3.1 ‎‎[expr.sizeof]‎‎ و ‎sizeof(bool)‎، والتي تتوقف على التنفيذ بالكامل وليس لها حدّ أدنى، فإنّّ متطلّبات الحد الأدنى لأحجام هذه الأنواع موجود في القسم 3.9.1 ‏‎[basic.fundamental]‎‎ من المعيار، وسنوضّحه أدناه. حجم char تحدّد جميع إصدارات معيار C++‎، في الفقرة 5.3.3.1، أنّ ‎sizeof‎ تعيد القيمة ‎1‎ لكلّ من ‎unsigned char‎ و ‎signed char‎ و ‎char‎ (مسألة ما إذا كان النوع ‎char‎ مؤشّرا - ‎signed‎ - أو غير مؤشّر - ‎unsigned‎ - تتعلّق بالتنفيذ). الإصدار ≥ C++‎ 14 النوع ‎char‎ كبير بما يكفي لتمثيل 256 قيمة مختلفة، وهو مناسب لتخزين وحدات رموز UTF-8. حجم الأنواع العددية الصحيحة المُؤشّرة وغير المُؤشّرة تنصّ المواصفات القياسية، في الفقرة 3.9.1.2، أنّه في قائمة أنواع الأعداد الصحيحة القياسية المُؤشّرة، التي تتكون من ‎signed char‎ و ‎short ‎int‎ و ‎int‎ و ‎long ‎int‎ و ‎long ‎long ‎int‎، فإنّّ كل نوع سيوفّر مساحة تخزينية تكافئ على الأقل المساحة التخزينيّة للنّوع السابق في القائمة. إضافة لذلك، وكما هو مُوضّح في الفقرة 3.9.1.3، كل نوع من هذه الأنواع يقابله نوع صحيح قياسي غير مُؤشّر، هذه الأنواع هي: unsigned char unsigned short int unsigned int unsigned long int unsigned long long int هذه الأنواع في النهاية لها نفس حجم ومُحاذاة النوع المؤشّر المقابل بالإضافة إلى ذلك، وكما هو مُوضّح في الفقرة 3.9.1.1، النوع ‎char‎ له نفس متطلّبات ‎signed char‎ و ‎unsigned char‎ فيما يخصّ الحجم والمحاذاة. الإصدار < C++‎ 11 قبل الإصدار C++‎ 11، لم يكن ‎long long‎ و ‎unsigned long long‎ جزءًا من معيار C++‎. لكن بعد إدخالهما في لغة C في معيار C99، دعمت العديدُ من المصرّفات النوع ‎long long‎ كنوع عددي صحيح مؤشّر، و unsigned long long كنوع عددي صحيح غير مؤشّر موسّع، له نفس قواعد أنواع C. يضمن المعيار ما يلي: 1 == sizeof(char) == sizeof(signed char) == sizeof(unsigned char) <= sizeof(short) == sizeof(unsigned short) <= sizeof(int) == sizeof(unsigned int) <= sizeof(long) == sizeof(unsigned long) الإصدار ≥ C++‎ 11 <= sizeof(long long) == sizeof(unsigned long long) لا ينصّ المعيار على حد أدنى للأحجام لكل نوع على حدة. بدلاً من ذلك، لكلّ نوعٍ من الأنواع مجالًا من الحدود الدنيا التي يمكن أن يدعمها، والتي تكون موروثة على النحو المُوضّح في الفقرة 3.9.1.3 من معيار C، في الفقرة 1.2.4.2.1. ويمكن استنتاج الحد الأدنى لحجم نوع ما من ذلك المجال من خلال تحديد الحد الأدنى لعدد البتّات المطلوبة، لاحظ أنّه في معظم الأنظمة قد يكون المجال المدعوم الفعلي لأيّ نوع أكبر من الحد الأدنى، وأنه في الأنواع المُؤشّرة تتوافق المجالات مع مكمّل (complement) واحد، وليس مع مكمّلين اثنين كما هو شائع؛ وذلك لتسهيل توافق مجموعة واسعة من المنصات مع المعايير. النوع النطاق الأدنى العدد الأدنى المطلوب للبِتَّات signed char -127 إلى 127 8 unsigned char 0 إلى 255 8 signed short -32,767 إلى 32,767 16 unsigned short 0 إلى 65,535 16 signed int -32,767 إلى 32,767 16 unsigned int 0 إلى 65,535 16 signed long -2,147,483,647 إلى 2,147,483,647 32 unsigned long 0 إلى 4,294,967,295 32 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; } الإصدار ≥ C++‎ 11 النوع النطاق الأدنى العدد الأدنى المطلوب للبِتَّات signed long long -9,223,372,036,854,775,807 إلى 9,223,372,036,854,775,807 64 unsigned long long 0 إلى 18,446,744,073,709,551,615 64 قد تختلف أحجام الأنواع من تنفيذ لآخر بما أنه يُسمح للأنواع أن تكون أكبر من الحد الأدنى لمتطلّبات الحجم، وأبرز مثال على ذلك تجده في نموذَجي البيانات المخزّنة على 64 بتّة: LP64 و LLP64، إذ أنّه في أنظمة LLP64 (مثل Windows 64-bit)، فإنّ الأنواع ‎ints‎ و ‎long‎ تُخزّن على 32 بتّة (32-bit)، وفي LP64 (مثل 64-bit Linux)، فإنّ ‎int‎ مخزّنة على 32 بتّة، أمّا ‎long‎ فمُخزّنة على 64 بتّة. لهذا من الخطأ افتراض أنّ أنواع الأعداد الصحيحة لها نفس الحجم في جميع الأنظمة. الإصدار ≥ C++‎ 11 إذا كانت هناك حاجة إلى أنواع عددية صحيحة ذات أحجام ثابتة فاستخدم أنواعًا من الترويسة ، لكن لاحظ أنّ المعيار لا يجبر التنفيذ (implementations) على دعم الأنواع ذات الحجم المضبوط: ‎int8_t‎ ‎int16_t‎ ‎int32_t‎ ‎int64_t‎ ‎intptr_t‎ ‎uint8_t‎ ‎uint16_t‎ ‎uint32_t‎ ‎uint64_t‎ ‎uintptr_t‎ الإصدار ≥ C++‎ 11 حجم char16_t‎ و char32_t‎ يتعلق حجم ‎char16_t‎ و ‎char32_t‎ بالتنفيذ كما ينصّ على ذلك المعيار في الفقرة 5.3.3.1، مع الشروط الواردة في الفقرة 3.9.1.5: ‎char16_t‎ كبير بما يكفي لتمثيل أيّ وحدة رمز من UTF-16، كما أنّ له نفس الحجم والإشارة والمحاذاة التي للنوع ‎uint_least16_t‎، وعليه فحجمه يساوي 16 بتّة على الأقل. ‎char32_t‎ كبير بما يكفي لتمثيل أي وحدة رمز في UTF-32، كما أنّ له نفس الحجم والإشارة والمحاذاة التي للنوع ‎uint_least32_t‎، وعليه فيجب أن يساوي حجمه 32 بتّة على الأقل. حجم bool يتعلق حجم ‎bool‎ بالتنفيذ، ولا يساوي بالضرورة ‎1‎. حجم wchar_t ‎wchar_t‎، كما هو مُوضّح في الفقرة 3.9.1.5 هو نوع متميّز إذ يمكن أن يمثّل مجال قيمه كل وحدات الرموز (code unit) في أكبر مجموعة محارف موسّعة من بين اللغات المدعومة، وله نفس الحجم والإشارة والمحاذاة لأحد الأنواع العددية الصحيحة الأخرى، والذي يمثّل نوعه الأساسي (underlying type). يتعلق حجم هذا النوع بالتنفيذ كما هو مُعرَّف في الفقرة 5.3.3.1، وقد يساوي على سبيل المثال 8 أو 16 أو 32 بتّة على الأقل؛ وإذا كان النظام يدعم اليونيكود فينبغي أن يُخزّن النوع ‎wchar_t‎ على 32 بتّة على الأقل (باستثناء Windows، إذ يُخزّن النّوع ‎wchar_t‎ على 16 بتّة لأغراض التوافق). وهو موروث من معيار C90، في ISO 9899: 1990 الفقرة 4.1.5، لكن مع بعض التعديلات البسيطة. ويساوي حجم ‎wchar_t‎ غالبًا 8 أو 16 أو 32 بتّة، وذلك حسب التنفيذ. مثلًا: في أنظمة يونكس والأنظمة المشابهة لها، تُخزّن ‎wchar_t‎ على 32 بتّة، وعادة ما تستخدم في UTF-32. في ويندوز، تُخزّن ‎wchar_t‎ على 16 بتّة، وتُستخدم مع UTF-16. على الأنظمة التي لا تدعم إلّا 8 بتّات فقط، تُخزّن ‎wchar_t‎ على 8 بتّات. الإصدار ≥ C++‎ 11 إذا كنت تريد دعم اليونيكود، فإنّه يُوصى باستخدام ‎char‎ لأجل الترميز UTF-8، و ‎char16_t‎ لأجل الترميز UTF-16، و ‎char32_t‎ لأجل الترميز UTF-32، بدلاً من استخدام ‎wchar_t‎. نماذج البيانات يمكن أن تختلف أحجام أنواع الأعداد الصحيحة بين المنصات كما ذكرنا أعلاه، وما يلي هي أكثر النماذج شيوعًا (الأحجام محسوبة بالبتّات): النموذج int long مؤشر LP32 (2/4/4) 16 32 32 ILP32 (4/4/4) 32 32 32 LLP64 (4/4/8) 32 32 64 LP64 (4/8/8) 32 64 64 من بين هذه النماذج: نظام ويندوز 16-بت استخدم LP32. الأنظمة الشبيهة بيونكس مثل يونكس ولينكس ونظام ماك 10 (Mac OSX) وغيرها، ذات معمارية 32 منها، وكذلك نظام ويندوز 32-بت، تستخدم جميعها ILP32. ويندوز ‎‎64-bit يستخدم LLP64. اليونكسات ذات معمارية ‎‎64-bit تستخدم LP64. لاحظ أنّ هذه النماذج غير مذكورة في المعيار. النوع Char قد يكون مُؤشَّرًا أو لا لا ينصّ المعيار على وجوب أن يكون النوع ‎char‎ مؤشَّرًا من عدمه، لذا فإنّّ كل مصرّف ينفذه بشكل مختلف، أو قد يسمح بتعديله باستخدام سطر الأوامر. مجالات الأنواع العددية تتعلّق مجالات أنواع الأعداد الصحيحة بالتنفيذ، كما توفّر الترويسة قالب std::numeric_limits<T>‎‎ الذي يوفّر الحدّين الأدنى والأقصى لقيم جميع الأنواع الأساسية. تفي القيم بالضمانات التي يحدّدها معيار C عبر الترويسات و C++‎ 11 ‎‎‎‎‎‎<=‎‎ ) <cinttypes) std::numeric_limits<signed char>::min()‎‎ - تساوي SCHAR_MIN، وهي أصغر من أو تساوي ‎‎-127. ‎std::numeric_limits::max()‎ تساوي SCHAR_MAX، وهي أكبر من أو تساوي 127. std::numeric_limits<unsigned char>::max()‎‎ تساوي UCHAR_MAX، وهي أكبر من أو تساوي 255. std::numeric_limits<short>::min()‎‎ تساوي SHRT_MIN، وهي أصغر من ‎‎-32767 أو تساويها. std::numeric_limits<short>::max()‎‎ تساوي SHRT_MAX، وهي أكبر من أو تساوي 32767. std::numeric_limits<unsigned short>::max()‎‎ تساويUSHRT_MAX، وهي أكبر من أو تساوي 65535. std::numeric_limits<int>::min() تساوي INT_MIN، وهي أصغر من ‎‎-32767 أو تساويها. std::numeric_limits<int>::max()‎‎ تساوي INT_MAX، وهي أكبر من أو تساوي 32767. ‎‎std::numeric_limits<unsigned int>::max()‎ تساوي UINT_MAX، وهي أكبر من أو تساوي 65535. std::numeric_limits<long>::min()‎‎ تساوي LONG_MIN، وهي أصغر من أو تساوي ‎‎-2147483647. std::numeric_limits<long>::max()‎‎ تساوي LONG_MAX، وهي أكبر من أو تساوي 2147483647. std::numeric_limits<unsigned long>::max()‎‎ تساوي ULONG_MAX، وهي أكبر من أو تساوي 4294967295. ‎‎الإصدار ≥ C++11 std::numeric_limits<long long>::min()‎‎ تساوي ‎LLONG_MIN‎، وهي أكبر من أو تساوي ‎‎-9223372036854775807. std::numeric_limits<long long>::max()‎‎ تساوي ‎LLONG_MAX‎، وهي أكبر من أو تساوي 9223372036854775807. std::numeric_limits<unsigned long long>::max() تساوي ‎ULLONG_MAX‎، وهي أكبر من أو تساوي 18446744073709551615. بالنسبة للنوع العشري‏ ‎T‎، فإنّّ ‎max()‎ تمثّل القيمة المنتهية (finite) القصوى، بينما تمثّل ‎min()‎ الحدّ الأدنى للقيمة المُوحّدة الموجبة. تمّ توفير بعض الأعضاء الإضافيين للأنواع العشرية، وهي متعلّقة بالتنفيذ أيضًا، ولكنّها تلبّي بعض الضمانات التي يحدّدها المعيار C عبر الترويسة . يعيد ‎digits10‎ عدد الأرقام العشرية الخاصّة بالدقة. std::numeric_limits<float>::digits10 تساوي FLT_DIG، والتي لا تقلّ عن 6. std::numeric_limits<double>::digits10 تساوي DBL_DIG، والتي لا تقلّ عن 10. ‎‎std::numeric_limits<long double>::digits10 تساوي LDBL_DIG، والتي لا تقلّ عن 10. العضو ‎min_exponent10‎ هو الحد الأدنى السلبي E بحيث يكون 10 أسّ E طبيعيًّا. std::numeric_limits<float>::min_exponent10 تساوي ‎FLT_MIN_10_EXP‎، والتي يساوي على الأكثر ‎‎-37. std::numeric_limits<double>::min_exponent10 تساوي ‎DBL_MIN_10_EXP‎، والتي تساوي على الأكثر ‎‎-37. std::numeric_limits<long double>::min_exponent10 تساوي ‎LDBL_MIN_10_EXP‎، والتي تساوي على الأكثر ‎‎-37. العضو ‎max_exponent10‎ هو الحد الأقصى E بحيث يكون 10 أسّ E منتهيًا (finite). std::numeric_limits<float>::max_exponent10 يساوي ‎FLT_MIN_10_EXP‎، ولا يقل عن 37. std::numeric_limits<double>::max_exponent10 يساوي ‎DBL_MIN_10_EXP‎، ولا يقل عن 37. std::numeric_limits<long double>::max_exponent10 تساوي ‎LDBL_MIN_10_EXP‎، ولا يقل عن37. إذا كان العضو ‎is_iec559‎ صحيحًا، فإنّّ النوع سيكون مطابقًا للمواصفات IEC 559 / IEEE 754، وبالتالي سيُحدَّد مجاله من قبل المعيار. تمثيل قيم الأنواع العشرية ينصّ المعيار أن لا تقلّ دقة النوع ‎long double‎ عن دقة النوع ‎double‎، والذي ينبغي ألّا تقل دقّته عن دقّة النوع ‎float‎؛ وأنّ النوع ‎long double‎ ينبغي أن يكون قادرًا على تمثيل أيّ قيمة يمثّلها النوع ‎double‎، وأن يمثّل ‎double‎ أيّ قيمة يمكن أن يمثّلها النوع ‎float‎، أما تفاصيل التمثيل فتتعلّق بالتنفيذ. وبالنسبة لنوع عشري ‎T‎، فإنّّ ‎std::numeric_limits<T>::radix‎ تحدّد الجذر المُستخدم في تمثيل ‎T‎، وإذا كانت ‎std::numeric_limits<T>::is_iec559‎ صحيحة، فإنّّ تمثيل ‎T‎ يطابق أحد التنسيقات المُعرَّفة من قبل معيار IEC 559 / IEEE 754. التدفق الزائد عند التحويل من عدد صحيح إلى غد صحيح مُؤشّر عند تحويل عدد صحيح مؤشّر أو غير مؤشّر إلى نوع عددي صحيح مؤشّر ولا تكون قيمته قابلة للتمثيل في النوع المقصود، فإنّّ القيمة المُنتجة تتعلّق بالتنفيذ. مثلًا، لنفرض أن مجال النوع signed char في هذا التنفيذ يكون [-128,127]، ومجال النوع unsigned char من 0 حتى 255: int x = 12345; signed char sc = x; // معرفة بالتنفيذ sc قيمة unsigned char uc = x; // مهيأة عند القيمة 57 uc النوع الأساسي وحجم التعدادات إذا لم يكن النوع الأساسي (underlying type) لنوع تعدادي مُحدّدا بشكل صريح، فإنّّه سيُعرَّف من قبل التنفيذ. enum E { RED, GREEN, BLUE, }; using T = std::underlying_type<E>::type; // يعرِّفه التنفيذ ومع ذلك، فإنّّ المعيار ينصّ على ألّا يكون نوع التعداد الأساسي أكبر من ‎int‎ إلّا إن لم يكن النوعان ‎int‎ و ‎unsigned‎ ‎int‎ قادريْن على تمثيل جميع قيم التعداد. لذلك في الشيفرة أعلاه، النوع T يمكن أن يكون ‎int‎ أو ‎unsigned int‎ أو ‎short‎ ولكن ليس ‎long long‎ مثلًا. لاحظ أنّ التعداد له نفس حجم نوعه الأساسي (كما يعيده التابع ‎sizeof‎). القيمة العددية لمؤشر نتيجة تحويل مؤشّر إلى عدد صحيح باستخدام ‎reinterpret_cast‎ تتعلّق بالتنفيذ، لكن "… يُهدف إلى ألا تكون النتيجة مفاجئة للمطوّرين الذين يعرفون نظام العنونة في الجهاز." int x = 42; int *p = &x; long addr = reinterpret_cast<long> (p); std::cout << addr << "\n"; // طبع رقم عنوان وبالمثل، فإنّّ المُؤشر الناتج عن تحويل عدد صحيح يكون أيضًا متعلّقًا بالتنفيذ، والطريقة الصحيحة لتخزين مؤشّر كعدد صحيح هي استخدام النوعين ‎uintptr_t‎ أو ‎intptr_t‎، انظر المثال التالي حيث لم يكن uintptr_t في C++03 وإنما في C99، كنوع اختياري في الترويسة : #include <stdint.h> uintptr_t uip; الإصدار ≥ C++‎ 11 يوجد std::uintptr_t اختياري في C++11: #include <cstdint> std::uintptr_t uip; تُحيل C++‎ 11 إلى C99 لتعريف ‎uintptr_t‎ (المعيار C99، ‏6.3.2.3): بالنسبة لغالبية المنصات الحديثة، يمكنك أن تفترض أنّ مساحة العنونة (address space) مسطحة وأنّ الحسابيات على ‎uintptr_t‎ تكافئ الحسابيات على ‎char *‎، ومن الممكن أن يجري التنفيذ أيّ تعديل عند تحويل ‎void *‎ إلى ‎uintptr_t‎ طالما أنّه يمكن عكس التعديل عند التحويل من ‎uintptr_t‎ إلى ‎void *‎. مسائل تقنية في الأنظمة المتوافقة مع XSI ‏(X/Open System Interfaces)، فإنّّ النوعين ‎intptr_t‎ و ‎uintptr_t‎ إلزاميان، أمّا في الأنظمة الأخرى فهما اختياريان. الدوال ليست كائنات ضمن معيار C، ولا يضمن معيار C أنّ ‎uintptr_t‎ يستطيع تخزين مؤشّر دالة. كذلك يتطّلب التوافق مع POSIX ‏(2.12.3) أنّ: معيار C99 الفقرة 7.18.1: قد يكون ‎uintptr_t‎ مناسبًا إذا كنت تريد العمل على بتّات المؤشّر بشكل قد يتعذّر في حال استخدمت عددًا صحيحًا مؤشّرًا. عدد البتات في البايت البايت (byte) في C++‎ هو المساحة التي يشغلها كائن ‎char‎، وعدد البتات في البايت مُحدّد في ‎CHAR_BIT‎، ومُعرّف في ‎climits‎ ولا يقل عن 8. وفي حين أن عدد بتّات البايت في معظم الأنظمة الحديثة هو 8 وأن أنظمة POSIX تتطلّب أن يساوي ‎CHAR_BIT‎ القيمة 8 بالضبط، إلا أنّ هناك بعض الأنظمة التي يكون فيها ‎CHAR_BIT‎ أكبر من 8، فقد يساوي مثلًا 8 أو 16 أو 32 أو 64 بتّة. هذا الدرس جزء من سلسلة دروس عن C++‎. ترجمة -بتصرّف- للفصل Chapter 71: Implementation-defined behavior من كتاب C++ Notes for Professionals
  20. التعبيرات النمطية (تُسمّى أحيانًا regexs أو regexps) هي صِيغ نصّية تمثّل الأنماط التي يمكن مطابقتها في السلاسل النصّية، وقد تدعم التعبيرات النمطيّة التي قُدِّمت في C++‎ 11 -اختياريًا- إعادة مصفوفة من السلاسل النصّية المطابِقة، أو صيغة نصّية أخرى تحدّد كيفيّة استبدال الأنماط المتطابقة في السلاسل النصية. الصياغة الأولى للدالة regex_match: bool regex_match(BidirectionalIterator first, BidirectionalIterator last, smatch& sm, const regex& re, regex_constraints::match_flag_type flags) يمثّل BidirectionalIterator أيَّ مُكرّر محارف يوفّر عامليْ الزيادة (increment) والإنقَاص (decrement). والوسيط smatch يمكن أن يكون كائنًا ‎cmatch‎ أو أيّ متغيّر آخر من الصنف ‎match_results‎ يقبل النوع ‎BidirectionalIterator‎، ويمكن حذف هذا الوسيط إذا لم تكن بحاجة إلى إعادة نتائج التعبير النمطي. وتعيد ما إذا كان التعبير ‎re‎ يطابق كامل تسلسل المحارف المُعُرّف بواسطة ‎first‎ و ‎last‎. الصياغة الثانية للدالة regex_match: bool regex_match(const string& str, smatch& sm, const regex re&, regex_constraints::match_flag_type flags) قد تكون string من النّوع ‎const char*‎ أو قيمة نصّية يسارية، وتُحذَف الدوالّ التي تقبل سلسلة نصيّة يمينية R- بشكل صريح. الوسيط smatch يمكن أن يكون كائنًا ‎cmatch‎ أو أيّ متغيّر آخر من الصنف ‎match_results‎ يقبل سلسلة نصية. ويمكن حذف الوسيط ‎smatch‎ إذا لم تكن بحاجة إلى إعادة نتائج التعبير النمطي .تعيد الدالة regex_match ما إذا كان التعبير ‎re‎ قد طابق كامل السلسلة النصية str. أمثلة عن regex_match و regex_search const auto input = "Some people, when confronted with a problem, think \"I know, I'll use regular expressions.\""s; smatch sm; cout << input << endl; إن انتهى input بعلامة تنصيص تحتوي كلمة تبدأ بالكلمة "reg" وكلمة أخرى تبدأ بـ "ex" فالتقط الجزء السابق من input. if (regex_match(input, sm, regex("(.*)\".*\\breg.*\\bex.*\"\\s*$"))) { const auto capture = sm[1].str(); cout << '\t' << capture << endl; // الخرج: "\tSome people, when confronted with a problem, think\ n "; ابحث في الجزء الملتقط عن "a problem" أو "problems #". if (regex_search(capture, sm, regex("(a|d+)\\s+problems?"))) { const auto count = sm[1] == "a"s ? 1 : stoi(sm[1]); cout << '\t' << count << (count > 1 ? " problems\n" : " problem\n"); // الخرج: --> "\t1 problem\ n " cout << "Now they have " << count + 1 << " problems.\n"; // الخرج: "Now they have 2 problems\ n " } } هذا مثال حيّ على ذلك. مثال عن مُكرّر التعبيرات النمطية regex_iterator تُعد ‎regex_iterator‎ خيارًا ممتازًا عند معالجة الخرج الملتقَط بشكل متكرر، وسيعيد تحصيل ‎regex_iterator‎ كائن ‎match_result‎، وهذا يفيد في الالتقاطات الشرطية (conditional captures) أو الالتقاطات المترابطة. لنقل أنّنا نريد تقطيع (tokenize) مقتطف من شيفرة C++‎: enum TOKENS { NUMBER, ADDITION, SUBTRACTION, MULTIPLICATION, DIVISION, EQUALITY, OPEN_PARENTHESIS, CLOSE_PARENTHESIS }; يمكننا تقطيع هذه السلسلة النصّية: const auto input = "42/2 + -8\t=\n(2 + 2) * 2 * 2 -3"s‎ ‎ باستخدام مكرّر تعبيرات نمطية ‎regex_iterator‎ على النحو التالي: vector<TOKENS> tokens; const regex re { "\\s*(\\(?)\\s*(-?\\s*\\d+)\\s*(\\)?)\\s*(?:(\\+)|(-)|(\\*)|(/)|(=))" }; for_each(sregex_iterator(cbegin(input), cend(input), re), sregex_iterator(), [& ](const auto &i) { if (i[1].length() > 0) { tokens.push_back(OPEN_PARENTHESIS); } tokens.push_back(i[2].str().front() == '-' ? NEGATIVE_NUMBER : NON_NEGATIVE_NUMBER); if (i[3].length() > 0) { tokens.push_back(CLOSE_PARENTHESIS); } auto it = next(cbegin(i), 4); for (int result = ADDITION; it != cend(i); ++result, ++it) { if (it->length() > 0 U) { tokens.push_back(static_cast<TOKENS> (result)); break; } } }); match_results<string::const_reverse_iterator > sm; if (regex_search(crbegin(input), crend(input), sm, regex { tokens.back() == SUBTRACTION ? "^\\s*\\d+\\s*-\\s*(-?)" : "^\\s*\\d+\\s*(-?)" })) { tokens.push_back(sm[1].length() == 0 ? NON_NEGATIVE_NUMBER : NEGATIVE_NUMBER); } هذا مثال حيّ. ينبغي أن يكون وسيط ‎regex‎ قيمة يسارية (L-value)، إذ أنّ القيم اليمينيّة لن تعمل. المراسي (Anchors) توفّر C++‎ أربع مراسي فقط: ‎^‎ - تمثّل بداية السلسلة النصّية ‎$‎ - تمثّل نهاية السلسلة النصّية ‎\b‎ - تمثّل محرف ‎\W‎ أو بداية أو نهاية السلسلة النصّية. ‎\B‎ - تمثّل محرف ‎\w‎. في المثال التالي سنحاول التقاط عددٍ مع إشارَتِه: auto input = "+1--12 * 123/+1234"s; smatch sm; if (regex_search(input, sm, regex { "(?:^|\\b\\W)([+-]?\\d+)" })) { do { cout << sm[1] << endl; input = sm.suffix().str(); } while (regex_search(input, sm, regex { "(?:^\\W|\\b\\W)([+-]?\\d+)" })); } هذا مثال حيّ لاحظ أنّ المرساة لا تستهلك أيّ محرف. مثال على استخدام regex_replace تأخذ هذه الشيفرة عدّة أنماط من الأقواس، وتعيد تنسيقها إلى نمط K&R أو كيرنيجان وريتشي (إشارة إلى أسلوب الأقواس المستخدم في نواة يونكس الأولى). const auto input = "if (KnR)\n\tfoo();\nif (spaces) {\n foo();\n}\nif (allman)\n{\n\tfoo();\n}\nif (horstmann)\n{\tfoo();\n}\nif (pico)\n{\tfoo(); }\nif (whitesmiths)\n\t{\n\tfoo();\n\t}\n"s; cout << input << regex_replace(input, regex("(.+?)\\s*\\{?\\s*(.+?;)\\s*\\}?\\s*"), "$1{\n\t$2\n}\n") << endl; مثال حيّ. مثال على استخدام regex_token_iterator ‎std::regex_token_iterator‎ هي أداة مفيدة للغاية لاستخراج عناصر من ملف يحتوي قيمًا مفصولة بفواصل، كما أنّها قادرة أيضًا على التقاط الفواصل، على خلاف الطرق الأخرى التي تجد صعوبة في ذلك: const auto input = "please split,this,csv, ,line,\\,\n"s; const regex re{ "((?:[^\\\\,]|\\\\.)+)(?:,|$)" }; const vector<string> m_vecFields{ sregex_token_iterator(cbegin(input), cend(input), re, 1), sregex_token_iterator() }; cout << input << endl; copy(cbegin(m_vecFields), cend(m_vecFields), ostream_iterator<string>(cout, "\n")); مثال حي. ينبغي أن يكون الوسيط ‎regex‎ قيمة يسارية (L-value)، فالقِيم اليمينيّة (R-value) لن تعمل. المحدِّدات الكمية لنفترض أنّ لدينا سلسلة نصية ثابتة (‎const string input‎) تحتوي رقم هاتف وعلينا أن نتحقّق من صحّته. يمكن أن نبدأ بطلب مدخلات رقمية مع أيّ عدد من المحدِّدات الكمية ‎regex_match(input, regex("\\d*"))‎، أو مع محدِّد كمي ‎regex_match(input, regex("\\d+"))‎ واحد أو أكثر، بيْد أنّ كليهما سيفشلان إذا كانت المدخلات ‎input‎ تحتوي على سلسلة نصّية رقمية غير صالحة مثل: "123". سنستخدم n أو أكثر من المحدِّدات الكمية للتحقّق من أنّنا حصلنا على 7 أرقام على الأقل: regex_match(input, regex("\\d{7,}")) سيضمن هذا أنّنا سنحصل على العدد الصحيح من أرقام الهاتف، لكن يمكن أن تحتوي ‎input‎ أيضًا على سلسلة رقمية أطول ممّا ينبغي مثل: "123456789012"، لذلك فالحلّ هو استخدام محدِّد كمي بين n و m بحيث يكون عدد أحرف ‎input‎ محصورًا بين 7 و 11 رقمًا regex_match(input, regex("\\d{7,11}")); هذا أفضل، لكن ما تزال هنا مشكلة، إذ ينبغي أن ننتبه إلى السلاسل الرقمية غير القانونية التي تقع في النطاق [7، 11]، مثل: "123456789"، لذا دعنا نجعل رمز البلد (country code) اختياريًا عبر استخدام محدِّد كمي كسول(lazy quantifier): regex_match(input, regex("\\d?\\d{7,10}")) من المهمّ أن تعلم أنّ المحدِّد الكمي الكسول يحاول مطابقة أقل عدد ممكن من المحارف، وعليه فإنّّ الطريقة الوحيدة للمطابقة هي إذا كانت هناك فعليًا 10 محارف متطابقة مع ‎\d{7,10}‎. (لمطابقة الحرف الأول بطمع (greedy)، سيكون علينا استخدام: ‎\d{0,1}‎}.) يمكن ضمّ المحدِّد الكمي الكسول (lazy quantifier) إلى أيّ محدِّد كمي آخر. الآن، كيف يمكننا جعل رمز المنطقة اختياريًا، وعدم قبول رمز الدولة إلّا في حال كان رمز المنطقة موجودًا؟ regex_match(input, regex("(?:\\d{3,4})?\\d{7}")) تتطّلب ‎\d{7}‎ في هذا التعبير النمطي النهائي سبعة أرقام، ومسبوقة -اختياريًا- إما بثلاثة أو أربعة أرقام. لاحظ أنّنا لم نضم المحدِّد الكسول : ‎\d{3,4}?\d{7}‎، إذ أنّ ‎\d{3,4}?‎ يمكن أن يطابق إمّا 3 أو 4 محارف، بيْد أنّه يُفضّل الاكتفاء بـ 3 محارف، ( لهذا يُسمّونه كسولًا). بدلاً من ذلك، جعلنا المجموعة غير الملتقِطة (non-capturing group) لا تنجح في المُطابقة إلّا مرّة واحدة على الأكثر، مع تفضيل عدم التطابق. وهذا يتسبّب في منع التطابق إذا لم تتضمن ‎input‎ رمز المنطقة، كما في: "1234567". أود أن أشير في ختام موضوع المحدِّدات الكمية، إلى محدِّد آخر يمكنك استخدامه، وهو المحدِّد الكمي المُتملِّك (possessive quantifier). كلا المحدِّديْن سواءً المكمّم القنوع أو المكمّم المتملّك، يمكن ضمّهما إلى أيّ مكمّم آخر. وظيفة المكمّم المتملّك الوحيدة هي مساعدة محرّك التعبير النمطي عبر إخباره بأخذ ما أمكن من الأحرف المطابقة وعدم التخلي عنها حتى لو تسبّب ذلك في فشل التعبير النمطي. على سبيل المثال، التعبير التالي لا معنى له: regex_match(input, regex("\\d{3,4}+\\d{7}))‎‎ لأنّ مُدخلًا ‎input‎ مثل:" 1234567890 " لن يُطابَق بالتعبير ‎‎\d{3,4}+‎، وسيُطابق دائمًا بأربعة محارف، حتى لو كانت مطابقة 3 محارف كافية لإنجاح التعبير النمطي. يُستخدم المحدِّد المتملّك عادة عندما تحدّ الوحدة المحدَّدة كميًا عددَ المحارف القابلة للمطابقة. على سبيل المثال: regex_match(input, regex("(?:.*\\d{3,4}+){3}")) يمكن استخدامها إذا كانت ‎input‎ تحتوي أيًّا ممّا يلي: 123 456 7890 123-456-7890 (123)456-7890 (123) 456 - 7890 بيْد أنّ فائدة هذا التعبير النمطي تظهر عندما تتضمّن ‎input‎ مُدخلاً غير صالح، مثل: 12345 - 67890 بدون استخدام المحدِّد الكمي المتملّك، سيتعيّن على محرّك التعبير النمطي الرجوع واختبار كل توليفات ‎‎.*‎‎، سواء مع 3 أو 4 محارف للتحقّق ممّا إن كان يستطيع العثور على تركيبة مطابقة. سيبدأ التعبير النمطي، باستخدام المحدِّد المتملّك، من حيث توقّف المحدِّد المتملّك الثاني، أي المحرف "0"، ثمّ سيحاول محرّك التعبير النمطي ضبط ‎.*‎ للسماح بمطابقة ‎\d{3,4}‎؛ وفي حال تعذّر ذلك سيفشل التعبير النمطي، ولن يرجِع للخلف للتحقّق ممّا إذا كان من الممكن إنجاح المطابقة عبر إعادة ضبط ‎.*‎ في مرحلة أبكر. تقطيع سلسلة نصية Splitting a string هذا مثال توضيحيّ على كيفية تقسيم سلسلة نصّية: std::vector<std::string> split(const std::string &str, std::string regex) { std::regex r{ regex }; std::sregex_token_iterator start{ str.begin(), str.end(), r, -1 }, end; return std::vector<std::string>(start, end); } split("Some string\t with whitespace ", "\\s+"); // "Some", "string", "with", "whitespace" هذا الدرس جزء من سلسلة دروس عن C++‎. ترجمة -بتصرّف- للفصل Chapter 70: Regular expressions من كتاب C++ Notes for Professionals
  21. التكرار Iteration do تقدِّم تعليمة do حلقة do-while، انظر المثال التالي حيث نحصل على المحرف التالي غير الفارغ من مجرى الدخل القياسي: char read_char() { char c; do { c = getchar(); } while (isspace(c)); return c; } while تقدّم تعليمة while حلقة while. int i = 0; // اطبع عشر نجمات while (i < 10) { putchar('*'); i++; } حلقة for النطاقية range-based std::vector primes = {2, 3, 5, 7, 11, 13}; for (auto prime: primes) { std::cout << prime << std::endl; } for تقدّم تعليمة for حلقة for، يمكن استخدامها في C++‎ 11 والإصدارات الأحدث لتقديم حلقة for النطاقية (range-based for loop). // اطبع عشر نجمات for (int i = 0; i < 10; i++) { putchar('*'); } التكرار على تعداد ليس هناك حلٌّ مضمَّن مسبقًا للتكرار (iterate) على كائن تعداد (enumeration)، لكن هناك عدة طرق أخرى لذلك: بالنسبة للتعدادات (‎enum‎) ذات القيم المتتالية: enum E { Begin, E1 = Begin, E2, // .. En, End }; for (E e = E::Begin; e != E::End; ++e) { // e افعل شيئا ما بـ } الإصدار ≥ C++‎ 11 يجب تنفيذ العامل ‎operator ++‎ مع ‎enum class‎: E& operator ++ (E& e) if (e == E::End) { throw std::out_of_range("for E& operator ++ (E&)"); } e = E(static_cast < std::underlying_type < E > ::type > (e) + 1); return e; } استخدام حاوية كمتّجه (‎std::vector‎): enum E { E1 = 4, E2 = 8, // .. En }; std::vector<E> build_all_E() { const E all[] = {E1, E2, /*..*/ En}; return std::vector<E>(all, all + sizeof(all) / sizeof(E)); } std::vector < E > all_E = build_all_E(); ثمّ: for (std::vector<E>::const_iterator it = all_E.begin(); it != all_E.end(); ++it) { E e = *it; // e افعل شيئا ما بـ } الإصدار ≥ C++‎ 11 أو يمكن استخدام ‎std::initializer_list‎: enum E { E1 = 4, E2 = 8, // .. En }; constexpr std::initializer_list < E > all_E = { ثم بعد ذلك: for (auto e: all_E) { // e افعل شيئا ما بـ } التعدادات النطاقية (Scoped enums) قدّمت C++‎ 11 ما يعرف باسم التعداد النطاقي، وهي تعدادات يلزَم تأهيل أعضائها عبر ‎enumname::membername‎. ويُصرَّح عن التعدادات النطاقية باستخدام الصيغة ‎enum class‎، فمثلًا لتخزين ألوان قوس قزح، نكتب ما يلي: enum class rainbow { RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, VIOLET }; وللوصول إلى لون معين: rainbow r = rainbow::INDIGO; لا يمكن تحويل أصناف التعدادات (‎enum ‎class‎) ضمنيًا إلى أعداد صحيحة (‎int‎) بدون تحويل النوع (cast). لذلك فإنّ التعبير int x = rainbow::RED غير صالح. تتيح لك التعدادات النطاقية أيضًا تحديد النوع الأساسي (underlying type)، أي النوع المستخدم لتمثيل العضو. والذي يساوي افتراضيًا النوع ‎int‎. مثلًا، في لعبة تيك تاك تو (Tic-Tac-Toe)، يمكنك تخزين القطع كما يلي: enum class piece: char { EMPTY = '\0', X = 'X', O = 'O', }; لعلك انتبهت إلى أن التعداد ‎enum‎ يمكن أن يحتوي فاصلة زائدة بعد العضو الأخير. التصريح المسبق عن التعدادات في C++‎ 11 يمكن التصريح المسبق (forward declaration) عن التعدادات النطاقية على النحو التالي: ... enum class Status; // تصريح مسبق Status doWork(); // استخدام التصريح المسبق ... enum class Status { Invalid, Success, Fail }; Status doWork() // يلزم تصريح كامل لأجل التقديم { return Status::Success; } أمّا التعدادات غير النطاقية، فيمكن التصريح عنها مُسبقًا عبر: ... enum Status: int; // تصريح مسبق، يلزم نوع صريح Status doWork(); // استخدام التصريح المسبق ... enum Status: int { Invalid = 0, Success, Fail }; // يلزم مطابقة نوع التصريح المسبق static_assert(Success == 1); انظر إن شئت مثال "تاجر الفاكهة الأعمى" الأكثر تعقيدًا. التصريح بالتعداد Enumeration Declaration تسمح التعدادات القياسية للمستخدمين بالتصريح عن اسم يمثّل مجموعة من الأعداد الصحيحة، وتسمى هذه الأسماء مجتمعةً عدّادات (enumerators)، وتُعرّف التِّعدادات والعَدّادات المرتبطة بها على النحو التالي: enum myEnum { enumName1, enumName2, }; يعدّ التعداد نوعًا متميزًا عن جميع الأنواع الأخرى، ويكون اسم هذا النوع في المثال أعلاه هو ‎myEnum‎، ويُتوقّع أن تُخمِّن كائنات هذا النوع قيمة العدّاد داخل التعداد. العدّادات المُصرّح عنها ضمن التعداد تكون قيمًا ثابتة من نوع ذلك التعداد، ورغم أنّ العدّادات تُعلَن ضمن النوع، إلا أنّ عامل النطاق - scope operator‏‏ - ‎::‎ ليس ضروريًا للوصول إلى الاسم. لذلك، فاسم العدّاد الأول سيكون ‎enumName1‎. الإصدار ≥ C++‎ 11 يمكن استخدام عامل النطاق -اختياري- للوصول إلى عدّاد داخل التعداد. لذلك، يمكن كتابة ‎enumName1‎ على النحو التالي ‎myEnum::enumName1‎. وتُسند إلى العدّادات قيم عددية صحيحة تبدأ من 0 وتزداد بـ 1 لكل عدّاد إضافي في التعداد. لذلك في المثال أعلاه، فإنّ قيمة ‎enumName1‎ تساوي 0، وقيمة ‎enumName2‎ تساوي 1. كذلك يمكن إسناد قيمة معيّنة إلى العدّادات من قبل المستخدم؛ وعندئذ يجب أن تكون تلك القيمة تعبيرًا عدديًا صحيحًا ثابتًا. أيضًا، سيُسند إلى العدّادات التي لم تُوفّر قيمها صراحة "قيمة العداد السابق + 1". enum myEnum { enumName1 = 1, // 1 القيمة ستكون enumName2 = 2, // 2 القيمة ستكون enumName3, // القيمة ستكون 5، أي القيمة السابقة + 1 enumName4 = 7, // 7 القيمة ستكون enumName5, // 8 القيمة ستكون enumName6 = 5, // القيمة ستكون 5، يجوز العودة إلى الوراء enumName7 = 3, // القيمة ستكون 3، يجوز إعادة استخدام الأعداد enumName8 = enumName4 + 2, // القيمة ستكون 9، يجوز أخذ العدادات السابقة وتعديلها }; التعدادات في تعليمات switch يشيع استخدام العدادات مع عبارات switch، ومن ثمّ فهي تظهر عادة في أجهزة الحالة (state machines). من الميزات المفيدة التي تقدّمها التعدادات في عبارات switch هو أنّه في حال لم تُضمَّن أيّ تعليمة افتراضية في switch، ولم تُستخدم جميع قيم التعداد، فسيُطلِق المصرّف تحذيرًا. enum State { start, middle, end }; ... switch (myState) { case start: ... case middle: ... } // warning: enumeration value 'end' not handled in switch [-Wswitch] هذا الدرس جزء من سلسلة دروس عن C++‎. ترجمة -وبتصرّف- للفصل Chapter 68: Enumeration من الكتاب C++ Notes for Professionals
  22. الترتيب وحاويات التسلسلات std::sort هي خوارزمية لتَرتيب مجموعة من القي، وتوجد في ترويسة المكتبة القياسية ‎algorithm‎، وهي مُعرّفة بواسطة زوج من المُكرّرات. وتأخذ ‎std::sort‎ كائنًا داليًّا كمعامل أخير للموازنة بين قيمتين، ثم تحدد الترتيب بناءً على ذلك. لاحظ أنّ ‎std::sort‎ ليست مستقرة. يجب أن تفرض دالّة الموازنة ترتيبًا صارمًا وضعيفًا على العناصر. وعمومًا فإن عامل الموازنة => و =< كافيان. وتُستخدم خوارزمية ‎std::sort‎ لترتيب حاوية ذات مُكرّر وصول عشوائي (random-access iterators): الإصدار ≥ C++‎ 11 #include <vector> #include <algorithm> std::vector<int> MyVector = {3, 1, 2} // < المقارنة الافتراضية لــ std::sort(MyVector.begin(), MyVector.end()); تتطلّب std::sort أن تكون المكرِّرات عشوائية الوصول، ولا توفّر الحاويَتان std::list و std::forward_list (منذ C++‎ 11) مُكرِّرات وصول عشوائي، لذا لا يمكن استخدامهما مع std::sort. لكن لديهما دالة ‎sort‎ التابعة، والتي تقدّم خوارزميةَ ترتيبٍ تعمل مع أنواع المكرّرات الخاصّة بها. الإصدار ≥ C++‎ 11 #include <list> #include <algorithm> std::list<int> MyList = {3, 1, 2} // < المقارنة الافتراضية لــ // ترتيب القائمة كاملة MyList.sort(); ترتب الدالة ‎sort‎ القائمة بأكملها دائمًا، لذا لا يمكنها ترتيب مجموعة فرعية (sub-range) من العناصر، لكن بأي حال بما أن لكلٍّ من القوائم (‎list‎) والقوائم الأمامية ‎forward_list‎ عمليات ربط سريعة (fast splicing operations)، فتستطيع استخراج العناصر التي تريد ترتيبها من القائمة ثم ترتِّبها، ثم تعيدها حيث كانت على النحو التالي: void sort_sublist(std::list < int > & mylist, std::list < int > ::const_iterator start, std::list < int > ::const_iterator end) { // استخراج وترتيب المجال الفرعي المحدد std::list < int > tmp; tmp.splice(tmp.begin(), list, start, end); tmp.sort(); // إعادة المجال حيث كان list.splice(end, tmp); } الترتيب باستخدام std::map تصاعديًا وتنازليًا يرتّب هذا المثال عناصر قاموس تصاعديًا بحسب المفتاح، وتستطيع استخدام أي نوع بما في ذلك الصنف بدلًا من std::string، انظر المثال التالي: #include <iostream> #include <utility> #include <map> int main() { std::map < double, std::string > sorted_map; // ترتيب أسماء الكواكب بحسب حجمها sorted_map.insert(std::make_pair(0.3829, "عطارد")); sorted_map.insert(std::make_pair(0.9499, "الزهرة")); sorted_map.insert(std::make_pair(1, "الأرض")); sorted_map.insert(std::make_pair(0.532, "المريخ")); sorted_map.insert(std::make_pair(10.97, "المشتري")); sorted_map.insert(std::make_pair(9.14, "زحل")); sorted_map.insert(std::make_pair(3.981, "أورانوس")); sorted_map.insert(std::make_pair(3.865, "نبتون")); for (auto const & entry: sorted_map) { std::cout << entry.second << " (" << entry.first << " من قطر الأرض)" << '\n'; } } الناتج سيكون: (عطارد (0.3829 من قطر الأرض (المريخ (0.532 من قطر الأرض (الزهرة (0.9499 من قطر الأرض (الأرض (1 من قطر الأرض (نبتون (3.865 من قطر الأرض (أورانوس (3.981 من قطر الأرض (زحل (9.14 من قطر الأرض (المشتري (10.97من قطر الأرض إذا كانت هناك مُدخلات ذات مفاتيح متساوية، فاستخدم القاموس المتعدّد ‎multimap‎ بدلًا من القاموس ‎map‎ (كما في المثال أدناه). لترتيب العناصر تنازليا، أعلِن عن القاموس باستخدام عامل موازنة مناسب (‎std::greater<>‎): #include <iostream> #include <utility> #include <map> int main() { std::multimap < int, std::string, std::greater < int >> sorted_map; // ترتيب أسماء الحيوانات تنازليا بحسب عدد الأرجل sorted_map.insert(std::make_pair(6, "حشرة")); sorted_map.insert(std::make_pair(4, "قطة")); sorted_map.insert(std::make_pair(100, "الحريشية")); sorted_map.insert(std::make_pair(2, "دجاجة")); sorted_map.insert(std::make_pair(0, "سمكة")); sorted_map.insert(std::make_pair(4, "فرس")); sorted_map.insert(std::make_pair(8, "عنكبوت")); for (auto const & entry: sorted_map) { std::cout << entry.second << " لها " << entry.first << " أرجل" << '\n'; } } الناتج سيكون: الحريشية لها 100 أرجل العنكبوت لها 8 أرجل الحشرة لها 6 أرجل القطة لها 4 أرجل الفرس لها 4 أرجل الدجاجة لها 2 أرجل السمكة لها 0 أرجل ترتيب حاويات التسلسل باستخدام عامل "أصغر من" سيرتِّب ‎std::sort‎ العناصر، إذا لم تُمرَّر أيّ دالة ترتيب، من خلال استدعاء العامل ‎operator<‎ على أزواج من العناصر، ويجب أن تُعيد نوعًا يمكن تحويله إلى قيمة بوليانية ‎bool‎، وتحتوي الأنواع الأساسية (مثل الأعداد الصحيحة، والعشرية، والمؤشرات…) على عوامل موازنة. نستطيع زيادة تحميل هذا العامل لجعل استدعاء ‎sort‎ الافتراضي يعمل على الأنواع المُعرّفة من قبل المستخدم. انظر: // تضمين حاويات التسلسل #include <vector> #include <deque> #include <list> // تضمن خوارزمية الترتيب #include <algorithm> class Base { public: // v على القيمة variable منشئ يضبط Base(int v): variable(v) {} وهنا استخدم variable لتتيح عامل الترتيب الكامل "أصغر من" أو less، وستمثل this الجانب الأيسر من الموازنة دومًا، نتابع: bool operator < (const Base & b) const { return this -> variable < b.variable; } int variable; }; int main() { std::vector < Base > vector; std::deque < Base > deque; std::list < Base > list; أنشئ هنا عنصرين لترتيبهما: Base a(10); Base b(5); والآن أدرجهما في نهاية الحاوية: vector.push_back(a); vector.push_back(b); deque.push_back(a); deque.push_back(b); list.push_back(a); list.push_back(b); والآن، رتب البيانات باستخدام دالة (operator<(const Base &b: std::sort(vector.begin(), vector.end()); std::sort(deque.begin(), deque.end()); // بشكل مختلف List ينبغي ترتيب القائمة list.sort(); return 0; } ترتيب التسلسلات باستخدام دوال الموازنة إليك المثال التوضيحي التالي: // تضمين التسلسلات #include <vector> #include <deque> #include <list> // أدخل خوارزمية الترتيب #include <algorithm> class Base { public: // v على القيمة variable منشئ يضبط Base(int v): variable(v) {} int variable; }; bool compare(const Base & a, const Base & b) { return a.variable < b.variable; } int main() { std::vector < Base > vector; std::deque < Base > deque; std::list < Base > list; أنشئ عنصرين لترتيبهما: Base a(10); Base b(5); أدرج عنصرين في نهاية الحاوية: vector.push_back(a); vector.push_back(b); deque.push_back(a); deque.push_back(b); list.push_back(a); list.push_back(b); رتب البيانات باستخدام دالة الموازنة: std::sort(vector.begin(), vector.end(), compare); std::sort(deque.begin(), deque.end(), compare); list.sort(compare); return 0; } ترتيب التسلسلات باستخدام تعابير لامدا lambda (‏‏C++‎ 11) انظر المثال التوضيحي التالي: الإصدار ≥ C++‎ 11 // تضمين التسلسلات #include <vector> #include <deque> #include <list> #include <array> #include <forward_list> // تضمين خوارزمية الترتيب #include <algorithm> class Base { public: // v على القيمة variable منشئ يضبط Base(int v): variable(v) {} int variable; }; int main() { // أنشئ عنصرين لترتيبهما Base a(10); Base b(5); // لذا سنستخدم قائمة تهييئ لإدراج العناصر C++11 نحن نستخدم std::vector <Base> vector = {a, b}; std::deque <Base> deque = {a, b}; std::list <Base> list = {a, b}; std::array <Base, 2> array = {a, b}; std::forward_list<Base> flist = {a, b}; نستطيع ترتيب البيانات باستخدام تعبير لامدا ضمني (inline). std::sort(std::begin(vector), std::end(vector), [](const Base & a, const Base & b) { return a.variable < b.variable; }); كذلك يمكن أن نمرر كائن لامدا كموازن ونعيد استخدامه أكثر من مرة، انظر: auto compare = [](const Base & a, const Base & b) { return a.variable < b.variable; }; std::sort(std::begin(deque), std::end(deque), compare); std::sort(std::begin(array), std::end(array), compare); list.sort(compare); flist.sort(compare); return 0; } ترتيب المصفوفات المبنية مسبقًا ترتّب خوارزمية ‎sort‎ التسلسلات المُعرّفة بواسطة المُكرِّرات، وهذا كاف لترتيب مصفوفة ما مبنية مسبًا (تُعرَف أيضًا باسم مصفوفات من نمط C). الإصدار ≥ C++‎ 11 int arr1[] = {36, 24, 42, 60, 59}; // ترتيب الأعداد تصاعديا sort(std::begin(arr1), std::end(arr1)); // ترتيب الأعداد تنازليا sort(std::begin(arr1), std::end(arr1), std::greater < int > ()); قبل الإصدار C++‎ 11، كان من اللازم حساب نهاية المصفوفة باستخدام حجمها: الإصدار < C++‎ 11 // استخدام قيمة مباشرة sort(arr1, arr1 + 5); // استخدام تعبير كحل بديل const size_t arr1_size = sizeof(arr1) / sizeof( * arr1); sort(arr1, arr1 + arr1_size); ترتيب تسلسل باستخدام ترتيب مخصّص إن كانت للقيم الموجودة في الحاوية عوامل مُحمّلة تحميلًا زائدًا، فنستخدم ‎std::sort‎ مع كائن دالّي مُخصّص لترتيب عناصر الحاوية تصاعديًا أو تنازليًا: الإصدار ≥ C++‎ 11 #include <vector> #include <algorithm> #include <functional> std::vector v = {5,1,2,4,3}; // (1,2,3,4,5) الترتيب التصاعدي std::sort(v.begin(), v.end(), std::less < int > ()); // أو std::sort(v.begin(), v.end()); // (5,4,3,2,1) الترتيب التنازلي std::sort(v.begin(), v.end(), std::greater < int > ()); // أو std::sort(v.rbegin(), v.rend()); الإصدار ≥ C++‎ 14 في الإصدار C++‎ 14، ليس علينا تمرير وسيط القالب لدوالّ الموازنة، إذ يمكن استنتاج ذلك استنادًا إلى ما تم تمريره: std::sort(v.begin(), v.end(), std::less<>()); // ترتيب تصاعدي std::sort(v.begin(), v.end(), std::greater<>()); // ترتيب تنازلي هذا الدرس جزء من سلسلة دروس عن C++‎. ترجمة -بتصرّف- للفصل Chapter 67: Sorting من كتاب C++ Notes for Professionals
  23. مولد قيم عشوائية حقيقيّة لإنشاء قيم عشوائية حقّا (generate true random) يمكن استخدامها في التشفير، يجب استخدام ‎std::random_device‎ كمُولِّد. #include <iostream> #include <random> int main() { std::random_device crypto_random_generator; std::uniform_int_distribution < int > int_distribution(0, 9); int actual_distribution[10] = {0,0,0,0,0,0,0,0,0,0}; for (int i = 0; i < 10000; i++) { int result = int_distribution(crypto_random_generator); actual_distribution[result]++; } for (int i = 0; i < 10; i++) { std::cout << actual_distribution[i] << " "; } return 0; } تُستخدم std::random_device بنفس طريقة استخدام مولّد القيم العشوائية الزائفة (pseudo random value). ورغم ذلك فيمكن تنفيذ ‎std::random_device‎ انطلاقًا من محرّك أعداد عشوائية زائفة تُحدد وفق التنفيذ في حال لم يتوفّر مصدر غير حتمي (non-deterministic) مثل جهاز خاص بتوليد القيم العشوائية لاستخدامه في التنفيذ. يمكن الكشف عن مثل هذه التنفيذات من خلال الدالة التابعة ‎entropy‎ (التي تعيد 0 في حال كان المولد حتميًّا تمامًا)، لكنّ العديد من المكتبات الشائعة (مثل libstdc++‎ و LLVM's libc++‎) تعيد دائمًا القيمة 0، حتى عند استخدام مولّدات عشوائية خارجية عالية الجودة . توليد عدد عشوائي زائف تنشئ مولّدات الأعداد شبه العشوائية قيمًا يمكن تخمينها استنادًا إلى القيم التي تم توليدها سابقًا، هذا يعني أنها حتمية. ولا تستخدم مولّدات الأعداد شبه العشوائية في الحالات التي تستلزم أعدادًا عشوائية حقًّا. #include <iostream> #include <random> int main() { std::default_random_engine pseudo_random_generator; std::uniform_int_distribution < int > int_distribution(0, 9); int actual_distribution[10] = {0,0,0,0,0,0,0,0,0,0}; for (int i = 0; i < 10000; i++) { int result = int_distribution(pseudo_random_generator); actual_distribution[result]++; } for (int i = 0; i <= 9; i++) { std::cout << actual_distribution[i] << " "; } return 0; } تنشئ هذه الشيفرة مولّدَ أعدادٍ عشوائية زائفة، وتوزيعًا يولّد أعدادًا صحيحة في نطاق [0،9] باحتمال متساوي، ويحسب المولّد بعد ذلك عدد مرّات إنشاء كل نتيجة. يحدّد معامل القالب ‎std::uniform_int_distribution<T>‎ نوع العدد الصحيح الذي يجب إنشاؤه. استخدم ‎std::uniform_real_distribution<T>‎ لتوليد الأعداد العشرية (floats) والأعداد العشرية المزدوجة (doubles). استخدام المولد مع عدة توزيعات من الممكن استخدام مولّد الأعداد العشوائية مع عدة توزيعات، ويجب ذلك حقيقة. #include <iostream> #include <random> int main() { std::default_random_engine pseudo_random_generator; std::uniform_int_distribution < int > int_distribution(0, 9); std::uniform_real_distribution < float > float_distribution(0.0, 1.0); std::discrete_distribution rigged_dice({1,1,1,1,1,100}); std::cout << int_distribution(pseudo_random_generator) << std::endl; std::cout << float_distribution(pseudo_random_generator) << std::endl; std::cout << (rigged_dice(pseudo_random_generator) + 1) << std::endl; return 0; } عرّفنا في هذا المثال مولِّدًا واحدًا فقط، ثمّ استخدمناه لإنشاء أعداد عشوائية في ثلاثة توزيعات مختلفة، وسيولّد التوزيع ‎rigged_dice‎ قيمة بين 0 و 5، لكن سيولّد في الأغلبية الساحقة من الحالات ‎5‎، لأنّ احتمال إنشاء ‎5‎ يساوي ‎100 / 105‎. هذا الدرس جزء من سلسلة دروس عن C++‎. ترجمة -بتصرّف- للفصل Chapter 65: Random number generation من كتاب C++ Notes for Professionals
  24. std::next_permutation المثال التالي يبدل تسلسل المجال [first, last] ويحوّله إلى التبديل التالي الأعلى في الترتيب المعجمي (lexicographically higher permutation)، ويمكن تخصيص قاعدة التبديل عبر ‎cmpFun‎. template < class Iterator > bool next_permutation(Iterator first, Iterator last); template < class Iterator, class Compare > bool next_permutation(Iterator first, Iterator last, Compare cmpFun); المعاملات ‎first‎ - بداية المجال المُراد تبديله (مُضمّن) ‎last‎ - نهاية المجال المراد تبديله (غير مُضمّن) القيمة المعادة تعيد true إن كان التبديل موجودًا، وخلاف ذلك، يُحوّل المجال إلى أصغر تبديل معجمية (lexicographically smallest permutation)، ثم تُعاد القيمة false. التعقيد Complexity التعقيد يساوي O(n)‎، حيث تمثّل n المسافة من ‎first‎ إلى ‎last‎. إليك المثال التالي: std::vector< int > v { 1, 2, 3 }; do { for (int i = 0; i < v.size(); i += 1) { std::cout << v[i]; } std::cout << std::endl; } while (std::next_permutation(v.begin(), v.end())); هذا يطبع جميع تقليبات المجال 1،2،3 وفق ترتيب معجمي تصاعدي. الخرج: 123 132 213 231 312 321 std::for_each في الشيفرة أدناه، نطبّق الدالّة ‎f‎ على نتيجة تحصيل كلّ مُكرّر في المجال ‎[first, last)‎ بدءًا من ‎first‎ وحتى ‎last - 1‎. template < class InputIterator, class Function > Function for_each(InputIterator first, InputIterator last, Function f); المعاملات ‎first, last‎ - المجال الذي ستُطبّق ‎f‎ عليه. ‎f‎ - كائن قابل للاستدعاء يُطبّق على نتيجة تحصيل كل مكرّر في المجال [‎first, last‎). القيمة المُعادة تعاد ‎f‎ إن كان الإصدار أقدم من C++‎ 11، وإلّا فستُعاد std::move(f)‎. التعقيد تُطبّق ‎f‎ عدد ‎last - first‎ مرّة. مثال الإصدار ≥ C++‎ 11 std::vector<int> v { 1, 2, 4, 8, 16 }; std::for_each(v.begin(), v.end(), [](int elem) { std::cout << elem << " "; }); تطبيق الدالّة المُعطاة على كل عنصر من المتجه ‎v‎ يؤدي إلى طباعة هذا العنصر في مجرى الخرج ‎stdout‎. std::accumulate هذه الخوارزمية مُعرّّفة في الترويسة template < class InputIterator, class T > T accumulate(InputIterator first, InputIterator last, T init); // (1) template < class InputIterator, class T, class BinaryOperation > T accumulate(InputIterator first, InputIterator last, T init, BinaryOperation f); // (2) تُجرى std::accumulate عملية الطي (fold operation) باستخدام الدالّة ‎f‎ على المجال ‎[first, last)‎ بدءا من ‎init‎ كقيمة تراكمية (accumulator value)، وهذا يكافئ: T acc = init; for (auto it = first; first != last; ++it) acc = f(acc, * it); return acc; في الإصدار (1)، يُستخدم ‎operator+‎ بدلًا من ‎f‎، لذا فإنّ مراكمة قيمِ حاويةٍ يعادل مجموع عناصر تلك الحاوية. المعاملات ‎first, last‎ - المجال الذي ستُطبّق ‎f‎ عليه. ‎init‎ - القيمة الأولية للتراكم. ‎f‎ - دالّة الطي الثنائية (binary folding function). القيمة المُعادة القيمة المتراكمة الناتجة عن التطبيق المتتابع للدالّة ‎f‎. التعقيد التعقيد يساوي O(n × k)‎، و n هنا يمثّل المسافة من ‎first‎ إلى ‎last‎، فيما تمثّل O(k)‎ تعقيد الدالّة ‎f‎. انظر المثال البسيط التالي عن مُراكمة الجَمْع: std::vector<int> v { 2, 3, 4 }; auto sum = std::accumulate(v.begin(), v.end(), 1); std::cout << sum << std::endl; الناتج سيكون: 10 تحويل الأرقام (digits) إلى عدد (number): الإصدار<C++‎ 11 class Converter { public: int operator()(int a, int d) const { return a * 10 + d; } }; ثمّ: const int ds[3] = {1, 2, 3}; int n = std::accumulate(ds, ds + 3, 0, Converter()); std::cout << n << std::endl; الإصدار ≥ C++‎ 11 const std::vector<int> ds = {1, 2, 3}; int n = std::accumulate(ds.begin(), ds.end(), 0, [](int a, int d) { return a * 10 + d; }); std::cout << n << std::endl; الناتج سيكون: 123 std::find تساعد std::find على العثور على أوّل ظهور لعنصر val داخل مجال ‎[first, last)‎‎‎. template < class InputIterator, class T > InputIterator find (InputIterator first, InputIterator last, const T& val); المعاملات ‎first‎ : يمثل مُكرّرًا يشير إلى بداية المجال. ‎last‎ يمثل مُكرّرًا يشير إلى نهاية المجال. ‎val‎ يمثل القيمة المبحوث عنها داخل المجال. القيمة المعادة يُعاد مكرِّر يشير إلى أوّل عنصر في المجال يساوي (==) val، أما إن لم يكن هناك عنصر يحقّق ذلك، فسيُعاد مكرّر يشير إلى last. انظر المثال التالي: #include <vector> #include <algorithm> #include <iostream> using namespace std; int main(int argc, const char * argv[]) { سننشئ متجهًا هنا: vector<int> intVec {4, 6, 8, 9, 10, 30, 55,100, 45, 2, 4, 7, 9, 43, 48}; ثم نعرِّف المكرِّرات: vector < int > ::iterator itr_9; vector < int > ::iterator itr_43; vector < int > ::iterator itr_50; والآن نستدعي find: itr_9 = find(intVec.begin(), intVec.end(), 9); //occurs twice itr_43 = find(intVec.begin(), intVec.end(), 43); //occurs once // قيمة غير موجودة في المتجهة itr_50 = find(intVec.begin(), intVec.end(), 50); //does not occur cout << "first occurrence of: " << * itr_9 << endl; cout << "only occurrence of: " << * itr_43 << endl; نستطيع إثبات أن itr_9 تشير إلى الظهور الأول لـ 9 عبر فحص القيمة التي تلي 9، والتي يجب أن تكون 10 وليس 43، نتابع: cout << "element after first 9: " << * (itr_9 + 1) << ends; وسنلقي نظرة على العنصر الموجود قبل النهاية لتجنب تحصيل ()intVec.end: cout << "last element: " << * (itr_50 - 1) << endl; return 0; } يكون ناتج ذلك كله: first occurrence of: 9 only occurrence of: 43 element after first 9: 10 last element: 48 std::min_element تُستخدم std::min_element لإيجاد أصغر عنصر في مجال معيّن. template < class ForwardIterator > ForwardIterator min_element(ForwardIterator first, ForwardIterator last); template < class ForwardIterator, class Compare > ForwardIterator min_element(ForwardIterator first, ForwardIterator last, Compare comp); المعاملات ‎first‎ - مكرّر يشير إلى بداية المجال ‎last‎ - مكرّر يشير إلى نهاية المجال ‎comp‎ - مؤشّر دالّة أو كائن دالّة يأخذ وسيطين ويعيد إما true أو false موضحًا إذا كان الوسيط الأول أصغر من الوسيط الثاني. يُشتَرَط ألًا تعدّل هذه الدالّة المُدخلاتِ. القيمة المعادة يُعاد مكرّر إلى أصغر عنصر في المجال. التعقيد التعقيد يساوي O(1)‎ - n‎، حيث يمثل n عدد العناصر المُقارنَة. انظر المثال التالي: #include <iostream> #include <algorithm> #include <vector> #include <utility> // make_pair لاستخدام using namespace std; // دالّة تقارن زوجين bool pairLessThanFunction(const pair < string, int > & p1, const pair < string, int > & p2) { return p1.second < p2.second; } int main(int argc, const char * argv[]) { vector<int> intVec {30,200,167,56,75,94,10,73,52,6,39,43}; vector> pairVector = {make_pair("y", 25), make_pair("b", 2), make_pair("z", 26), make_pair("e", 5) }; // < العامل الافتراضي هو auto minInt = min_element(intVec.begin(), intVec.end()); // pairLessThanFunction استخدام auto minPairFunction = min_element(pairVector.begin(), pairVector.end(), pairLessThanFunction); // intVector اطبع أصغر قيمة في cout << "min int from default: " << * minInt << endl; // pairVector اطبع أصغر قيمة في cout << "min pair from PairLessThanFunction: " << ( * minPairFunction).second << endl; return 0; } الناتج سيكون: min int from default: 6 min pair from PairLessThanFunction: 2 std::find_if تُستخدم std::find_if لإيجاد أوّل عنصر في المجال يحقّق دالّة شرطية ‎pred‎. template < class InputIterator, class UnaryPredicate > InputIterator find_if(InputIterator first, InputIterator last, UnaryPredicate pred); المُعاملات ‎first‎ => مكرّر يشير إلى بداية المجال ‎last‎ => مكرّر يشير إلى نهاية المجال ‎pred‎ => دالّة شرطية - predicate - (تعيد إمّا true أو false) القيمة المعادة يُعاد مكرّر يشير إلى أول عنصر في المجال يحقّق الدالّة الشرطية، وسيُعاد مكرّر إلى last في حال لم يكن هناك أيّ عنصر يحقّق ذلك. انظر المثال التالي: #include <iostream> #include <vector> #include <algorithm> using namespace std; /* عرِّف بعض الدوالّ الشرطية */ // يساوي 10 x إن كان true إعادة bool multOf10(int x) { return x % 10 == 0; } // إن كان العنصر أكبر من القيمة الممرّرة true إعادة class Greater { int _than; public: Greater(int th): _than(th) { } bool operator()(int data) const { return data > _than; } }; int main() { vector myvec {2, 5, 6, 10, 56, 7, 48, 89, 850, 7, 456}; // lambda مع دالّة vector < int > ::iterator gt10 = find_if(myvec.begin(), myvec.end(), [](int x) { return x > 10; }); // >= // مع مؤشّر دالّة vector < int > ::iterator pow10 = find_if(myvec.begin(), myvec.end(), multOf10); // مع كائن دالّي vector < int > ::iterator gt5 = find_if(myvec.begin(), myvec.end(), Greater(5)); // غير موجود vector < int > ::iterator nf = find_if(myvec.begin(), myvec.end(), Greater(1000)); // nf points to myvec.end(); // myvec.end() التحقّق مما إذا كان المؤشّر يشير إلى if (nf != myvec.end()) { cout << "nf points to: " << * nf << endl; } else { cout << "item not found" << endl; } cout << "First item > 10: " << * gt10 << endl; cout << "First Item n * 10: " << * pow10 << endl; cout << "First Item > 5: " << * gt5 << endl; return 0; } الناتج سيكون: item not found First item > 10: 56 First Item n * 10: 10 First Item > 5: 6 استخدام std::nth_element لإيجاد الوسيط تأخذ خوارزمية ‎std::nth_element‎ ثلاثة مُكرِّرات: مكرّر يشير إلى البداية، وآخر يشير إلى الموضع n، وآخر يشير إلى النهاية. وبمجرد عودة الدالّة، سيكون العنصر رقم n (في الترتيب وليس في الموضع) هو أصغر n عنصر. تحتوي الدالّة على تحميلات زائدة أكثر تفصيلًا، على سبيل المثال، بعضها تأخذ كائنات دالّية للموازنة. ملاحظة: هذه الدالّة فعّالة للغاية - إذ أنّ تعقيدها خطّي. في المثال التالي، سنُعرّف الوسيط median، وهو القيمة التي تفصل النصف الأعلى من التسلسل عن النصف الأصغر بحيث يتساوى على طرفيه عدد القيم بعد ترتيبها تصاعديًا، أي أنه العنصر الأوسط في الترتيب. للتّبسيط، سنُعرّف وسيط تسلسل مكوّن من n عنصر على أنّه العنصر الموجود في الموضع ⌈ n / 2 ⌉ حسب الترتيب. مثلًا، سيكون وسيط تسلسل طوله 5 هو العنصر الأصغر الثالث، وكذلك وسيط تسلسل بطول 6 عناصر. لاستخدام هذه الدالّة لإيجاد الوسيط، يمكننا استخدام ما يلي، لنبدأ بــ: std::vector v{5, 1, 2, 3, 4}; std::vector < int > ::iterator b = v.begin(); std::vector < int > ::iterator e = v.end(); std::vector < int > ::iterator med = b; std::advance(med, v.size() / 2); // الوسيط موجود في الموضع الثاني std::nth_element(b, med, e); // v[2] الوسيط الآن هو ولإيجاد نقطة تجزئة من الدرجة p ‏‏(p-th quantile‏‏)، سنغيّر بعض السطور أعلاه: const std::size_t pos = p * std::distance(b, e); std::advance(nth, pos); ستكون نقطة التجزئة في الموضع ‎pos‎. std::count تحسُب std::count عدد العناصر التي تساوي قيمةً معيّنة val: template < class InputIterator, class T > typename iterator_traits < InputIterator > ::difference_type count(InputIterator first, InputIterator last, const T & val); المعاملات ‎first‎ - مكرّر يشير إلى بداية المجال. *‎last‎ => مكرّر يشير إلى نهاية المجال. ‎val‎ - القيمة المراد حساب تكرار حدوثها في المجال. القيمة المعادة عدد العناصر التي تساوي (==) val في المجال. انظر المثال التالي: #include <vector> #include <algorithm> #include <iostream> using namespace std; int main(int argc, const char * argv[]) { // إنشاء متجهة vector intVec{4,6,8,9,10,30,55,100,45,2,4,7,9,43,48}; //9, 55, 101 حساب مرات حدوث size_t count_9 = count(intVec.begin(), intVec.end(), 9); //occurs twice size_t count_55 = count(intVec.begin(), intVec.end(), 55); //occurs once size_t count_101 = count(intVec.begin(), intVec.end(), 101); //occurs once // اطبع النتيجة cout << "There are " << count_9 << " 9s" << endl; cout << "There is " << count_55 << " 55" << endl; cout << "There is " << count_101 << " 101" << ends; // 4 ابحث عن أول عنصر في المتجهة يساوي vector < int > ::iterator itr_4 = find(intVec.begin(), intVec.end(), 4); // حساب مرات حدوثه في المتجه size_t count_4 = count(itr_4, intVec.end(), * itr_4); // should be 2 cout << "There are " << count_4 << " " << * itr_4 << endl; return 0; } الناتج سيكون: There are 2 9s There is 1 55 There is 0 101 There are 2 4 std::count_if تحسب std::count_if عدد العناصر في المجال التي تحقّق شرطًا معينًا. template < class InputIterator, class UnaryPredicate > typename iterator_traits < InputIterator > ::difference_type count_if(InputIterator first, InputIterator last, UnaryPredicate red); المعاملات ‎first‎ - مكرّر يشير إلى بداية المجال. ‎last‎ - مكرّر يشير إلى نهاية المجال. ‎red‎ - دالّة شرطية (تعيد إما true أو false). القيمة المعادة عدد العناصر في المجال التي تحقّق الشرط. انظر المثال التالي: #include <iostream> #include <vector> #include <algorithm> using namespace std; /* عرِّف بعض الدوال لاستخدامها كشروط */ // فرديا number إن كان true إعادة bool isOdd(int i) { return i % 2 == 1; } // أكبر من قيمة وسيط المنشِئ number إن كان true كائن دالي يعيد provided class Greater { int _than; public: Greater(int th): _than(th) {} bool operator()(int i) { return i > _than; } }; int main(int argc, const char * argv[]) { // أنشئ متجهًا vector myvec = {1,5,8,0,7,6,4,5,2,1,5,0,6,9,7}; // لحساب عدد العناصر الزوجية lambda استخدام دالّة size_t evenCount = count_if(myvec.begin(), myvec.end(), [](int i) { return i % 2 == 0; }); // >= C++11 // استخدام مؤشّر دالّة لحساب عدد الأعداد الفردية في النصف الأول من المتجهة size_t oddCount = count_if(myvec.begin(), myvec.end() - myvec.size() / 2, isOdd); // استخدام كائن دالّي لحساب عدد العناصر الأصغر من 5 size_t greaterCount = count_if(myvec.begin(), myvec.end(), Greater(5)); cout << "vector size: " << myvec.size() << endl; cout << "even numbers: " << evenCount << " found" << endl; cout << "odd numbers: " << oddCount << " found" << endl; cout << "numbers > 5: " << greaterCount << " found" << endl; return 0; } الناتج سيكون: vector size: 15 even numbers: 7 found odd numbers: 4 found numbers > 5: 6 found هذا الدرس جزء من سلسلة دروس عن C++‎. ترجمة -بتصرّف- للفصل Chapter 62: Standard Library Algorithms من كتاب C++ Notes for Professionals
  25. std::optional: القيم الاختيارية تُستخدم القيم الاختيارية (المعروفة أيضًا باسم "أنواع الشّك") لتمثيل نوع قد يكون محتواه موجودًا أو لا، وقد قُدِّمت في C++‎ 17 على هيئة صنف ‎std::optional‎. فمثلًا، قد يحتوي كائن من النوع ‎std::optional<int>‎ على قيمة من النوع ‎int‎، أو قد لا يحتوي على أيّ قيمة. وتُستخدَم القيم الاختيارية إمّا لتمثيل قيمة قد لا تكون موجودة، أو كنوعٍ للقيمة المعادة من دالّة قد تفشل أحيانًا في إعادة نتيجة ذات معنى. استخدام قيمة اختيارية لتمثيل غياب القيمة كانت المؤشّرات التي تحمل القيمة ‎nullptr‎ قبل C++‎ 17 تمثّل عدم وجود أيّ قيمة، يعد هذا حلاً جيدًا للكائنات الكبيرة التي خُصِّصت ديناميكيًا وتُدار بواسطة مؤشّرات. لكن لا يعمل هذا الحلّ بشكل جيّد مع الأنواع الصغيرة أو الأوّلية (primitive) مثل ‎int‎، والتي نادرًا ما تُخصّص أو تُدار ديناميكيًا من قبل المؤشّرات، ويوفّر ‎std::optional‎ حلاً مثاليًا لهذه المشكلة الشائعة. في المثال أدناه، عرّفنا البنية Person‎، والتي تمثل شخصًا، هذا الشخص يمكنه أن يمتلك حيوانًا أليفًا (pet)، لكنّ ذلك ليس ضروريًا. ولكي نخبر المصرّف بأنّ الحقل pet اختياري هنا، فسنُصرّح عنه بواسطة المُغلّف ‎std::optional‎. #include <iostream> #include <optional> #include <string> struct Animal { std::string name; }; struct Person { std::string name; std::optional < Animal > pet; }; int main() { Person person; person.name = "John"; if (person.pet) { std::cout << person.name << "'s pet's name is " << person.pet - > name << std::endl; } else { std::cout << person.name << " is alone." << std::endl; } } القيم الاختيارية كقيمة معادة انظر المثال التالي: std::optional < float > divide(float a, float b) { if (b != 0. f) return a / b; return {}; } في المثال أعلاه، سنعيد الكسر ‎a/b‎، ولكن إذا لم يكن الكسر مُعرّفا (إن كان ‎b‎ يساوي 0 مثلًا)، فسَنعيد القيمة الاختيارية الفارغة. هذا مثال أكثر تعقيدًا: template < class Range, class Pred > auto find_if( Range&& r, Pred&& p ) { using std::begin; using std::end; auto b = begin(r), e = end(r); auto r = std::find_if(b, e, p); using iterator = decltype(r); if (r == e) return std::optional < iterator > (); return std::optional < iterator > (r); } template < class Range, class T > auto find( Range&& r, T const& t ) { return find_if( std::forward<Range>(r), [&t](auto&& x){return x==t;} ); } تبحث الدالّة ‎‎find( some_range, 7 )‎ في الحاوية أو النطاق ‎some_range‎ عن شيء يساوي العدد ‎7‎، وتفعل الدالة ‎find_if‎ عبر دالّة شرطية (predicate)، فتعيد إمّا قيمة اختيارية فارغة إذا لم يُعثر على أيّ شيء يساوي العدد ‎7‎، أو عنصرًا اختياريا يحتوي على مُكرّر إلى العنصر في حال كان موجودًا. هذا يتيح لك القيام بما يلي: if (find(vec, 7)) { // code } أو حتى: if (auto oit = find(vec, 7)) { vec.erase( * oit); } دون الحاجة إلى استخدام مُكرّرات begin/end أو إجراء الاختبارات. value_or انظر المثال التالي: void print_name( std::ostream& os, std::optional<std::string> const& name ) { std::cout "Name is: " << name.value_or("<name missing>") << '\n'; } يعيد التابع ‎value_or‎ القيمة المخزّنة في القيمة الاختيارية، أو يعيد الوسيط إذا لم يكن هناك أيّ شيء مُخزّن. يتيح لك هذا إمكانية أخذ القيمة الاختيارية (التي يمكن أن تكون فارغة) وتحديد سلوك افتراضي عندما تكون بحاجة إلى قيمة، وهكذا الطريقة، يمكن ترك تحديد "السلوك الافتراضي" إلى أن تكون هناك حاجة إليه، بدلًا من إنشاء قيمة افتراضية داخل مُحرِّك ما. مقاربات أخرى للقيم الاختيارية هناك العديد من الطرق الأخرى لحلّ المشكلة التي تحلها القيم الاختياريّة ‎std::optional‎، لكن لا طريقة كاملة من تلك الطرق: القيم الاختيارية مقابل المؤشّر في بعض الحالات، يمكننا تمثيل "اختياريّةِ كائنٍ" عبر توفير مؤشّر يشير إلى كائن موجود أو مؤشّر فارغ ‎nullptr‎ للإشارة إلى فشل العمليّة. ولكنّ استخدام هذه الطريقة يقتصر على الحالات التي تكون فيها الكائنات موجودة بالفعل - بالمقابل، يمكن للقيم الاختيارية ‎optional‎ أن تُستخدَم لإعادة كائنات جديدة دون الحاجة إلى تخصيص الذاكرة. القيم الاختيارية مقابل القيم التنبيهية من المقاربات الشائعة استخدام قيمة خاصّة للإشارة إلى أنّ القيمة لا معنى لها، وقد تكون هذه القيمة مثلًا 0 أو ‎-1 بالنسبة للأعداد الصحيحة، أو ‎nullptr‎ بالنسبة للمؤشّرات. مثلًا، لنفترض أنّ هناك دالّة تحاول العثور على فهرس أول ظهور لحرف في سلسلة نصية، في حال كان الحرف موجودًا في السلسلة النصية، فستعيد فهرس أول ظهور له، أمّا في حال لم يكن موجودًا، فستعيد القيمة ‎-1 للدلالة على أنّ الحرف غير موجود في السلسلة النصية. القيمة ‎-1 تمثل القيمة التنبيهية، لأنها تنبّهنا إلى أمر ما (في هذا المثال، تنبِّهنا إلى عدم وجود الحرف المبحوث عنه في السلسلة النصية). المشكلة في هذه المقاربة أنّها تقلل من مساحة القيم الصالحة (لا يمكنك التمييز بين القيمة 0 الصالحة والقيمة 0 التي لا معنى لها)، وليست كل الأنواع فيها خيار طبيعي للقيم التنبيهية. القيم الاختيارية مقابل الأزواج std::pair‎,> من المقاربات الشائعة أيضًا توفير زوج يكون أحد عُنصَريه قيمةً بوليانية ‎bool‎ للإشارة إلى كون القيمة ذات معنى أم لا، ويشترط هذا أن يكون نوع القيمة قابلًا للإنشاء افتراضيًا (default-constructible) في حالة حدوث خطأ، وهو أمر غير ممكن في بعض الأنواع، وقد يكون ممكنًا ولكن غير مرغوب بالنسبة لأنواع أخرى. بالمقابل، لا تحتاج القيم الاختيارية ‎optional<T>‎ في حال حدوث خطأ إلى بناء أي شيء. استخدام القيم الاختيارية لتمثيل فشل دالة قبل C++‎ 17، كانت الدوالّّ تمثِّل الفشلَ عادة بإحدى الطرق التالية: إعادة مؤشّر فارغ. على سبيل المثال، استدعاء دالّة ‎Delegate *App::get_delegate()‎ على نسخة من الصنف ‎App‎ ليس لها مُفوّض (delegate) سيعيد ‎nullptr‎. يُعدّ هذا حلاً جيدًا للكائنات التي خُصِّصت ديناميكيًا أو الكائنات الكبيرة التي تُدار عبر المُؤشّرات، لكنه ليس حلاً جيدًا بالنسبة للكائنات الصغيرة التي عادةً ما تكون مرصوصة (stack-allocated) وتُمرّر عن طريق النسخ. تحجز قيمة محدّدة من النوع المُعاد للإشارة إلى الفشل. على سبيل المثال، قد يؤدي استدعاء دالّة ‎unsigned shortest_path_distance(Vertex a, Vertex b)‎ على رأسين (vertices) غير مُتصلين إلى إعادة 0 للإشارة إلى هذه الحقيقة. يتم إقران القيمة المُعادة مع قيمة بوليانية ‎bool‎ لتحديد ما إذا كانت القيمة المعادة ذات معنى أم لا. على سبيل المثال، يؤدي استدعاء دالّة ‎std::pair<int, bool> parse(const std::string &str)‎ باستخدام وسيط يحتوي سلسلة نصّية لا تتضمّن عددًا صحيحًا إلى إعادة زوج يضمّ عددًا صحيحا غير محدد وقيمة بوليانية تساوي ‎false‎. في هذا المثال، يُعطى لزيد حيوانين أليفَين، سوسن وسوسان، ثم تُستدعى الدالّة ‎Person::pet_with_name()‎ لاسترداد الكلب "وافي". ونظرًا لأنّ زيد لا يملك كلبًا باسم "وافي"، فإن الدالّة ستفشل، وستُعاد القيمة ‎std::nullopt‎ بدلاً من ذلك. #include <iostream> #include <optional> #include <string> #include <vector> struct Animal { std::string name; }; struct Person { std::string name; std::vector < Animal > pets; std::optional < Animal > pet_with_name(const std::string & name) { for (const Animal & pet: pets) { if (pet.name == name) { return pet; } } return std::nullopt; } }; int main() { Person john; zaid.name = "زيد"; Animal susan; susan.name = "سوسن"; zaid.pets.push_back(susan); Animal susanne; susanne.name = "سوسان"; zaid.pets.push_back(susanne); std::optional < Animal > dog = john.pet_with_name("وافي"); if (dog) { std::cout << "يملك زيد حيوانًا أليفًا اسمه وافي" << std::endl; } else { std::cout <<”لا يملك زيد حيوانًا أليفًا اسمه وافي" << std::endl; } } std::function: تغليف الكائنات القابلة للاستدعاء يوضح المثال التالي طريقة استخدام std::function: #include <iostream> #include <functional> std:: function < void(int, const std::string & ) > myFuncObj; void theFunc(int i, const std::string & s) { std::cout << s << ": " << i << std::endl; } int main(int argc, char * argv[]) { myFuncObj = theFunc; myFuncObj(10, "hello world"); } استخدام std::function مع std::bind إن احتجت إلى استدعاء دالّة مع تمرير وسائط إليها فإنّ استخدام ‎std::function‎ مع ‎std::bind‎ سيعطيك حلولًا فعّالة للغاية كما هو موضّح أدناه. class A { public: std:: function < void(int, const std::string & ) > m_CbFunc = nullptr; void foo() { if (m_CbFunc) { m_CbFunc(100, "event fired"); } } }; class B { public: B() { auto aFunc = std::bind( & B::eventHandler, this, std::placeholders::_1, std::placeholders::_2); anObjA.m_CbFunc = aFunc; } void eventHandler(int i, const std::string & s) { std::cout << s << ": " << i << std::endl; } void DoSomethingOnA() { anObjA.foo(); } A anObjA; }; int main(int argc, char * argv[]) { B anObjB; anObjB.DoSomethingOnA(); } ربط std::function مع نوع آخر قابل للاستدعاء يوضح المثال التالي كيفية استخدام std::function لاستدعاء دالة من نمط C، ودالة تابعة لصنف، وعامل ()operator، ودالة لامدا. ويتم استدعاء الدالة من خلال الوسائط الصحيحة ووسائط بترتيب مختلف، وكذلك بأنواع وأعداد مختلفة. #include <iostream> #include <functional> #include <iostream> #include <vector> using std::cout; using std::endl; using namespace std::placeholders; // دالّة بسيطة لتُستدعى double foo_fn(int x, float y, double z) { double res = x + y + z; std::cout << "foo_fn called with arguments: " << x << ", " << y << ", " << z << " result is : " << res << std::endl; return res; } // بنية مع دالة تابعة لاستدعائها struct foo_struct { // الدالة المراد استدعاؤها double foo_fn(int x, float y, double z) { double res = x + y + z; std::cout << "foo_struct::foo_fn called with arguments: " << x << ", " << y << ", " << z << " result is : " << res << std::endl; return res; } // هذا التابع له بصمة مختلفة، مع ذلك يمكن استخدامه // لاحظ أنّ ترتيب المعاملات قد تغير double foo_fn_4(int x, double z, float y, long xx) { double res = x + y + z + xx; std::cout << "foo_struct::foo_fn_4 called with arguments: " << x << ", " << z << ", " << y << ", " << xx << " result is : " << res << std::endl; return res; } // جعل الكائن بأكمله قابلا للاستدعاء operator() التحميل الزائد للعامل double operator()(int x, float y, double z) { double res = x + y + z; std::cout << "foo_struct::operator() called with arguments: " << x << ", " << y << ", " << z << " result is : " << res << std::endl; return res; } }; int main(void) { // typedefs using function_type = std:: function < double(int, float, double) > ; // foo_struct نسخة foo_struct fs; // سنخزّن هنا كل الدوالّ المربوطة std::vector < function_type > bindings; // var #1 - يمكن استخدام دالّة بسيطة وحسب function_type var1 = foo_fn; bindings.push_back(var1); // var #2 - يمكنك استخدام تابع function_type var2 = std::bind( & foo_struct::foo_fn, fs, _1, _2, _3); bindings.push_back(var2); // var #3 - يمكنك استخدام تابع مع بصمة مختلفة // لها عدد مختلف من المعاملات ومن أنواع مختلفة foo_fn_4 function_type var3 = std::bind(&foo_struct::foo_fn_4, fs, _1, _3, _2, 0l); bindings.push_back(var3); // var #4 - مُحمَّل تحميلا زائدا operator() يمكنك استخدام كائن ذي معامل function_type var4 = fs; bindings.push_back(var4); // var #5 - lambda يمكنك استخدام دالّة function_type var5 = [](int x, float y, double z) { double res = x + y + z; std::cout << "lambda called with arguments: " << x << ", " << y << ", " << z << " result is : " << res << std::endl; return res; }; bindings.push_back(var5); std::cout << "Test stored functions with arguments: x = 1, y = 2, z = 3" << std::endl; for (auto f: bindings) f(1, 2, 3); } انظر هذا المثال الحي. الناتج: Test stored functions with arguments: x = 1, y = 2, z = 3 foo_fn called with arguments: 1, 2, 3 result is : 6 foo_struct::foo_fn called with arguments: 1, 2, 3 result is : 6 foo_struct::foo_fn_4 called with arguments: 1, 3, 2, 0 result is : 6 foo_struct::operator() called with arguments: 1, 2, 3 result is : 6 lambda called with arguments: 1, 2, 3 result is : 6 تخزين وسائط الدالة في صف std::tuple تحتاج بعض البرامج إلى تخزين الوسائط إلى حين استدعاء بعض الدوالّ في المستقبل. ويوضّح هذا المثال كيفية استدعاء أيّ دالّة باستخدام الوسائط المخزّنة في صفّ std::tuple #include <iostream> #include <functional> #include <tuple> #include <iostream> // دالّة بسيطة لاستدعائها double foo_fn(int x, float y, double z) { double res = x + y + z; std::cout << "foo_fn called. x = " << x << " y = " << y << " z = " << z << " res=" << res; return res; } // مساعِدات من أجل تسريع الصف. template < int... > struct seq {}; template < int N, int...S > struct gens: gens < N - 1, N - 1, S... > {}; template < int...S > struct gens < 0, S... > { typedef seq < S... > type; }; //استدعاء المساعِدات template < typename FN, typename P, int...S > double call_fn_internal(const FN & fn, const P & params, const seq < S... > ) { return fn(std::get < S > (params)...); } // std::tuple استدعاء الدالّة مع الوسائط المُخزنة في template < typename Ret, typename...Args > Ret call_fn(const std:: function < Ret(Args...) > & fn, const std::tuple < Args... > & params) { return call_fn_internal(fn, params, typename gens < sizeof...(Args) > ::type()); } int main(void) { // الوسائط std::tuple < int, float, double > t = std::make_tuple(1, 5, 10); // الدالّة المراد استدعاؤها std:: function < double(int, float, double) > fn = foo_fn; // استدعاء الدالّة مع تمرير الوسائط المُخزّنة إليها call_fn(fn, t); } هذا مثال حي الناتج: foo_fn called. x = 1 y = 5 z = 10 res=16 استخدام std::function مع تعابير لامدا والصنف std::bind #include <iostream> #include <functional> using std::placeholders::_1; // std::bind ستُستخدَم في مثال int stdf_foobar(int x, std:: function < int(int) > moo) { return x + moo(x); // std::function moo استدعاء } int foo (int x) { return 2+x; } int foo_2 (int x, int y) { return 9*x + y; } int main() { int a = 2; /* مؤشّرات الدوالّ */ std::cout << stdf_foobar(a, & foo) << std::endl; // 6 ( 2 + (2+2) ) // stdf_foobar(2, foo) يمكن أن تكون أيضا /* Lambda تعابير */ /* std::function في كائن lambda يمكن تخزين دالّة مُغلقة من تعبير */ std::cout << stdf_foobar(a, [capture_value](int param) -> int { return 7 + capture_value * param; }) << std::endl; // result: 15 == value + (7 * capture_value * value) == 2 + (7 + 3 * 2) /* std::bind تعابير */ /* std::bind يمكن تمرير نتيجة التعبير * مثلا عبر ربط المعاملات باستدعاء مؤشّر دالّة */ int b = stdf_foobar(a, std::bind(foo_2, _1, 3)); std::cout << b << std::endl; // b == 23 == 2 + ( 9*2 + 3 ) int c = stdf_foobar(a, std::bind(foo_2, 5, _1)); std::cout << c << std::endl; // c == 49 == 2 + ( 9*5 + 2 ) return 0; } الحمل الزائد للدوال (function overhead) يمكن أن تتسبب std::function‎ في حِمل زائد كبير لأنّ std::function‎ لها دلالات قيمية (value semantics)، فسيكون من اللازم أن تنسخ أو تنقل ما استُدعِي إليها. ولكن بما أنها تستطيع أخذ كائن قابل للاستدعاء من أيّ نوع، فسيتعينّ عليها في كثير من الأحيان أن تخصّص ذاكرة ديناميكية بذلك. تحتوي بعض تطبيقات ‎function‎ على ما يسمى "تحسين الكائنات الصغيرة" (small object optimization)، وفيها تُخزّن الأنواع الصغيرة مثل مؤشّرات الدوالّ أو مؤشّرات الأعضاء أو الكائنات الدالّية (Functors) ذات الحالة الصغيرة، مباشرة في كائن الدالّة ‎function‎. لكنّ هذا لن يعمل إلّا إن كان النوع قابلًا للإنشاء النقلي عند الاعتراض (noexcept move constructible). أيضًا، لا يتطلب معيار C++‎ أن توفّر جميع التطبيقات مثل هذا التحسين. إليك المثال التالي: // ملف الترويسة using MyPredicate = std::function<bool(const MyValue &, const MyValue &)>; void SortMyContainer(MyContainer &C, const MyPredicate &pred); // الملف المصدري void SortMyContainer(MyContainer &C, const MyPredicate &pred) { std::sort(C.begin(), C.end(), pred); } يُعدّ معامل القالب هو الحلّ المفضّل لـ ‎SortMyContainer‎، ولكن إن افترضنا أنّ هذا غير ممكن أو غير مرغوب فيه لسبب من الأسباب، فلن تحتاج ‎SortMyContainer‎ إلى تخزين ‎pred‎ إلا في إطار استدعائها. ومع ذلك، فقد تُخصّص لـ ‎pred‎ ذاكرةً إن كان الكائن الدّالِّي (functor) المُعطى ذا حجم غير معروف. تخصّص ‎function‎ الذاكرة لأنّها تحتاج إلى شيءٍ لتنسخ أو تنقل إليه، كما أنها تأخذ ملكية الكائن القابل للاستدعاء الذي مُرِّر إليها، لكنّ ‎SortMyContainer‎ لا تحتاج إلى امتلاك المُستدعِي ولكنها تحتاج إلى مرجع إليه وحسب، لذا فلا داعي لاستخدام ‎function‎ هنا. كذلك لا يوجد أي نوع دالّة قياسي يشير حصرًا إلى كائنٍ قابل للاستدعاء، لذلك سيكون عليك أن تنتظر إيجاد حلّ لهذا، أو يمكنك التعايش مع الحمل الزائد. أيضّا، لا تملك ‎function‎ أيّ وسيلة فعّالة للتحكم في الموضع الذي تأتي منه تخصيصات الذاكرة الخاصّة بالكائن. صحيح أنّ لها مُنشئات تأخذ كائن تخصيص ‎allocator‎، ولكنّ العديد من التقديمات لا تقدّمها بالشكل الصحيح … أو لا تقدما بتاتًا. الإصدار ≥ C++‎ 17 لم يعُد المنشئ ‎function‎ الذي يأخذ كائن تخصيصٍ ‎allocator‎ جزءًا من النوع. لذلك لا توجد أيّ طريقة لإدارة التخصيص. واعلم أن استدعاء ‎function‎ أبطأ من استدعاء المحتويات مباشرةً. كذلك يجب أن يكون الاستدعاء عبر function غير مباشر لأنّ نُسَخَ ‎function‎ يمكن أن تحتوي كائنًا قابلًا للاستدعاء، ويكافئ الحِمل الزائد الناتج عن استدعاء ‎function‎ الحِمل الزائد الناتج عن استدعاء دالّة وهمية. std::forward_list: القوائم الإدراجية النوع std::forward_list هو حاوية تدعم الإدراج السريع للعناصر من أي مكان فيها وكذلك إزالة تلك العناصر، لكنها لا تدعم الوصول العشوائي السريع. يُنفَّذ النوع std::forward_list كقائمة مرتبطة أحادية (singly-linked list)، وليس لها عمومًا أيّ حِمل زائد (overhead) مقارنة بتطبيقها في C. وتوفّر هذه الحاوية، مقارنة بـ ‎std::list‎، مساحة تخزين أكثر كفاءة عند غياب الحاجة للتكرار ثنائي الاتجاه (bidirectional iteration). انظر المثال التالي: #include <forward_list> #include <string> #include <iostream> template < typename T > std::ostream & operator << (std::ostream & s, const std::forward_list < T > & v) { s.put('['); char comma[3] = { '\0', ' ', '\0' }; for (const auto & e: v) { s << comma << e; comma[0] = ','; } return s << ']'; } int main() { // c++11 صياغة قائمة المهييء في std::forward_list < std::string > words1 { "the", "frogurt", "is", "also", "cursed" }; std::cout << "words1: " << words1 << '\n'; // words2 == words1 std::forward_list < std::string > words2(words1.begin(), words1.end()); std::cout << "words2: " << words2 << '\n'; // words3 == words1 std::forward_list < std::string > words3(words1); std::cout << "words3: " << words3 << '\n'; // words4 is {"Mo", "Mo", "Mo", "Mo", "Mo"} std::forward_list < std::string > words4(5, "Mo"); std::cout << "words4: " << words4 << '\n'; } الناتج: words1: [the, frogurt, is, also, cursed] words2: [the, frogurt, is, also, cursed] words3: [the, frogurt, is, also, cursed] words4: [Mo, Mo, Mo, Mo, Mo] التوابع إليك قائمة التوابع الخاصة بالنوع std::forward_list: اسم التابع التعريف operator= يعيّن قيمًا إلى الحاوية assign يعيّن قيمًا إلى الحاوية get_allocator يعيد المخصِّص المرتبط به (associated allocator) front يصل إلى العنصر الأول before_begin يعيد مكررًا إلى العنصر قبل البداية cbefore_begin يعيد مكررًا ثابت إلى العنصر قبل البداية begin يعيد مكررًا إلى البداية cbegin يعيد مكررًا ثابت إلى البداية end يعيد مكررًا إلى النهاية cend يعيد مكررًا إلى النهاية empty يتحقق إن كانت الحاوية فارغة max_size يعيد الحد الأقصى للعدد الممكن من العناصر clear يمسح المحتويات insert_after يُدرِج عناصرًا بعد عنصر ما emplace_after ينشئ عنصرًا مكان عنصر آخر erase_after يمحو عنصرًا موجودا بعد عنصر ما push_front يدرج عنصرًا في البداية emplace_front ينشئ عنصرًا في البداية pop_front يزيل العنصر الأول resize يغير عدد العناصر المخزنة swap يبدل المحتويات merge يدمج قائمتين مرتبتين splice_after ينقل عناصر من قائمة أمامية أخرى remove يزيل العناصر التي تحقق شرطا محددا remove_if يزيل العناصر التي تحقق شرطا محددا reverse يعكس ترتيب العناصر unique يزيل العناصر المتساوية المتتالية sort يرتب العناصر 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; } std::pair: الأزواج عوامل الموازنة معامِلات هذه العوامل هي ‎lhs‎ و ‎rhs‎: ‎operator==‎ - يتحقّق هذا العامل من أنّ عناصر كلا الزوجين ‎lhs‎ و ‎rhs‎ متساويان، وتكون القيمة المُعادة هي true إن كان lhs.first == rhs.first و lhs.second == rhs.second، وإلّا فستكون false. std::pair < int, int > p1 = std::make_pair(1, 2); std::pair < int, int > p2 = std::make_pair(2, 2); if (p1 == p2) std::cout << "equals"; else std::cout << "not equal"; // ستُظهر التعليمة هذا، لأن الزوجين غير متماثلين ‎operator!=‎ - يتحقّق هذا العامل ممّا إذا كان أيّ من عناصر الزوجيين ‎lhs‎ و ‎rhs‎ غير متساويين، وتكون القيمة المُعادة هيtrue إن كان ‎lhs.first != rhs.first أو lhs.second != rhs.second، وإلا فستُعاد القيمة false. ‎operator<‎ - إذا كان lhs.first<rhs.first‎، فسيُعيد true وإن كان rhs.first<lhs.first‎ فسيُعيد false. وكذلك إن كان ‎lhs.second<rhs.second‎ فسيُعيد ‎true‎، أما خلاف ذلك سيُعيد ‎false‎. ‎operator<=‎ - يُعيد ‎!(rhs<lhs)‎ ‎operator>‎ - يعيد ‎rhs<lhs ‎operator>=‎ - يعيد ‎!(lhs<rhs)‎ هذا مثال آخر يستخدم حاويات أزواج، ويستخدم العامل ‎operator<‎ لترتيب الحاوية. #include <iostream> #include <utility> #include <vector> #include <algorithm> #include <string> int main() { std::vector<std::pair<int, std::string>> v = { {2, "baz"}, {2, "bar"}, {1, "foo"} }; std::sort(v.begin(), v.end()); for (const auto & p: v) { std::cout << "(" << p.first << "," << p.second << ") "; // (1,foo) (2,bar) (2,baz) :الناتج } } إنشاء زوج والوصول إلى عناصره تتيح لنا الأزواج أن نعامل كائِنين كما لو كانا كائنًا واحدًا، ويمكن إنشاء الأزواج بسهولة بمساعدة دالّة القالب std::make_pair. وهناك طريقة أخرى، وهي إنشاء زوج وتعيين عنصُريه (‎first‎ و ‎second‎) لاحقًا. #include <iostream> #include <utility> int main() { std::pair < int, int > p = std::make_pair(1, 2); // إنشاء الزوج std::cout << p.first << " " << p.second << std::endl; // الوصول إلى العناصر // يمكننا أيضا إنشاء الزوج وتعيين عناصره لاحقا std::pair < int, int > p1; p1.first = 3; p1.second = 4; std::cout << p1.first << " " << p1.second << std::endl; // يمكننا أيضا إنشاء زوج باستخدام منشئ std::pair < int, int > p2 = std::pair < int, int > (5, 6); std::cout << p2.first << " " << p2.second << std::endl; return 0; } std::atomics: الأنواع الذرية كل استنساخ (instantiation) وتخصيص للقالب ‎std::atomic‎ يعرّف نوعًا ذريًا (atomic type)، فإن قامت أحد الخيوط (threads) بالكتابة في كائن ذرّي أثناء قراءة مسلك آخر منه، فإنّ السّلوك سيكون مُعرّفًا بشكل جيد، ولن يحدث أيّ مشكل. إضافة إلى ذلك، قد يؤدّي الدخول إلى الكائنات الذرية إلى تهيئة تزامن بين الخيوط ويطلب دخولًا غير ذرّي للذاكرة (non-atomic memory accesses) كما هو مُعرَّف من قِبل ‎std::memory_order‎. يمكن استنساخ std::atomic مع أيّ نوع قابل للنسخ (‎TriviallyCopyable type T. std::atomic‎)، لكن std::atomic ليست قابلة للنسخ أو النقل. توفّر المكتبة القياسية تخصيصات للقالب std::atomic للأنواع التالية: 1) تعريف تخصيص كامل للنوع البولياني ‎bool‎، وعُرِّفت قيمة التعريف النوعي (typedef) الخاصة به بحيث يُتعامل معه على أنّه نوع ذرّي std::atomic<T>‎ غير مُخصَّص، فيما عدا أنّه ستكون له مخطط (layout) قياسي، ومنشئ افتراضي أولي، ومدمّرات واضحة، كما سيدعم صيغة التهيئة الإجمالية (aggregate initialization syntax): Typedef التخصيص std::atomic_bool std::atomic<bool> 2) تخصيصات كاملة وتعريفات نوعية typedefs للأنواع العددية الصحيحة، كما يلي: Typedef التخصيص std::atomic_char std::atomic<char‎>‎ std::atomic_char std::atomic<char>‎ std::atomic_schar std::atomic<signed char>‎ std::atomic_uchar std::atomic<unsigned char>‎ std::atomic_short std::atomic<short>‎ std::atomic_ushort std::atomic<unsigned short>‎ std::atomic_int std::atomic<int>‎ std::atomic_uint std::atomic<unsigned int>‎ std::atomic_long std::atomic<long>‎ std::atomic_ulong std::atomic<unsigned long>‎ std::atomic_llong std::atomic<long long>‎ std::atomic_ullong std::atomic<unsigned long long>‎ std::atomic_char16_t std::atomic<char16_t>‎ std::atomic_char32_t std::atomic<char32_t>‎ std::atomic_wchar_t std::atomic<wchar_t>‎ std::atomic_int8_t std::atomic<std::int8_t>‎ std::atomic_uint8_t std::atomic<std::uint8_t>‎ std::atomic_int16_t std::atomic<std::int16_t>‎ std::atomic_uint16_t std::atomic<std::uint16_t>‎ std::atomic_int32_t std::atomic<std::int32_t>‎ std::atomic_uint32_t std::atomic<std::uint32_t>‎ std::atomic_int64_t std::atomic<std::int64_t>‎ std::atomic_uint64_t std::atomic<std::uint64_t>‎ std::atomic_int_least8_t std::atomic<std::int_least8_t>‎ std::atomic_uint_least8_t std::atomic<std::uint_least8_t>‎ std::atomic_int_least16_t std::atomic<std::int_least16_t>‎ std::atomic_uint_least16_t std::atomic<std::uint_least16_t>‎ std::atomic_int_least32_t std::atomic<std::int_least32_t>‎ std::atomic_uint_least32_t std::atomic<std::uint_least32_t>‎ std::atomic_int_least64_t std::atomic<std::int_least64_t>‎ std::atomic_uint_least64_t std::atomic<std::uint_least64_t>‎ std::atomic_int_fast8_t std::atomic<std::int_fast8_t>‎ std::atomic_uint_fast8_t std::atomic<std::uint_fast8_t>‎ std::atomic_int_fast16_t std::atomic<std::int_fast16_t>‎ std::atomic_uint_fast16_t std::atomic<std::uint_fast16_t>‎ std::atomic_int_fast32_t std::atomic<std::int_fast32_t>‎ std::atomic_uint_fast32_t std::atomic<std::uint_fast32_t>‎ std::atomic_int_fast64_t std::atomic<std::int_fast64_t>‎ std::atomic_uint_fast64_t std::atomic<std::uint_fast64_t>‎ std::atomic_intptr_t std::atomic<std::intptr_t>‎ std::atomic_uintptr_t std::atomic<std::uintptr_t>‎ std::atomic_size_t std::atomic<std::size_t>‎ std::atomic_ptrdiff_t std::atomic<std::ptrdiff_t>‎ std::atomic_intmax_t std::atomic<std::intmax_t>‎ std::atomic_uintmax_t std::atomic<std::uintmax_t>‎ هذا مثال بسيط على استخدام std::atomic_int:‎ #include <iostream> // std::cout #include <atomic> // std::atomic, std::memory_order_relaxed #include <thread> // std::thread std::atomic_int foo(0); void set_foo(int x) { foo.store(x, std::memory_order_relaxed); // تعيين القيمة الذرية } void print_foo() { int x; do { x = foo.load(std::memory_order_relaxed); // الحصول على القيمة الذرّية } while (x == 0); std::cout << "foo: " << x << '\n'; } int main() { std::thread first(print_foo); std::thread second(set_foo, 10); first.join(); //second.join(); return 0; } // foo: 10 std::variant: المتغايرات إنشاء مؤشرات للتوابع الزائفة (Create pseudo-method pointers) يمكنك استخدام كائن متغَاير (Variant) للشطب الخفيف للنوع (light weight type erasure). انظر المثال التالي: template < class F > struct pseudo_method { F f; // C++17 السماح باستنتاج نوع الصنف في pseudo_method(F && fin): f(std::move(fin)) {} // عامل بحث كوينج->* لا بأس بما أنه تابع زائف template < class Variant > // متغاير LHS للتحقق من أنّ SFINAE إضافة اختبار friend decltype(auto) operator->*( Variant&& var, pseudo_method const& method ) { // تعيد تعبير لامدا يعيد توجيه استدعاء دالة ما var->*method // مما يجعلها تبدو كأنها تتصرف كمؤشّر تابع return [&](auto&&...args)->decltype(auto) { // للحصول على نوع المتغاير visit استخدم return std::visit( [&](auto&& self)->decltype(auto) { return method.f( decltype(self)(self), decltype(args)(args)... ); }, std::forward < Var > (var) ); }; } }; يؤدي هذا إلى إنشاء نوع يزيد تحميل العامل ‎operator->*‎ بمتغاير ‎Variant‎ على الجانب الأيسر. في المثال التالي، سنستخدم استنتاج نوع الصنف الخاص بـ C++ 17 من أجل إيجاد وسيط القالب لـ print، يجب أن يكون self هو أول وسيط يأخذه تابع لامدا الزائف، ثم يأخذ بقية الوسائط ثم يستدعي الدالة. pseudo_method print = [](auto&& self, auto&&...args)->decltype(auto) { return decltype(self)(self).print( decltype(args)(args)... ); }; والآن إن كان لدينا نوعان لكل منهما تابع ‎print‎: struct A { void print(std::ostream & os) const { os << "A"; } }; struct B { void print(std::ostream & os) const { os << "B"; } }; لاحظ أنّهما نوعان غير مترابطان، نستطيع تنفيذ ما يلي: std::variant<A,B> var = A{}; (var->*print)(std::cout); سيتم إرسال الاستدعاء مباشرة إلى ‎A::print(std::cout)‎، لكن لو هيأنا ‎var‎ باستخدام ‎B{}‎، فسيتم إرساله إلى ‎B::print(std::cout)‎. وإذا أنشأنا نوعًا جديدًا C … struct C {}; … فسيكون لدينا: std::variant<A,B,C> var = A{}; (var->*print)(std::cout); ستفشل عملية التصريف، لأنه لا يوجد تابع ‎C.print(std::cout)‎. سوف يسمح توسيع الشيفرة أعلاه باكتشاف واستخدام دوال ‎print‎، ربّما باستخدام ‎if constexpr‎ ضمن التابع الزائف ‎print‎. هذا مثال حي يستخدم ‎boost::variant‎ بدلاً من ‎std::variant‎. الاستخدامات الرئيسية للمتغايرات تنشي الشيفرة التالية متغايرًا (اتحادًا موسومًا tagged union) يمكنه تخزين إمّا عدد صحيح (‎int‎) وإمّا سلسلة نصية‎string‎. std::variant< int, std::string > var; يمكننا تخزين أحد هذين النوعين في الكائن المتغاير: var = "hello"s; ويمكننا الوصول إلى مُحتوياته عبر ‎std::visit‎: // "hello\n" طباعة visit( [](auto&& e) { std::cout << e << '\n'; }, var); عن طريق تمرير دالّّة لامدا متعددة الأشكال أو كائن دالّّة مشابه، وإذا كنا متأكّدين من النوع، فيمكننا الحصول عليه على النحو التالي: auto str = std::get<std::string>(var); ولكن هذا سوف يرفع اعتراضًا إن أخطأنا تقدير النوع. auto* str = std::get_if<std::string>(&var); إن أخطأت التقدير فستُعاد القيمة ‎nullptr‎. تضمن المُتغايرات عدم تخصيص ذاكرة ديناميكية، باستثناء تلك التي تُخصَّص من قبل أنواعها المُضمّنة، ولا يُخزَّن إلا نوع واحد فقط في المتغاير، ويرافقها في حالات نادرة رفع اعتراضات أثناء الإسناد وغياب إمكانية آمنة للتراجع يمكن أن يصبح المتغاير فارغًا. كما تتيح لك المُتغايرات تخزين قيم من عدّة أنواع في متغيّر واحد بأمان وكفاءة. فهي أساسًا اتحادات‎union‎ ذكية وآمنة. إنشاء متغاير هذا لا يشمل المُخصِّصات (allocators). struct A {}; struct B { B()=default; B(B const&)=default; B(int){}; }; struct C { C()=delete; C(int) {}; C(C const&)=default; }; struct D { D( std::initializer_list<int> ) {}; D(D const&)=default; D()=default; }; std::variant < A, B > var_ab0; // A() يحتوي std::variant < A, B > var_ab1 = 7; // a B(7) يحتوي std::variant < A, B > var_ab2 = var_ab1; // a B(7) يحتوي std::variant < A, B, C > var_abc0 { std::in_place_type < C > , 7 }; // a C(7) يحتوي std::variant < C > var_c0; // C لأجل ctor غير قانوني، لا توجد قيمة افتراضية لـ std::variant<A,D> var_ad0( std::in_place_type<D>, {1,3,3,4} ); // D{1,3,3,4} يحتوي std::variant < A, D > var_ad1(std::in_place_index < 0 > ); // A{} يحتوي std::variant<A,D> var_ad2( std::in_place_index<1>, {1,3,3,4} ); // D{1,3,3,4} يحتوي std::iomanip و std::any std::setprecision عند استخدام std::setprecision في التعبير ‎out << setprecision(n)‎ أو ‎in >> setprecision(n)‎، فإنّها تضبط معامل الدقة (precision parameter) الخاصّ بمجرى الخرج أو الدخل عند القيمة n. معامل هذه الدالّة يكون عددًا صحيحًا، ويمثل قيمة الدقة الجديدة. انظر المثال التالي: #include <iostream> #include <iomanip> #include <cmath> #include <limits> int main() { const long double pi = std::acos(-1.L); std::cout << "default precision (6): " << pi << '\n' << "std::precision(10): " << std::setprecision(10) << pi << '\n' << "max precision: " << std::setprecision(std::numeric_limits < long double > ::digits10 + 1) << pi << '\n'; } //Output // 3.14159 : في الدقة الافتراضية (6) يكون الناتج // std::precision(10): 3.141592654 // 3.141592653589793239 :الدقة القصوى std::setfill عند استخدام std::setfill في تعبير ‎out << setfill(c)‎، فإنّها تُضبط محرف الملء (fill character) الخاصّ بمجرى الخرج عند القيمة ‎c‎. ملاحظة: يمكن الحصول على محرف الملء الحالي عبر الدالّة ‎std::ostream::fill‎. مثال: #include <iostream> #include <iomanip> int main() { std::cout << "default fill: " << std::setw(10) << 42 << '\n' << "setfill('*'): " << std::setfill('*') << std::setw(10) << 42 << '\n'; } // 42 :الافتراضي // setfill('*'): ********42 std::setiosflags عند استخدام std::setiosflags في التعبير ‎out << setiosflags(mask)‎ أو ‎in >> setiosflags(mask)‎، فإنّها تضبط كل رايات التنسيق (format flags) الخاصّة بمجرى الخرج أو الدخل كما هو محدّد من قِبل القناع (mask). هذه قائمة بكل رايات ‎std::ios_base::fmtflags‎: ‎dec‎: استخدام أساس عشري لدخل وخرج (I / O) الأعداد الصحيحة ‎oct‎: استخدام أساس ثماني (octal base) لدخل وخرج الأعداد الصحيحة ‎hex‎: استخدام أساس ست عشري (hexadecimal base) لدخل وخرج الأعداد الصحيحة ‎basefield‎ - ‎dec‎|‎oct‎|‎hex‎|‎0‎: مفيدة لتقنيع (masking) العمليات ‎left‎: التعديل الأيسر (إضافة محارف الملْء إلى اليمين) ‎right‎: التعديل الأيمن (إضافة محارف الملء إلى اليسار) ‎internal‎: التعديل الداخلي (إضافة محارف الملء إلى نقطة معيّنة في الداخل) adjustfield - left|right|internal : مفيدة لتقنيع العمليات. ‎scientific‎: توليد الأنواع العددية العشرية باستخدام الصيغة العلمية، أو الصيغة الثمانية (hex notation) في حال اقترنت بـ fixed. ‎fixed‎: توليد الأنواع العددية العشرية باستخدام الصيغة الثابتة (fixed notation)، أو الصيغة الثمانية (hex notation) في حال اقترنت بـ scientific. *‎floatfield‎ - ‎scientific‎|‎fixed‎|(scientific‎|‎fixed‎)|‎0‎: مفيدة لتقنيع لعمليات ‎boolalpha‎: إدراج واستخراج نوع منطقي وفق تنسيق أبجدي رقمي ‎showbase‎: إنشاء سابقة (prefix) تشير إلى الأساس الرقمي لخرج الأعداد الصحيحة، وتتطلّب إشارة إلى العُملة في حال الدخل والخرج الماليّ. ‎showpoint‎: إنشاء محرفَ الفاصلة العشرية (decimal-point character) دون قيد أو شرط لخرج الأعداد العشرية ‎showpos‎: توليد المحرف ‎+‎ للأعداد غير السالبة ‎skipws‎: تخطي المسافات البيضاء الموجودة في البداية قبل عمليات الإدخال ‎unitbuf‎: نقل (flush) الناتج بعد كل عملية خرج ‎uppercase‎: استبدال بعض الأحرف الصغيرة بالأحرف الكبيرة المقابلة لها في بعض مخرجات عمليات الإخراج. أمثلة على المعدِّلات: #include <iostream> #include <string> #include<iomanip> int main() { int l_iTemp = 47; std::cout << std::resetiosflags(std::ios_base::basefield); std::cout << std::setiosflags(std::ios_base::oct) << l_iTemp << std::endl; // ==> 57 std::cout << std::resetiosflags(std::ios_base::basefield); std::cout << std::setiosflags(std::ios_base::hex) << l_iTemp << std::endl; // ==> 2f std::cout << std::setiosflags(std::ios_base::uppercase) << l_iTemp << std::endl; // ==> 2F std::cout << std::setfill('0') << std::setw(12); std::cout << std::resetiosflags(std::ios_base::uppercase); std::cout << std::setiosflags(std::ios_base::right) << l_iTemp << std::endl; // ==> 00000000002f std::cout << std::resetiosflags(std::ios_base::basefield | std::ios_base::adjustfield); std::cout << std::setfill('.') << std::setw(10); std::cout << std::setiosflags(std::ios_base::left) << l_iTemp << std::endl; // ==> 47........ std::cout << std::resetiosflags(std::ios_base::adjustfield) << std::setfill('#'); std::cout << std::setiosflags(std::ios_base::internal | std::ios_base::showpos); std::cout << std::setw(10) << l_iTemp << std::endl; // ==> +#######47 double l_dTemp = -1.2; double pi = 3.14159265359; std::cout << pi << " " << l_dTemp << std::endl; // ==> +3.14159 -1.2 std::cout << std::setiosflags(std::ios_base::showpoint) << l_dTemp << std::endl; // ==> -1.20000 std::cout << setiosflags(std::ios_base::scientific) << pi << std::endl; // ==> +3.141593e+00 std::cout << std::resetiosflags(std::ios_base::floatfield); std::cout << setiosflags(std::ios_base::fixed) << pi << std::endl; // ==> +3.141593 bool b = true; std::cout << std::setiosflags(std::ios_base::unitbuf | std::ios_base::boolalpha) << b; // ==> true return 0; } std::setw انظر المثال التالي حيث يطبع السطر الثاني val في أقصى يسار شاشة الخرج، بينما يطبعها السطر الثالث في حقل إخراج طوله 10 بدءًا من النهاية اليمنى للحقل: int val = 10; std::cout << val << std::endl; std::cout << std::setw(10) << val << std::endl; يكون الخرج ما يلي: 10 10 1234567890 (السطر الأخير موجود للمساعدة على رؤية مواضع الأحرف). عندما نحتاج إلى أن يكون الخرج مُنسّقًا بتنسيق معيّن، فقد نحتاج إلى ضبط عرض الحقل، ويمكن القيام بذلك باستخدام std::setw و std::iomanip. توضّح الشيفرة التالية صيغة ‎std::setw‎: std::setw(int n) يمثّل n في هذا المثال طول حقل الخرج الذي سيُعيَّن. std::any يوضح المثال التالي كيفية استخدام std::any: std::any an_object{ std::string("hello world") }; if (an_object.has_value()) { std::cout << std::any_cast<std::string>(an_object) << '\n'; } try { std::any_cast<int>(an_object); } catch(std::bad_any_cast&) { std::cout << "Wrong type\n"; } std::any_cast<std::string&>(an_object) = "42"; std::cout << std::any_cast<std::string>(an_object) << '\n'; المخرجات الناتجة: hello world Wrong type 42 std::set و std::multiset: المجموعات والمجموعات المتعددة تمثّل المجموعات "‎set‎" نوعًا من الحاويات عناصرها مُرتّبة وغير مكرّرة، أمّا المجموعات المتعدّدة ‎multiset‎، فتشبه المجموعات العادية، لكن العناصر المتعددة تكون لها نفس القيمة. تغيير الترتيب الافتراضي لمجموعة ما لدى الصّنفين ‎set‎ و ‎multiset‎ توابع مقارنة افتراضية، ولكن قد تحتاج أحيانًا في بعض الحالات إلى زيادة تحميلها. فمثلًا، لنفترض أنّنا نخزّن سلاسل نصية في مجموعة ما، ونحن نعلم أن تلك السلاسل تحتوي على قيم رقمية فقط. يكون الترتيب الافتراضي قائمًا على المقارنة الأبجدية للسلاسل النصّية، وعليه فلن يتطابق الترتيب مع الترتيب الرقمي. وإن أردت ترتيبها ترتيبًا عدديًا فستحتاج إلى كائن دالّي (functor) لزيادة تحميل تابع الموازنة: #include <iostream> #include <set> #include <stdlib.h> struct custom_compare final { bool operator()(const std::string & left, const std::string & right) const { int nLeft = atoi(left.c_str()); int nRight = atoi(right.c_str()); return nLeft < nRight; } }; int main() { std::set<std::string> sut({"1", "2", "5", "23", "6", "290"}); std::cout << "### Default sort on std::set<std::string> :" << std::endl; for (auto && data: sut) std::cout << data << std::endl; std::set<std::string, custom_compare> sut_custom({"1", "2", "5", "23", "6", "290"}, custom_compare {}); std::cout << std::endl << "### Custom sort on set :" << std::endl; for (auto && data: sut_custom) std::cout << data << std::endl; auto compare_via_lambda = [](auto &&lhs, auto &&rhs){ return lhs > rhs; }; using set_via_lambda = std::set<std::string, decltype(compare_via_lambda)>; set_via_lambda sut_reverse_via_lambda({"1", "2", "5", "23", "6", "290"}, compare_via_lambda); std::cout << std::endl << "### Lambda sort on set :" << std::endl; for (auto && data: sut_reverse_via_lambda) std::cout << data << std::endl; return 0; } يكون الخرج ما يلي: ### Default sort on std::set<std::string> : 1 2 23 290 5 6 ### Custom sort on set : 1 2 5 6 23 290 ### Lambda sort on set : 6 5 290 23 2 1 في المثال أعلاه، يمكن استخدام ثلاث طرق مختلفة لإضافة عمليات مقارنة إلى المجموعات "‎std::set‎"، ولكلّ منها فوائدها. الترتيب الافتراضي يستخدم الترتيب الافتراضي عامل المقارنة الخاصّ بالمفتاح (الوسيط الأول للقالب)، وغالبًا ما يكون المفتاح إعدادًا افتراضيًا مناسبًا للدالّة ‎std::less<T>‎. وستستخدم هذه الدالة العامل ‎operator<‎ الخاص بالكائن ما لم تكن قد خُصِّصت، هذا مفيد خاصّة عندما تحاول شيفرة أخرى استخدام ترتيب معيّن، إذ يجعل الشيفرة متناسقة. ستؤدي كتابة الشيفرة بهذه الطريقة إلى تسهيل تحديثها عندما تكون تغييرات المفتاح جزءًا من واجهة برمجية (API)، فمثلًا إن كان لدينا صنف يحتوي على عضوين، وسيتغيّر إلى صنف يحتوي 3 أعضاء، فستُحدَّث جميع النُّسخ عبر تحديث ‎operator<‎ الخاص بالصنف. وكما تتوقع، فإن استخدام التصنيف الافتراضي كخيار افتراضي منطقي ومقبول. الترتيب المُخصّص يمكن إضافة ترتيب مُخصّص عبر كائن له عامل مقارنة عندما لا تكون المقارنة الافتراضية مناسبة، كما في المثال أعلاه إذ تشير السلاسل النصيّة إلى أعداد صحيحة. يمكن أيضًا استخدام الترتيب المُخصّص في حال كنت ترغب في مقارنة المؤشّرات (الذكية) استنادًا إلى الكائن الذي تشير إليه، أو في حال كنت تحتاج إلى قيود خاصّة في عملية المقارنة، كمقارنة الأزواج ‎std::pair‎ بقيمة العنصر الأول ‎first‎ فقط. يجب أن تحرص على أن يكون الترتيب مستقرًّا (stable sorting) عند إنشاء عامل مقارنة، أي أنّ نتيجة عامل المقارنة بعد الإدراج يجب ألا تتغيّر، وإلا فهذا يعني أنّ السلوك غير محدّد. لذلك احرص على ألّا يستخدم عامل المقارنة إلّا البيانات الثابتة (الأعضاء، الدوالّ الثابتة، …). وستصادف غالبًا -كما في المثال أعلاه- أصنافًا بدون عوامل مقارنة، وينتج عن هذا منشئاتٌ افتراضية ومنشئاتُ نسخ (copy constructors)، ويسمح لك المُنشئ الافتراضي بحذف النسخة في وقت الإنشاء، كما أنّ منشئ النسخ ضروريّ لأنّ المجموعة تأخذ نسخة من مُعامل المقارنة. الترتيب عبر تعابير لامدا تعابير لامدا هي طريقة مختصرة لكتابة الدوالّ، وتتيح لنا كتابة امل المقارنة في سطور قليلة مما يسهل قراءة الشيفرة الكلّية. ما يعيب استخدام تعابير لامدا هو أنّه سيكون لكلّ واحد منها نوع محدّد في وقت التصريف، لذلك سيكون التعبير ‎decltype(lambda)‎ مختلفًا في كل تُصرَّف نفس وحدة التصريف (ملف cpp) عند وجود أكثر من وحدة تصريف تكون مُدرجة في ملفات الترويسة، ولهذا يوصى باستخدام كائنات الدوالّ كعوامل مقارنة عند استخدامها داخل ملفات الترويسة. سترى هذا النوع من الإنشاء غالبًا عند استخدام مجموعة "‎std::set‎" ضمن النطاق المحلي للدالّة. في حين يُفضَّل استعمال كائن دالّة عند استخدامه كوسيط لدالّة أو كعضو في صنف. خيارات الترتيب الأخرى نظرًا لأنّ عامل المقارنة الخاصّ بالمجموعات ‎std::set‎ عبارة عن وسيط قالب، فيمكن استخدام جميع الكائنات القابلة للاستدعاء كعوامل مقارنة، وما الأمثلة أعلاه إلا حالات خاصّة وحسب، ولا توجد قيود على هذه الكائنات القابلة للاستدعاء إلا ما يلي: يجب أن تكون نسخة قابلة للإنشاء النّسخي (copy constructable) ويجب أن تكون قابلة للاستدعاء مع وسيطين من نوع المفتاح نفسه (التحويلات الضمنية مسموح بها رغم عدم استحسانها، لأنها قد تضرّ بالأداء). حذف قيم من مجموعة إذا كنت تريد إفراغ المجموعة أو المجموعة المتعدّدة من كل عناصرها، فيمكنك استخدام ‎clear‎: std::set < int > sut; sut.insert(10); sut.insert(15); sut.insert(22); sut.insert(3); sut.clear(); // يساوي 0 sut حجم ثم يمكنك استخدام التابع ‎erase‎ الذي يوفّر بعض الوظائف التي تشبه عملية الإدراج: std::set < int > sut; std::set < int > ::iterator it; sut.insert(10); sut.insert(15); sut.insert(22); sut.insert(3); sut.insert(30); sut.insert(33); sut.insert(45); // الحذف البسيط sut.erase(3); // استخدام مكرّر it = sut.find(22); sut.erase(it); // حذف مجال من القيم it = sut.find(33); sut.erase(it, sut.end()); std::cout << std::endl << "Set under test contains:" << std::endl; for (it = sut.begin(); it != sut.end(); ++it) { std::cout << * it << std::endl; } الخرج سيكون: Set under test contains: 10 15 30 تنطبق كلّ هذه التوابع أيضًا على المجموعات المتعدّدة ‎multiset‎، يرجى ملاحظة أنّه في حال طلبت حذف عنصر من مجموعة متعدّدة ‎multiset‎ وكان ذلك العنصر مُكرَّرًا، فستُحذَف جميع العناصر التي تساوي ذلك العنصر. إدراج قيم في مجموعة هناك ثلاث طرق لإدراج العناصر في المجموعات. إدراج بسيط للقيمة باستخدام التابع insert الذي يعيد زوجًا، ممّا يسمح للمُستدعي بالتحقق مما إذا كان الإدراج قد تمّ أم لا. يمكن الإدراج بإعطاء تلميح عن الموضع الذي ستُدرج فيه القيمة، والهدف من ذلك هو تحسين وقت الإدراج، لكن المشكلة أنّنا لا نعرف دائمًا الموضع الذي يجب أن تُدرج فيه القيمة. انتبه في هذه الحالة لأن طريقة إعطاء التلميح تختلف بحسب إصدارات المصرّفات. أخيرًا، يمكنك إدراج عدة قيم عن طريق إعطاء مؤشّر للبداية (مُضمّن) والنهاية (غير مُضمّن). #include <iostream> #include <set> int main() { std::set < int > sut; std::set < int > ::iterator it; std::pair < std::set < int > ::iterator, bool > ret; // إدراج بسيط sut.insert(7); sut.insert(5); sut.insert(12); ret = sut.insert(23); if (ret.second == true) std::cout << "# 23 has been inserted!" << std::endl; ret = sut.insert(23); // بما أنها مجموعة، والعدد 23 موجودا سلفا فيها، فستفشل عملية الإدراج if (ret.second == false) std::cout << "# 23 already present in set!" << std::endl; // إدراج مع تلميح لتسريع الأداء it = sut.end(); // وما بعده C++11 هذه الحالة محسَّنة في // بالنسبة للإصدارات السابقة، يمكن التأشير إلى العنصر الذي يسبق موضع الإدراج sut.insert(it, 30); // إدراج مجال من القيم std::set < int > sut2; sut2.insert(20); sut2.insert(30); sut2.insert(45); std::set < int > ::iterator itStart = sut2.begin(); std::set < int > ::iterator itEnd = sut2.end(); sut.insert(itStart, itEnd); // يُستثنى المكرر الثاني من الإدراج std::cout << std::endl << "Set under test contains:" << std::endl; for (it = sut.begin(); it != sut.end(); ++it) { std::cout << * it << std::endl; } return 0; } سينتج لنا الخرج التالي: # 23 has been inserted! # 23 already present in set! Set under test contains: 5 7 12 20 23 30 45 إدراج القيم في مجموعة متعددة جميع طرق الإدراج الخاصّة بالمجموعات تنطبق أيضًا على المجموعات المتعدّدة، لكن هناك خيار آخر، وهو تمرير قائمة تهيئة initializer_list: auto il = { 7, 5, 12 }; std::multiset < int > msut; msut.insert(il); البحث عن القيم في المجموعات والمجموعات المتعدّدة هناك عدّة طرق للبحث عن قيمة معيّنة في مجموعة ‎std::set‎ أو مجموعة متعدّدة ‎std::multiset‎، وللحصول على مُكرِّر يشير إلى موضع أوّل ظهور لمفتاح مُعيّن، يمكن استخدام الدالّة ‎find()‎ التي تعيد ‎end()‎ إذا لم يكن المفتاح موجودًا. std::set < int > sut; sut.insert(10); sut.insert(15); sut.insert(22); sut.insert(3); // 3, 10, 15, 22 auto itS = sut.find(10); // *itS == 10 القيمة موجودة، لذا itS = sut.find(555); // itS == sut.end() لم يُعثَر على القيمة، لذا std::multiset < int > msut; sut.insert(10); sut.insert(15); sut.insert(22); sut.insert(15); sut.insert(3); // 3, 10, 15, 15, 22 auto itMS = msut.find(10); الطريقة الأخرى هي استخدام الدالّة ‎count()‎، والتي تحسُب عدد القيم المطابقة التي عُثِر عليها في المجموعة أو المجموعة المتعدّدة (في حالة المجموعات، ستكون القيمة المُعادة إمّا 0 أو 1). باستخدام نفس القيم المذكورة أعلاه، سيكون لدينا: int result = sut.count(10); // 1 result = sut.count(555); // 0 result = msut.count(10); // 1 result = msut.count(15); // 2 في حالة المجموعات المتعدّدة، يمكن أن تكون هناك عدّة عناصر لها نفس القيمة، وللحصول على مجالٍ (range) يمثّل تلك العناصر يمكن استخدام الدالّة ‎equal_range()‎ التي تُعيد زوجًا يتألّف من مُكرّر الحدّ الأدنى (مُضمّن) ومُكرّر الحدّ الأعلى (غير مضمَن) على التوالي. وإذا لم يكن المفتاح موجودًا فسيشير كلا المُكرِّران إلى أقرب قيمة عليا وفق تابع المقارنة المُستخدم لترتيب المجموعة المتعدّدة المُعطاة. auto eqr = msut.equal_range(15); auto st = eqr.first; // '15' يشير إلى العنصر الأول auto en = eqr.second; // '22' يشير إلى العنصر eqr = msut.equal_range(9); // '10' يشيران إلى eqr.second و eqr.first كل من std::integer_sequence: تسلسلات الأعداد الصحيحة يمثّل قالب الصنف ‎std::integer_sequence<Type, Values...>‎ سلسلة من القيم العددية الصحيحة من نوع ‎Type‎، حيث ‎Type‎ هو أحد أنواع الأعداد الصحيحة المُضمّنة. تُستخدم هذه التسلسلات عند تنفيذ قوالب الأصناف أو الدوالّ التي تحتاج إلى الوصول الموضعي (positional access)، وتحتوي المكتبة القياسية أيضًا على أنواع مصنَعيّة (factory types) تنشئ تسلسلات تصاعدية من الأعداد الصحيحة انطلاقًا من عدد العناصر المُراد. تحويل صفّ std::tuple<T...>‎ إلى معاملات دالّة يمكن استخدام صفّ ‎std::tuple<T...>‎ لتمرير عدّة قيم إلى دالّة، فمثلًا يمكن استخدامه لتخزين سلسلة من المعاملات على شكل صف انتظار (queue)، ويجب تحويل عناصر هذه الصفوف عند معالجتها إلى وسائط استدعاء للدالة. #include <array> #include <iostream> #include <string> include <tuple> #include <utility> // ---------------------------------------------------------------------------- // الدوالّ المراد استدعاؤها void f(int i, std::string const & s) { std::cout << "f(" << i << ", " << s << ")\n"; } void f(int i, double d, std::string const & s) { std::cout << "f(" << i << ", " << d << ", " << s << ")\n"; } void f(char c, int i, double d, std::string const & s) { std::cout << "f(" << c << ", " << i << ", " << d << ", " << s << ")\n"; } void f(int i, int j, int k) { std::cout << "f(" << i << ", " << j << ", " << k << ")\n"; } // ---------------------------------------------------------------------------- // الدالّة الفعلية التي توسّع الصف template < typename Tuple, std::size_t...I > void process(Tuple const & tuple, std::index_sequence < I... > ) { f(std::get < I > (tuple)...); } // الواجهة المراد استدعاؤها، للأسف يجب أن تُرسل إلى دالّة أخرى لاستخلاص سلسلة الفهارس المُنشأة // std::make_index_sequence<N> من template < typename Tuple > void process(Tuple const & tuple) { process(tuple, std::make_index_sequence < std::tuple_size < Tuple > ::value > ()); } // ---------------------------------------------------------------------------- int main() { process(std::make_tuple(1, 3.14, std::string("foo"))); process(std::make_tuple('a', 2, 2.71, std::string("bar"))); process(std::make_pair(3, std::string("pair"))); process(std::array < int, 3 > { 1, 2, 3 }); } طالما كان الصنف يدعم ‎std::get<I>(object)‎ و ‎std::tuple_size<T>::value‎، فيمكن توسيعه باستخدام الدالّة ‎process()‎ أعلاه، إذ أنّ الدالّة نفسها مستقلّة تمامًا عن عدد الوسائط. إنشاء حزمة مُعاملات مُكوّنة من أعداد صحيحة تُستخدَم std::integer_sequence لتخزين سلسلة من الأعداد الصحيحة التي يمكن تحويلها إلى حُزمة معاملات، وفائدتها الرئيسيّة هو إمكانية إنشاء قوالب الأصناف المصنعيّة التي ستنشئ تلك التسلسلات: #include <iostream> #include <initializer_list> #include <utility> template < typename T, T...I > void print_sequence(std::integer_sequence < T, I... > ) { std::initializer_list < bool > { bool(std::cout << I << ' ')... }; std::cout << '\n'; } template < int Offset, typename T, T...I > void print_offset_sequence(std::integer_sequence < T, I... > ) { print_sequence(std::integer_sequence < T, T(I + Offset)... > ()); } int main() { // تحديد التسلسلات بشكل صريح print_sequence(std::integer_sequence < int, 1, 2, 3 > ()); print_sequence(std::integer_sequence < char, 'f', 'o', 'o' > ()); // توليد التسلسلات print_sequence(std::make_index_sequence < 10 > ()); print_sequence(std::make_integer_sequence < short, 10 > ()); print_offset_sequence < 'A' > (std::make_integer_sequence < char, 26 > ()); } يَستخدم قالب الدّالة ‎print_sequence()‎ قائمة التهيئة ‎std::initializer_list<bool>‎ عند توسيع تسلسل الأعداد الصحيحة لضمان ترتيب التقييم، وتجنّب إنشاء متغيّر [مصفوفة] غير مستخدم. تحويل سلسلة من الفهارس إلى نُسخ من عنصر ما يؤدي توسيع حزمة معاملات من الفهارس في تعبير فاصلة (comma expression) يحمل قيمةً ما، إلى إنشاء نسخة من القيمة المقابلة لكل فهرس. ويرى المُصرِّفان ‎gcc‎ و ‎clang‎ أنّ الفهرس ليس له أيّ تأثير، لذا يطلقان تحذيرًا بشأنه (يمكن إسكات ‎gcc‎ عبر تحويل الفهرس إلى قيمة فارغة ‎void‎): #include <algorithm> #include <array> #include <iostream> #include <iterator> #include <string> #include <utility> template < typename T, std::size_t...I > std::array < T, sizeof...(I) > make_array(T const & value, std::index_sequence < I... > ) { return std::array < T, sizeof...(I) > { (I, value)... }; } template < int N, typename T > std::array < T, N > make_array(T const & value) { return make_array(value, std::make_index_sequence < N > ()); } int main() { auto array = make_array < 20 > (std::string("value")); std::copy(array.begin(), array.end(), std::ostream_iterator < std::string > (std::cout, " ")); std::cout << "\n"; } هذا الدرس جزء من سلسلة مقالات عن C++‎. ترجمة -بتصرّف- للفصول 51 وحتى 60 من كتاب C++ Notes for Professionals