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

البحث في الموقع

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

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

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

نوع المحتوى


التصنيفات

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

التصنيفات

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

التصنيفات

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

التصنيفات

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

التصنيفات

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

التصنيفات

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

التصنيفات

  • الإنتاجية وسير العمل
    • مايكروسوفت أوفيس
    • ليبر أوفيس
    • جوجل درايف
    • شيربوينت
    • Evernote
    • Trello
  • تطبيقات الويب
    • ووردبريس
    • ماجنتو
    • بريستاشوب
    • أوبن كارت
    • دروبال
  • الترجمة بمساعدة الحاسوب
    • omegaT
    • memoQ
    • Trados
    • Memsource
  • برامج تخطيط موارد المؤسسات ERP
    • تطبيقات أودو odoo
  • أنظمة تشغيل الحواسيب والهواتف
    • ويندوز
    • لينكس
  • مقالات عامة

التصنيفات

  • آخر التحديثات

أسئلة وأجوبة

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

التصنيفات

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

ابحث في

ابحث عن


تاريخ الإنشاء

  • بداية

    نهاية


آخر تحديث

  • بداية

    نهاية


رشح النتائج حسب

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

  • بداية

    نهاية


المجموعة


النبذة الشخصية

  1. مرحبا بالعالم يطبع البرنامج التالي العبارة مرحبًا بالعالم! في مجرى الخرج القياسي (standard output stream): #include <iostream> int main() { std::cout << "مرحبا بالعالم!" << std::endl; } يمكنك رؤية التجربة الحية على Coliru. تحليل البرنامج لنحلل كل جزء من شيفرة البرنامج بالتفصيل: #include <iostream> هو موجّه معالجة مسبقة (preprocessor directive)، ويتضمن محتوى iostream، وهي ترويسة ملف C++‎ القياسي (standard C++ header file). الكلمة iostream هي ترويسة ملف مكتبة قياسية (standard library header file)، وتحتوي على تعريفات مُجريَا الدخل والخرج القياسيين (standard input and output streams). هذه التعريفات مُتضمنة في فضاء الاسم std، كما هو موضح أدناه. يوفر مُجريا الدخل/الخرج القياسيين (I/O) طريقة للبرامج يمكنه عبرها جلب مدخلات من مخرجات خاصة بنظام خارجي، والذي يكون طرفية في العادة. int main() { ... } تُعرِّف هذه الشيفرة دالة جديدة باسم main تستدعى عند تنفيذ البرنامج. يجب أن تكون هناك دالة main واحدة لكل برنامج C++‎، ويجب أن تعيد دائمًا عددًا من النوع int. يمثل int نوع القيمة التي تعيدها الدالة والتي تمثل رمز الخروج (exit code) الخاص بالدالة main. البرامج ذات رمز الخروج المساوي للقيمة 0 أو EXIT_SUCCESS تُعد ناجحة من قبل النظام الذي ينفّذ ذلك البرنامج. ترتبط كل رموز الخروج الأخرى بأخطاء تحددها هي. في حال عدم استخدام التعليمة return، ستعيد الدالة main (وبالتالي البرنامج نفسه) القيمة 0 افتراضيًّا. في هذا المثال لا نحتاج إلى كتابة ;return 0. جميع الدوال الأخرى، باستثناء تلك التي تُعيد النوع void، يجب أن تُعيد قيمة بشكل صريح من النوع الذي يُفترض أن تعيده، أو لا ينبغي أن تعيد أي قيمة على الإطلاق. std::cout << "مرحبا بالعالم!" << std::endl; تطبع هذه التعليمة السلسلة النصية "مرحبا بالعالم!" في مجرى الخرج القياسي، وتتألف من الأجزاء التالية: std: هي فضاء اسم، و :: هو عامل تحليل النطاق (scope resolution operator) الذي يسمح بالبحث عن الكائنات بأسمائها ضمن فضاء الاسم. هناك العديد من فضاءات الاسم. هنا نستخدم :: لإظهار أننا نريد استخدام cout من فضاء الاسم std. لمزيد من المعلومات، ارجع إلى المقال Scope Resolution Operator في توثيق ميكروسوفت. std::cout: هو كائن مجرى الخرج القياسي (standard output)، وهو مُعرّف في iostream، ويُطبع أيّ شيء يُمرَّر إليه في مجرى الخرج القياسي (stdout). >>: هي، في هذا السياق، عامل الإدراج (stream insertion operator)، ويُسمى كذلك لأنه يدرج كائنًا في كائن المجرى (stream object). تُعرِّف المكتبة القياسية العامل >> لإدراج بعض أنواع البيانات في مجاري الخرج. تدرج التعليمة stream << content المحتوى content في المجرى stream ثم تعيد المجرى نفسه بعد تحديثه. يسمح ذلك بإجراء الإدراجات المتسلسلة، مثلًا، تطبع التعليمة std::cout << "Foo" << " Bar";‎ السلسة النصية "Foo Bar" في سطر الأوامر. "مرحبا بالعالم!": هي سلسلة نصية حرفية (character string literal). عامل الإدراج الخاص بالسلاسل النصية مُعرّف في الملف iostream. std::endl: هو كائن خاص لمعالجة مجرى I/O، وهو مُعرّف في الملف iostream. إنّ إدراج معالج في مجرى ما يغير حالة ذلك المجرى. يقوم معالج المجرى std::endl بعملين: أولًا، يدرج محرف نهاية السطر (end-of-line character)، ثم ينقل البيانات (flushes) الموجودة في المخزن المؤقت (stream buffer) الخاص بالمجرى لجعل النص يظهر في سطر الأوامر. يضمن هذا أنّ البيانات المُدرجة في المجرى ستظهر بالفعل في وحدة التحكم. (تُخزنّ بيانات المجرى عادة في مخزن مؤقت، ثم تُنقَل على دفعات، إلا إذا أمرت بنقلها فوريًا.) هناك طريقة بديلة تُجنِّب نقل البيانات من المخزن المؤقت، وهي: std::cout << "Hello World!\n"; // سطرًا جديدًا \n يمثل الفاصلة المنقوطة ;: تُخطر الفاصلة المنقوطة (;) المُصرّف (compiler) بأنّ التعليمة البرمجية قد انتهت. تتطلب كل عبارات C++‎ وتعريفات الأصناف استخدام فاصلة منقوطة في النهاية. التعليقات التعليقات هي جزء من الشيفرة يتجاهلها مصرف C++‎. تُستخدم التعليقات لتوضيح بعض الجوانب التي قد تكون غامضة بخصوص تصميم أو طريقة عمل البرنامج. هناك نوعان من التعليقات في C++‎: التعليقات السطرية (Single-Line Comments) تجعل الشرطتان المائِلتان // النص الذي يليها وحتى بداية السطر الجديد تعليقًا: int main() { // هذا تعليق سطري int a; // هذا تعليق سطري كذلك int i; // هذا أيضا تعليق سطري } التعليقات الكُتلية (Block Comments) يُستخدَم المحرفان ‎/*‎ للإعلان عن بداية تعليق كتلي، فيما يُستخدَم المحرفان ‎*/‎ للإعلان عن نهاية التعليق. يُفسَّر النص الموجود بين العبارتين على أنه تعليق، حتى لو كان النص الموجود بينهما شيفرة C++‎ صالحة. يسمى هذا النوع من التعليقات عادة تعليقات "C-style"، لأنّ صياغة هذ النوع من التعليقات موروثة من سلف C++‎، أي اللغة C: int main() { /* * هذا تعليق كتلي */ int a; } يمكنك كتابة ما تشاء في التعليقات الكتلية لكن عندما يجد المصرّف رمز نهاية التعليق‎*/‎، فإنه ينهي التعليق الكتلي: int main() { /* تعليق كتلي يمتد * على عدة أسطر * وينتهي في السطر التالي */ int a; } يمكن أيضًا أن تبدأ التعليقاتُ الكتلية وتنتهي في سطر واحد. مثلا: void SomeFunction(/* الوسيط الأول */ int a, /* الوسيط الثاني */ int b); أهمية التعليقات كما هو الحال مع جميع لغات البرمجة، توفر التعليقات العديد من الفوائد، منها: توثيق ضمني للشيفرة البرمجية لتسهيل قراءتها وصيانتها شرح الغرض من الشيفرة البرمجية ودوالها توفير تفاصيل حول تاريخ الشيفرة أو المنطق وراءها وضع حقوق الطبع والنشر/التراخيص، أو ملاحظات حول المشروع، أو شكر خاص، أو التنويه بالمساهمين، وما إلى ذلك مباشرة في الشيفرة المصدرية (source code). من جهة أخرى، فإنّ للتعليقات جانبًا سلبيًا كذلك: يجب تعديلها لتعكس أي تغييرات في الشيفرة الإفراط في إدراج التعليقات قد يؤثر سلبًا على مقروئية الشيفرة. يمكن تقليل الحاجة إلى التعليقات عبر كتابة شيفرة واضحة ومُوثقة ذاتيًا (self-documenting). أحد الأمثلة على ذلك هو استخدام أسماء توضيحية للمُتغيرات والدوال والأنواع. وتوزيع المهام المترابطة منطقيًا في دوال منفصلة. تعليق الشيفرة واختبارها أثناء التطوير، يمكن أيضًا استخدام التعليقات لتعطيل أجزاء من الشيفرة بسرعة دون حذفها. غالبًا ما يكون هذا مفيدًا في مرحلة الاختبار أو التنقيح (debugging)، ولكن ينبغي محو تلك التعليقات بعد الانتهاء. يشار إلى هذا غالبًا باسم "تعليق الشيفرة" (commenting out). بالمثل، الاحتفاظ بالإصدارات القديمة من أجزاء من الشيفرة في التعليقات لجعلها متاحة للمراجعة قد يكون مزعجًا، لأنّ ذلك يُراكم الكثير من الشيفرة غير المستخدمة، ولا يضيف قيمة تذكر موازنةً بالاطلاع على تاريخ الشيفرة عبر نظام إصدارات (versioning system). عملية التصريف القياسية في C++‎‎ تُنتَج برامج C++‎ القابلة للتنفيذ عادة بواسطة المُصرّف (compiler)، وهو برنامج يترجم الشيفرة من لغة برمجة إلى شيفرة تنفيذية تفهمها الآلة وتُنفذ على الحاسوب من قبل المستخدم النهائي. استخدام المصرّف لتصريف الشيفرة يسمى عملية التصريف (compilation process‎). ورثت C++‎ آلية التصريف من سلفها، أي اللغة C. فيما يلي قائمة توضح الخطوات الرئيسية الأربع للتصريف في C++‎: ينسخ المعالج الأولي (preprocessor) للغة C++‎ محتويات كل ملفات الترويسات (header files) المتضمنة في ملف الشيفرة المصدري، ويولد «شيفرة عملية بدل» (macro code، ويعوّض الثوابت الرمزية (symbolic constants) المعرفّة باستخدام ‎#define‎ بقِيمهما. تُصرَّف الشيفرة المصدرية الموسعة التي أُنتِجت بواسطة المعالج الأولي لـ C++‎ إلى لغة التجميع (assembly language) المناسبة لنظام التشغيل. تُصرَّف الشيفرة المجمّعة التي أُنتِجت بواسطة المصرّف إلى تعليمات مُصرّفة (object code) مناسبة لنظام التشغيل. تُربَط التعليمات المُصرَّفة المُولّدة من قبل المجمّع (assembler) مع ملفات التعليمات المُصرَّفة (object code files) الخاصة بدوال المكتبات المستخدمة لإنتاج الملف القابل للتنفيذ. ملاحظة: أحيانًا تُربط بعض الشيفرات المُصرَّفة معًا، ولكن ليس لغرض إنشاء برنامج نهائي، إذ يمكن تحزيم (packaging) الشيفرة "المربوطة" لأجل استخدامها من قبل برامج أخرى. الحزم الناتجة هي ما يشير إليه مبرمجو C++‎ بالمكتبات (libraries). تدمج العديد من مصرّفات C++‎ أو تفكّك بعض مراحل عملية التصريف لتسهيل العملية، أو لأجل التحليل الإضافي. يستخدم مُبرمجو C++‎ أدوات مختلفة، لكنها جميعًا تتبع المقاربة أعلاه. الدوال الدالة هي كتلة من الشيفرة تحتوي سلسلة من العبارات. يمكن للدوال قبول وسائط (arguments) أو قيم، ويمكن أن تعيد قيمة واحدة، أو قد لا تعيد أي قيمة. لاستخدام دالة، يجب استدعاؤها مع تمرير وسائط إليها، ثم تعيد قيمة. لكل دالة بصمة نوعيّة (type signature)، والتي تمثل أنواع وسائطها، ونوع القيمة المعادة. الدوال مستوحاة من مفهومَي الإجراء (procedure) والدالة في الرياضيات. ملاحظات: دوال C++‎ هي إجراءات (procedures) بالأساس، ولا تتبع بدقة قواعد الدوال الرياضية وفق مفهوم علوم الرياضيات. تؤدي الدوال في العادة مهام محددة ويمكن استدعاؤها من أجزاء أخرى من البرنامج. كما يجب التصريح عن الدالة وتعرِيفها قبل استدعائها في البرنامج. قد تُخفى تعريفات الدوال الشائعة والمهمة في ملفات أخرى مُضمّنة، فذلك يسهِّل إعادة استخدامها في البرامج. وهذا أحد الاستخدامات الشائعة لملفات الترويسات (header files). التصريح عن دالة التصريح عن دالة (Function Declaration) هو عملية الإعلان عن وجود دالة، مع توضيح اسمها وبصمتها النوعيّة للمصرف. ويتبع الصياغة التالية: // الدالة التالية تقبل عددًا صحيحًا وتعيد عددًا صحيحًا int add2(int i); في المثال أعلاه، تخبر int add2(int i)‎ المصرّف بالمعلومات التالية عن الدالة المُصرَّح عنها: نوع القيمة المعادة هو int. اسم الدالة هو add2. عدد وسائط الدالة هو 1: الوسيط الأول من النوع int. سيُشار إلى الوسيط الأول داخل الدالة بالاسم i. اسم الوسيط اختياري؛ إذ يمكن التصريح بالدالة كما يلي: int add2(int); // يمكن حذف اسم الوسيط وفقًا لقاعدة التعريف الواحد (one-definition rule)، لا يمكن التصريح بدالة ذات بصمة (signature) نوعيّة معينة أو تعريفها أكثر من مرة واحدة في كامل شيفرة C++‎ المرئية للمصرّف. بمعنى آخر، لا يمكن إعادة تعريف دالة ذات بصمة نوعية معيّنة إلا مرة واحدة فقط. وبالتالي، فالشيفرة التالية غير صالحة في C++‎ وسيطلق المصرف خطأ عند تنفيذها: // int هي دالة من النوع add2 المصرف سيعلم أن int add2(int i); // (int) -> int // لا يمكن إعادة تعريف نفس الدالة بنفس البصمة int add2(int j); // (int) -> int إذا لم تُعِد الدالة أي قيمة، فإنّ نوع القيمة المعادة سيُكتب void. إذا لم تأخذ الدالة أيّ معاملات، فينبغي أن تكون قائمة المعاملات فارغة. // الدالة التالية لا تأخذ أي وسائط، ولا تعيد أي قيمة void do_something(); لاحظ أنه ما يزال بمقدور الدالة do_something التأثير في المتغيرات التي يمكنها الوصول إليها. استدعاء الدالة يمكن استدعاء الدالة بعد التصريح عنها. على سبيل المثال، يستدعي البرنامج التالي الدالة add2 مع القيمة 2 داخل الدالة main: #include <iostream> int add2(int i); // add2 تعريف الدالة int main() { // في هذا الموضع add2(2) سيتم تقييم std::cout << add2(2) << "\n"; // وستُطبع النتيجة return 0; } بالنظر إلى الشيفرة السابقة، نجد أنَّه ما تزال الدالة add2 تحتاج إلى توفير طريقة تنفيذ لها بالإضافة إلى تعريفها (جسم الدالة)، رغم أن تنفيذ الدالة لا يظهر مباشرة في الشيفرة، إذ يمكن جلبه من ملف آخر بربط هذا الملف مع الشيفرة التي تستدعي الدالة. تمثل add2(2)‎ صياغة استدعاء دالة. تعريف الدالة تعريف الدالة يشبه التصريح عنها، إلا أنه يحتوي أيضًا على الشيفرة الذي ستُنفّذ عند استدعاء الدالة، هذه الشيفرة تُسمى جسم الدالة (function body). فيما يلي مثال عن تعريف الدالة add2: // i القيمة التي ستُمرر إلى الدالة سيشار إليها بالاسم int add2(int i) { // بين القوسين يمثل داخل نطاق أو جسم الدالة int j = i + 2; return j; } زيادة تحميل الدوال يمكنك إنشاء عدة دوال تشترك في نفس الاسم، ولكن مع اختلاف المعاملات، أي تختلف بصمة الدالة فقط، وهذا ما يعرف «بزيادة تحميل دالة» (Function Overloading). // التنفيذ الأول int add2(int i) { int j = i + 2; return j; } // التنفيذ الثاني int add2(int i, int j) { int k = i + j + 2; return k; // } كلتا الدالتين تحملان الاسم add2، بيْد أنّ الدالة المُنفّذة تعتمد على عدد وأنواع المعاملات المُمررة في الاستدعاء. في معظم الحالات، يمكن لمصرّف C++‎ أن يحدد الدالة المراد استدعاؤها. لكن في بعض الحالات يجب ذكر النوع بوضوح. فسيتم تقييم الشيفرة المحتواة في التعريف الأول عند استدعاء الدالة نع معامل واحد بالشكل add2(1)‎. وفي حال استدعاء الدالة مع معاملين بالشكل add2(1, 3)‎، فسيتم تقييم الشيفرة المتضمنة في التعريف الثاني بدلًا من التعريف الأول. المعاملات الافتراضية يمكن تحديد القيم الافتراضية لمعاملات الدالة (Default Parameters) في تعريفات الدوال فقط. // 7 هي b القيمة الافتراضية للمعامل int multiply(int a, int b = 7); // جسم الدالة int multiply(int a, int b) { return a * b; } في هذا المثال، يمكن استدعاء multiply()‎ مع معامل واحد أو مُعاملين. في حال تمرير معامل واحدة فقط، فستكون القيمة الافتراضية للمعامل b هي 7 وسيتم ضرب القيمة الممررة تلك بالمعامل الافتراضي 7 آنذاك. يجب وضع المعاملات الافتراضية في الأخير على النحو التالي: // تعريف صحيح int multiply(int a = 10, int b = 20); // مُقدّمة int a تعريف غير صحيح لأن int multiply(int a = 10, int b); استدعاءات خاصة للدوال - العوامل توجد استدعاءات خاصة في C++‎ للدوال، وهي ذات صياغة مختلفة عن الصياغة التقليدية name_of_function(value1, value2, value3)‎. المثال الأكثر شيوعا هي العوامل (operators). وهي تسلسلات أحرف خاصة تحوَّل إلى استدعاءات دوال من طرف المصرّف، من أمثلة ذلك، ! و + و - و * و % و ‎<<‎ وغيرها. عادةً ما ترتبط هذه المحارف الخاصة باستخدامات غير برمجية، أو تُستخدم للتبسيط (على سبيل المثال، المحرف + يمثل عادةً مفهوم الإضافة). تعالج C++‎ هذه التسلسلات؛ وتحوّل كل عامل إلى استدعاء الدالة المقابلة. على سبيل المثال، التعبير‎ التالي: 3+3 يكافئ استدعاء الدالة التالية: operator+(3, 3) تبدأ جميع أسماء دوال العوامل بـ operator. في لغة C، لا يمكن تعيين معان جديدة لأسماء دوال العوامل عبر كتابة تعريفات إضافية ذات بصمات نوعية مختلفة، في C++‎، هذا جائز. يشار إلى ربط تعريفات إضافية بنفس اسم الدالة باسم التحميل الزائد للعامل (operator overloading) في C++‎، وهو اصطلاح شائع نسبيًا، ولكنه غير عام في C++‎. التصريح عن الدوال وقواعد مرئيتها في C++‎، يجب التصريح عن الشيفرة أو تعريفها قبل استخدامها. على سبيل المثال، ينتج عن الشيفرة التالية خطأ في وقت التصريف: int main() { foo(2); // (*) } void foo(int x) {} سيطلق خطأ، لأن الدالة foo استُدعِيت في السطر (*) قبل أن تُعرّف، إذ لا ترى الدالة main هذا التعريف المتأخر للدالة foo بعد استدعائها ضمنها. هناك طريقتان لحل هذه المشكلة: إما التصريح عن foo()‎ أو تعريفها قبل استخدامها في main()‎. هذا مثال على ذلك: // ووضع جسمها أولا foo التصريح عن الدالة void foo(int x) {} int main() { // الآن foo يمكن استدعاء الدالة foo(2); } من الممكن أيضًا التصريح المسبق (forward-declaration) عن الدالة عن طريق وضع تصريح عن قالبها (prototype declaration) قبل موضع استدعائها والذي يحدد نوع القيمة المعادة واسم الدالة وعدد وسائطها وأنواعها، ثم تعريف جسمها لاحقًا: // foo تصريح عن قالب لدالة باسم void foo(int); int main() { foo(2); // (*) } // ينبغي أن تطابق تصريح القالب أعلاه void foo(int x) { // هنا foo تعريف جسم } أصبحت الدالة foo مرئية، لذا يمكن استدعاؤها الآن ضمن الدالة main في السطر (*) رغم أنها لم تُعرّف بعد. يجب أن يحدد تصريح النموذج نوع القيمة المعادة (void)، واسم الدالة (foo)، وأنواع المتغيرات في قائمة الوسائط (int)، لكنّ أسماء الوسائط غير ضرورية. يمكن وضع تصريحات قوالب الدوال في ترويسة الملف: // foo.h الملف // تصريح عن قالب دالة void foo(int); ثم تقديم التعريف الكامل في موضع آخر: // foo.cpp --> foo.o // foo تضمين الملف الذي يحوي قالب الدالة #include "foo.h" // foo تعريف جسم الدالة void foo(int x) { } ثم، بمجرد تصريف الشيفرة، يمكن ربط كائن الملف (object file) المقابل foo.o بالكائن المصرّف حيث يتم استخدامه في مرحلة الربط، main.o: // main.cpp --> main.o // foo تصريح عن قالب الدالة #include "foo.h" int main() { foo(2); } // (*) استدعاء الدالة foo ممكن هنا، لأنه سبق التصريح عنها، وتعريف قالب ومتن الدالة foo مربوط عبر كائنات الملفات (object files). يحدث الخطأ "رمز خارجي غير محلول" (unresolved external symbol) إذا صادف المصرّف تصريحَ قالب واستدعاء لدالة، دون وجود لجسمها (body). قد يكون حل هذه الأخطاء معقدًا، لأنّ المصرّف لن يبلِّغ عن الخطأ حتى مرحلة الربط النهائية، ولن يعرف السطر الذي يجب الانتقال إليه في الشيفرة لإظهار الخطأ. المعالج الأولي المعالج الأولي (Preprocessor) هو جزء مهم من المصرّف إذ يقوم بتعديل الشيفرة المصدرية، وحذف بعض البتات، أو تغييرها، وإضافة أشياء أخرى. في الملفات المصدرية، يمكننا تضمين مُوجِّهات (directives) للمعالج الأولي. تخبر تلك الموجِّهات المعالج الأولي بتنفيذ إجراءات محددة. يبدأ الموجّه بالرمز # في سطر جديد مثل: #define ZERO 0 من أشهر مُوجهات المعالج الأولي، الموجهات التالية: #include <something> يأخذ هذا الموجه something ويُدرجه في ملفك حيث يظهر الموجه. مثلا، يبدأ برنامج "مرحبا بالعالم!" بالسطر التالي: #include <iostream> يضيف هذا السطر الدوال والكائنات التي تتيح لك استخدام المدخلات والمخرجات القياسية. لا تحتوي اللغة C (والتي تستخدم أيضًا المُعالج الأولي) نفس القدر من ترويسات الملفات كخَلَفها C++‎، كما أنه يمكنك في C++‎ استخدام جميع ترويسات C. المُوجِّه الثاني في الأهمية هو على الأرجح الموجّه التالي: #define something something_else يخبر هذا المُوجِّه المعالج الأولي بأنه يجب أن يبدِّل something مكان كل ظهور لـ something_else في الملف الحالي. يمكن أن يجعل هذا الموجّه بعض الأشياء تتصرف مثل دوال، لكنه مفهوم متقدم أشرنا إليه قبل قليل على أنه «شيفرة عملية بدل» (macro code)، ولن نتطرق إليه الآن. something_else ليس ضروريًا، ولكن في حال عدم إضافة something، سيُحذَف كل ظهور لـ something. هذا مفيد جدا، إذ يمكن استخدامه مع المُوجِّهات ‎#if و ‎#else و ‎#ifdef. وفق الصياغة التالية: #if something == true // شيفرة #else // شيفرة أخرى #endif #ifdef thing_that_you_want_to_know_if_is_defined // شيفرة #endif تدرِج هذه الموجِّهات الشيفرة الموجودة في البت الصحيح، وتحذف الآخرين. يمكن استخدام هذا لتضمين أجزاء من الشيفرة حصرًا في أنظمة تشغيل معينة دون الحاجة إلى إعادة كتابة الشيفرة بالكامل. هذه المقالة جزء من سلسلة مقالات عن C++‎. ترجمة -وبتصرّف- للفصل Chapter 1: Getting started with C++‎ من الكتاب C++ Notes for Professionals اقرأ أيضًا الدرس 2: القيم مصنَّفة النوع
  2. إمساك الاستثناءات 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; } إذا كنت متأكدًا من أنك لن تفعل أيّ شيء يؤدي إلى تغيير الاستثناء (مثل إضافة معلومات أو تعديل رسالة الخطأ المرفقة بالاستثناء)، فإنّّ إمساك الاستثناء بمرجع ثابت سيسمح للمٌصرّف بإجراء بعض التحسينات، ولكن هذا لن يلغي بالضرورة الحاجة إلى تشريح الكائن (كما هو مُوضّح في المثال أعلاه). الاستثناءات المخصصة ينبغي ألّا ترفع قيمًا خامًا 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
  3. تنفذ الحلقات التكرارية مجموعة من التعليمات إلى حين استيفاء شرط معين، وهناك ثلاثة أنواع من تلك الحلقات التكرارية في لغة C++: for و while و do…while. حلقة for النطاقية (Range-Based For) الإصدار ≥ C++‎ 11 يمكن استخدام حلقات for للتكرار على عناصر نطاق تكراري (iterator-based range) دون الحاجة إلى استخدام الفهارس العددية أو الوصول بشكل مباشر إلى المكررات: vector < float > v = { 0.4 f, 12.5 f, 16.234 f }; for (auto val: v) { std::cout << val << " "; } std::cout << std::endl; ستكرّر الشيفرة السابقة تعليمة std::cout << val << " ";‎ على كل عناصر v، وستحصل val على قيمة العنصر الحالي. الشيفرة التالية: for (for-range-declaration : for-range-initializer ) statement ستكون مكافئة لما يلي: { auto&& __range = for-range-initializer; auto __begin = begin-expr, __end = end-expr; for (; __begin != __end; ++__begin) { for-range-declaration = *__begin; statement } } الإصدار ≥ C++‎ 17 من الممكن أن تكون end من نوع مختلف عن begin في C++17، انظر: { auto&& __range = for-range-initializer; auto __begin = begin-expr; auto __end = end-expr; for (; __begin != __end; ++__begin) { for-range-declaration = *__begin; statement } } قُدِّم هذا التغيير في C++‎ لأجل التمهيد لدعم معيار النطاقات Ranges TS في الإصدار C++‎ 20، وعليه فإن الحلقة في هذه الحالة ستكون مكافئة لما يلي: { auto &&__range = v; auto __begin = v.begin(), __end = v.end(); for (; __begin != __end; ++__begin) { auto val = *__begin; std::cout << val << " "; } } لاحظ أن التعليمة auto val تصرح عن نوع القيمة التي ستكون نسخة من القيمة المخزنة داخل النطاق – سنهيئه استنساخًا أثناء تنفيذ البرنامج-، وإن كانت عملية نسخ القيم المخزنة في النطاق مكلفة فربما تود استخدام const auto &val. كذلك لست مضطرًا لاستخدام auto ، بل تستطيع استخدام أي اسم نوع typename طالما أنه قابل للتحويل من نوع قيمة النطاق. واعلم أن حلقة for النطاقية غير مناسبة في حال احتجت للوصول إلى المُكرّر، أو ستحتاج مجهودًا كبيرًا. إن أردت الإشارة للمكرِّر، فيمكنك ذلك بما يلي: vector < float >v = {0.4f, 12.5f, 16.234f}; for(float &val: v) { std::cout << val << " "; } كذلك يمكنك التكرار على مرجع const إذا كان لديك حاوية const: const vector<float> v = {0.4f, 12.5f, 16.234f}; for(const float &val: v) { std::cout << val << " "; } يمكنك استخدام مراجع إعادة التوجيه (forwarding references) في حال أعاد مُكرّر التسلسل كائنًا وكيلًا (proxy object) وكنت بحاجة إلى إجراء عمليات يمكن أن تغيّر قيمة الكائن. لكن بالمقابل، من المحتمل أن تربك من يقرأ شيفرتك إن استخدمت هذه الطريقة. vector<bool> v(10); for (auto &&val : v) { val = true; } يمكن أن يكون نوع "النطاق" المقدم إلى حلقة for النطاقية أحد الخيارات التالية: المصفوفات: float arr[] = {0.4f, 12.5f, 16.234f}; for (auto val : arr) { std::cout << val << " "; } لاحظ أنّ تخصيص المصفوفات الديناميكية لا يُحسب: float *arr = new float[3]{0.4f, 12.5f, 16.234f}; for (auto val : arr) // خطأ تصريفي { std::cout << val << " "; } أي نوع يحتوي على دوال begin()‎ و end()‎ تابعةٍ، يعيدُ مكرّرات إلى عناصر من ذلك النوع، ويمكنك استخدام حاويات المكتبات القياسية إضافة إلى استخدام الأنواع التي يحددها المستخدم (User-Defined)، انظر: struct Rng { float arr[3]; // المؤشرات ما هي إلا مكرّرات const float * begin() const { return &arr[0]; } const float * end() const { return &arr[3]; } float * begin() { return &arr[0]; } float * end() { return &arr[3]; } }; int main() { Rng rng = { { 0.4f, 12.5f, 16.234f } }; for (auto val: rng) { std::cout << val << " "; } } أيّ نوع لديه دوال begin(type)‎ و end(type)‎ غير تابعة (non-member)، يمكن إيجاده من خلال البحث بالوسائط (Arguments) استنادًا للنوع type، هذا مفيد في إنشاء نوع نطاقٍ (range type) دون الحاجة إلى تعديل نوع الصنف نفسه، انظر: namespace Mine { struct Rng { float arr[3]; }; // المؤشرات ما هي إلا مكررات const float * begin(const Rng & rng) { return &rng.arr[0]; } const float * end(const Rng & rng) { return &rng.arr[3]; } float * begin(Rng & rng) { return &rng.arr[0]; } float * end(Rng & rng) { return &rng.arr[3]; } } int main() { Mine::Rng rng = { { 0.4f, 12.5f, 16.234f } }; for (auto val: rng) { std::cout << val << " "; } } حلقة for التكرارية تنفّذ الحلقة for التعليمات الموجودة في متن الحلقة loop body ما دامَ شَرط الحلقة condition صحيحًا، وتُنفَّذ initialization statement مرة واحدة قبل تنفيذ الحلقة التكرارية، ثم تُنفّذ التعليمة iteration execution بعد كل تكرار. تٌعرَّف حلقة for كما يلي: for (/*initialization statement*/; /*condition*/; /*iteration execution*/) { // متن الحلقة } شرح الصيغة السابقة: تُنفَّذ تعليمة initialization statement مرة واحدة فقط في بداية حلقة for، وتستطيع هنا أن تصرِّح عن عدة متغيرات من نفس النوع، مثل int i = 0, a = 2, b = 3. لا تكون تلك المتغيرات صالحة إلا في نطاق الحلقة، وأما المتغيرات التي لها نفس الاسم وعُرِّفّت قبل تنفيذ الحلقة فإنها تُخفى أثناء تنفيذ الحلقة. تُقيَّم تعليمة condition قبل كل مرة يُنفَّذ فيها متن الحلقة (loop body) كي تُوقف الحلقة إن أعادت القيمة false. تُنفَّذ تعليمة iteration execution بعد تنفيذ متن الحلقة وقبل تقييم الشرط التالي إلا إن أُوقِفت حلقة for في المتن بواسطة break أو goto أو return، أو في حالة رفع اعتراض (throwing an exception). تستطيع وضع عدة تعليمات في الجزء iteration execution مثل a++, b+=10, c=b+a. تكافئ الشيفرةُ التالية حلقةَ for في حال كتابتها كحلقة while، حيث تنتقل الحلقة إلى جزء تنفيذ التكرار /*iteration execution*/ عند استخدام continue: /*initialization*/ while ( /*condition*/ ) { // متن الحلقة /*iteration execution*/ } غالبًا ما تُستخدم الحلقة for لتنفيذ تعليمات برمجية معيّنة عددًا محدّدًا من المرات. على سبيل المثال: for (int i = 0; i < 10; i++) { std::cout << i << std::endl; } أو: for (int a = 0, b = 10, c = 20; (a + b + c < 100); c--, b++, a += c) { std::cout << a << " " << b << " " << c << std::endl; } هذا مثال يوضّح إخفاء المتغيرات المُصرّح عنها قبل الحلقة، حيث نصرح في السطر الثاني عن المتغير i الذي ستتغير قيمته بين 0 و 9 أثناء تنفيذ الحلقة، ثم نستطيع يعود إلى قيمته الأصلية 99 بعد انتهاء تنفيذها. int i = 99; //i = 99 for (int i = 0; i < 10; i++) { // i التصريح عن متغير جديد } ولكن إن كنت تريد استخدام المتغيرات المُصرّحة سلفًا وعدم إخفائها، فاحذف التصريح. انظر المثال التالي إذ نستخدم متغير i المصرَّح عنه من قبل، والذي تتغير قيمته بين 0 و 9 أثناء تنفيذ الحلقة، لكن هذه المرة ستكون قيمته بعد تنفيذها 10. int i = 99; //i = 99 for (i = 0; i < 10; i++) { // i سنستخدم المتغير المُعرّف مسبقا } ملاحظات: يمكن لتعليمات التهيئة (initialization) والزيادة (increment) إجراء عمليات لا تتعلق بتعليمة شرط الحلقة، بل قد تكون فارغة تمامًا إن شئت ذلك، لكن من الأفضل أن نجري عمليات لها علاقة مباشرة بالحلقة لجعل الشيفرة أسهل في القراءة والفهم. لا يُرى المتغير الذي صُرِّح عنه في تعليمة التهيئة إلا داخل نطاق حلقة for، ويُحذف بمجرد إنهائها. لا تنس أنّ المتغير الذي تم التصريح عنها في initialization statement يمكن أثناء الحلقة، وكذلك المتغير المُحدّد في الشرط condition. انظر المثال التالي لحلقة تَعُدُّ من 0 إلى 10: for (int counter = 0; counter <= 10; ++counter) { std::cout << counter << '\n'; } // counter لا يمكن الوصول هنا إلى شرح الشيفرة السابقة: التعليمة int counter = 0 تهيّئ المتغير counter عند القيمة 0. لا يمكن استخدام هذا المتغير إلا داخل حلقة for. التعليمة ‎counter <= 10 هي شرط بولياني يتحقق ممّا إذا كان counter أصغر من أو يساوي 10. إذا كان الشرط صحيحًا (true) فستُنفّذ الحلقة. وإن كان false فستُنهى. counter++‎ هي عملية زيادة (increment) تزيد قيمة العدَّاد بـ 1 قبل التحقق من الشرط التالي. إن تركت جميع التعليمات فارغة، فستحصل على حلقة لا نهائية (infinite loop): for (;;) std::cout << "Never ending!\n"; حلقة while اللانهائية التي تكافئ حلقة for السابقة هي: while (true) std::cout << "Never ending!\n"; لكن على أي حال، يمكن إيقاف الحلقات اللانهائية باستخدام تعليمات break أو goto أو return، أو عبر رفع اعتراض (throwing an exception). انظر المثال التالي الذي يوضح التكرار على عناصر مجموعة من المكتبة القياسية STL (مثل vector) دون استخدام الترويسة : std::vector < std::string > names = { "Albert Einstein", "Stephen Hawking", "Michael Ellis" }; for (std::vector < std::string > ::iterator it = names.begin(); it != names.end(); ++it) { std::cout << * it << std::endl; } حلقة While تكرِّر حلقة while تنفيذ تعليمات معيّنة طالما كان شرطها صحيحًا، تُستخدم هذه الحلقة إن لم تكن تعرف عدد مرات تنفيذ جزء من الشيفرة بشكل مسبق. فمثلأ، لطباعة جميع الأعداد من 0 إلى 9، يمكن استخدام الشيفرة التالية: int i = 0; while (i < 10) { std::cout << i << " "; ++i; // عداد الزيادة } std::cout << std::endl; تُطبع الأعداد من 0 إلى 9 مع نهاية السطر الأخير. لاحظ أن دمج التعليمتيْن الأوليين صار ممكنًا منذ الإصدار C++ 17، انظر: while (int i = 0; i < 10) //... بقية الشيفرة هي نفسها يمكن استخدام الشيفرة التالية لإنشاء حلقة لا نهائية، وتستطيع إيقاف الحلقة عبر تعليمة break: while (true) { // أدخل هنا أي شيء تريد فعله بلا نهاية } هناك صورة آخرى لحلقات while، وهي do...while. وهي موضوع الفقرة التالية. حلقة do-while الحلقتان التكراريتان do-while و while متشابهتان، إلا أن الأولى تتحقق من الشرط في نهاية كل تكرار، وليس في بدايته، وعليه فإنّ الحلقة ستُنفّذ مرة واحدة على الأقل. ستطبع الشيفرة التالية العدد 0 إذ سيكون تقييمُ الشرط false في نهاية التكرار الأول: int i = 0; do { std::cout << i; ++i; // عداد الزيادة } while (i < 0); std::cout << std::endl; // يطبع صفرًا تنبيه: لا تنس الفاصلة المنقوطة في نهاية while(condition);‎، فهي إلزامية في بنية do-while. على النقيض من الحلقة do-while، فإن الشيفرة التالية لن تطبع شيئًا لأنّ شرط الحلقة لم يتحقق في بداية التكرار الأول: int i = 0; while (i < 0) { std::cout << i; ++i; // عداد الزيادة } std::cout << std::endl; // لن يُطبع أيّ شيء تنبيه: يمكن إنهاء الحلقة while حتى لو لم يصبح الشرط خاطئًا باستخدام أي من التعليمات الآتية: break أو goto أو return. int i = 0; do { std::cout << i; ++i; // عداد الزيادة if (i > 5) { break; } } while (true); std::cout << std::endl; // يطبع الأعداد من صفر إلى خمسة تُستخدم الحلقة do-while أحيانًا لكتابة وحدات الماكرو (macros) التي تتطلّب نطاقًا خاصًّا بها (في هذه الحالة، يتمّ حذف الفاصلة المنقوطة الزائدة من تعريف الماكرو، ويُطلب من المستخدم توفيرها): #define BAD_MACRO(x) f1(x); f2(x); f3(x); // f1 الشرط لا يحمي هنا إلا استدعاء if (cond) BAD_MACRO(var); #define GOOD_MACRO(x) do { f1(x); f2(x); f3(x); } while(0) // كل الاستدعاءات محميّة هنا if (cond) GOOD_MACRO(var); تعليمات التحكم في الحلقات: break و continue تُستخدم عبارتَا التحكم break و continue لتغيير مسار التنفيذ من تسلسله المعتاد، فتُدمَّر جميع الكائنات الآلية (automatic objects) التي أنشئت داخل نطاق ما بمجرد ترك تنفيذٍ لذلك النطاق. وتنهي التعليمة break الحلقة فورًا دون النظر لأي عوامل أخرى. for (int i = 0; i < 10; i++) { if (i == 4) break; // إنهاء الحلقة فورا std::cout << i << '\n'; } تكون النتيجة ما يلي: 1 2 3 أما التّعليمة continue لا توقف الحلقة على الفور، بل تتخطّى بقيّة التعليمات الموجودة في متن الحلقة وتذهب إلى بداية الحلقة (بما في ذلك تعليمة التحقّق من الشّرط). انظر المثال التالي حيث تقيَّم (if (i % 2 == 0 إلى true إن كان العدد زوجيًا، وتذهب continue فورًا إلى بداية الحلقة، لكن لا يُنتَقَل إلى التعليمة التالية إن لم تُنفَّذ. for (int i = 0; i < 6; i++) { if (i % 2 == 0) continue; std::cout << i << " is an odd number\n"; } تكون النتيجة ما يلي: 1 is an odd number 3 is an odd number 5 is an odd number لا تُستخدم break و continue إلا نادرًا، ذلك أنه يصعب معهما قراءة الشيفرة وفهمها، وتُستخدم أساليب أخرى أبسط بدلًا منهما.فمثلًا يمكن إعادة كتابة الحلقة for الأولى التي تستخدم break على النّحو التالي: for (int i = 0; i < 4; i++) { std::cout << i << '\n'; } وبالمثل، يمكن إعادة كتابة المثال الثّاني الذي يحتوي continue كالتالي: for (int i = 0; i < 6; i++) { if (i % 2 != 0) { std::cout << i << " is an odd number\n"; } } التصريح عن المتغيرات في العبارات الشَّرطية يسمح بالتصريح عن كائن في شرط حلقات for أو while، وسيُدرَج ذلك الكائن في النّطاق حتى نهاية الحلقة، وسيكون متاحًا خلال كل تكرارات الحلقة: for (int i = 0; i < 5; ++i) { do_something(i); } // لم يعد في النطاق i for (auto& a : some_container) { a.do_something(); } // لم يعد في النطاق a while(std::shared_ptr<Object> p = get_object()) { p-> do_something(); } // لم يعد في النطاق p لا يمكنك فعل الشيء نفسه مع حلقة do...while؛ إذ عليك التصريح عن المتغير قبل الحلقة، ثمّ وضع المتغير والحلقة داخل نطاق محلّي (local scope) إن أردت أن يُحذَف المتغير بعد انتهاء الحلقة: // هذه الشّيفرة لن تُصرّف do { s = do_something(); } while (short s > 0); // جيّد short s; do { s = do_something(); } while (s > 0); وذلك لأنّ متن الحلقة do...while يقيَّم قبل الوصول إلى الجزء (while)، وعليه فإن التصاريح الموضوعة في ذلك الجزء لن تكون مرئيّة أثناء التّكرار الأول للحلقة. تكرار حلقة for على نطاق فرعي تستطيع التكرار على جزء فرعي من حاوية أو نطاق ما باستخدام الحلقات النطاقية (range-base loops)، وذلك من خلال إنشاء كائن وكيل (proxy object). template < class Iterator, class Sentinel=Iterator > struct range_t { Iterator b; Sentinel e; Iterator begin() const { return b; } Sentinel end() const { return e; } bool empty() const { return begin() == end(); } range_t without_front(std::size_t count = 1) const { if (std::is_same< std::random_access_iterator_tag, typename std::iterator_traits<Iterator>::iterator_category >{} ) { count = (std::min)(std::size_t(std::distance(b, e)), count); } return { std::next(b, count), e }; } range_t without_back(std::size_t count = 1) const { if (std::is_same< std::random_access_iterator_tag, typename std::iterator_traits<Iterator>::iterator_category >{} ) { count = (std::min)(std::size_t(std::distance(b, e)), count); } return { b, std::prev(e, count) }; } }; template < class Iterator, class Sentinel > range_t<Iterator, Sentinel> range( Iterator b, Sentinal e ) { return { b, e }; } template < class Iterable > auto range(Iterable & r) { using std::begin; using std::end; return range(begin(r), end(r)); } template < class C > auto except_first(C & c) { auto r = range(c); if (r.empty()) return r; return r.without_front(); } نستطيع الآن فعل ما يلي: std::vector < int > v = {1, 2, 3, 4}; for (auto i: except_first(v)) std::cout << i << '\n'; يكون الناتج: 2 3 4 يجب أن تتذكّر أنّ الكائنات الوسيطة (intermediate objects) المُنشأة في الجزء for(:range_expression)‎ من حلقة for ستُحذف عند بدء تنفيذ الحلقة. هذ الدرس جزء من سلسلة دروس عن C++‎. ترجمة -بتصرّف- للفصل Chapter 11: Loops‎ من الكتاب C++ Notes for Professionals
  4. النطاقات Scopes المتغيرات العامة Global variables إذا أردت التصريح عن نسخة واحدة من متغير ما، وكانت هذه النسخة متاحة للوصول في عدة ملفات مصدرية source files، فمن الممكن أن نجعلها في النطاق العام global scope باستخدام الكلمة المفتاحية extern، فهي تخبر المصرِّف بوجود تعريف لهذا المتغير في مكان ما داخل الشيفرة الحالية، لذا فمن الممكن استخدامه في أي مكان، كما ستُجرى جميع عمليات القراءة / الكتابة في نفس المكان من الذاكرة. يتألّف المثال التالي من عدّة ملفات مصدرية: الملف my_globals.h: #ifndef __MY_GLOBALS_H__ #define __MY_GLOBALS_H__ extern int circle_radius; // سيُعرَّف في مكان ما circle_radius تخطر المصرّف بأنّ #endif الملف foo1.cpp: #include "my_globals.h" int circle_radius = 123; // extern تعريف المتغير الخارجي الملف main.cpp: #include "my_globals.h" #include <iostream> int main() { std::cout << "The radius is: " << circle_radius << "\n"; ' return 0; } سينتج الخرج التالي: The radius is: 123 المتغيرات المضمنة Inline variables يُسمح بتعريف مُتغير مُضمّن Inline variable في عدّة وحدات ترجمة دون انتهاك قاعدة التعريف الواحد. وفي حال تعريف متغيّر مُضمّن أكثر من مرة، فسيَدمِج الرابط linker كلّ تَعاريفه في كائن واحد في البرنامج النهائي. يمكن تعريف حقل ساكن static data member في صنف إن صُرِّح عنه بأنّه مُضمّن ‎inline‎. على سبيل المثال، يمكن تعريف الصنف في المثال التالي في الترويسة. ملاحظة: قبل C++‎ 17، كان من الضروري توفير ملفّ ‎.cpp‎ لاحتواء تعريف Foo::num_instances كي نضمن ألا يُعرّف أكثر من مرّة واحدة، لكن في C++‎ 17، أصبحت مُختلف تعريفات المتغيّر المضمن ‎Foo::num_instances‎ تشير إلى نفس الكائن ‎int‎. // not thread-safe قد لا يكون متوافقا مع المسالك class Foo { public: Foo() { ++num_instances; }~Foo() { --num_instances; } inline static int num_instances = 0; }; وفي حالّة خاصة، تكون الحقول الساكنة للتعابير الثابتة constexpr‎ مضُمّنة بطريقة غير صريحة implicitly. class MyString { public: MyString() { /* ... */ } // ... static constexpr int max_size = INT_MAX / 2; }; صار التعريف constexpr int MyString::max_sizeضروريًا في كل وحدات الترجمة في إصدار C++ 1. نطاق كتلة بسيط يبدأ نطاق متغيّر ما في كتلة ‎{ ... }‎ بعد التصريح وينتهي عند نهاية الكتلة، فإذا كانت هناك كتلة متشعّبة nested block، فتستطيع الكتلة الداخلية إخفاء نطاق المتغيّر الذي صُرِّح عنه في الكتلة الخارجية. { int x = 100; // ^ // يبدأ هنا `x` نطاق // } // <- ينتهي هنا `x` نطاق في حال التصريح عن متغير جديد داخل الكتلة المُتشعِّبة، وكان ذلك المتغير يحمل اسم متغير موجود في الكتلة الخارجية، فإنه يخفي المتغير الأول الذي في الكتلة الخارجية. { int x = 100; { int x = 200; std::cout << x; // → 200 } std::cout << x; // → 100 } قاعدة التعريف الواحد ODR انتهاك قاعدة ODR عبر تحليل التحميل الزائد يمكن انتهاك ODR إذا لم يشِر البحث عن الأسماء إلى نفس الكيان حتى في حال استخدام مقاطع tokens متماثلة للتعبير عن دالة مضمّنة معيّنة. لاحظ الدالة ‎func‎ في المثال التالي: header.h void overloaded(int); inline void func() { overloaded('*'); } foo.cpp ، تشير overloaded إلى (void overloaded(char: #include "header.h" void foo() { func(); } bar.cpp، تشير overloaded إلى (void overloaded(char: void overloaded(char); // أخرى “include” قد تأتي من عبارات تضمين #include "header.h" void bar() { func(); } لدينا انتهاك هنا لقاعدة ODR، إذ يشير المقطع ‎overloaded‎ إلى عدّة كيانات مختلفة في نفس وحدة الترجمة. مضاعفة الدوال المعرفة Multiply defined function إنّ أهم نتيجة لقاعدة التعريف الواحد هي أنّ الدوال غير المضمّنة ذات الارتباط الخارجي non-inline functions with external linkage يجب ألا تُعرَّف إلا مرّة واحدة في البرنامج، لكن يمكن التصريح عنها عدّة مرات. لذا لا ينبغي تعريف مثل هذه الدوال في الترويسات، إذ يمكن إدراج الترويسة عدّة مرات من قبل عدة وحدات ترجمة مختلفة. foo.h‎: #ifndef FOO_H #define FOO_H #include <iostream> void foo() { std::cout << "foo"; } void bar(); #endif foo.cpp: #include "foo.h" void bar() { std::cout << "bar"; } main.cpp‎: #include "foo.h" int main() { foo(); bar(); } في هذا البرنامج، عُرِّفت الدالّة الأبسط ‎foo‎ في الترويسة ‎foo.h‎، والتي ضُمِّنت مرّتين: مرّة من قبل ‎foo.cpp‎ ومرّة أخرى من قبل ‎main.cpp‎، ولذلك تحتوي كل وحدة ترجمة على تعريف ‎foo‎ الخاص بها. لاحظ أنّ دروع التضمين include guards في ‎foo.h‎ لا تمنع حدوث ذلك، إذ أنّ الملفين ‎foo.cpp‎ و ‎main.cpp‎ يشتملان على ‎foo.h‎ بشكل منفصل، وعلى الأرجح سينتج عن محاولة إنشاء هذا البرنامج خطأٌ في وقت الربط link-time error بسبب تعريف ‎foo‎ أكثر من مرّة. ويجب التصريح عن الدوال في الترويسات وتعريفها في ملفات ‎.cpp‎ المقابلة لتجنّب مثل هذه الأخطاء، مع بعض الاستثناءات. الدوال المضمنة Inline functions يمكن تعريف دالّة مُضمّنة ‎inline‎ في عدّة وحدات ترجمة بشرط أن تكون جميع تَعاريفها متطابقة، ويجب أن تُعرّف أيضًا في كل وحدة ترجمة تُستخدم فيها، لذا يجب تعريف الدوال المضمّنة في الترويسات، ولا توجد حاجة إلى ذكرها في ملف التنفيذ implementation file. سيتصرّف البرنامج كما لو كان هناك تعريف واحد للدالّة. foo.h‎: #ifndef FOO_H #define FOO_H #include <iostream> inline void foo() { std::cout << "foo"; } void bar(); #endif foo.cpp: #include "foo.h" void bar() { // تعريفات أخرى أكثر تعقيدا } main.cpp‎: #include "foo.h" int main() { foo(); bar(); } في هذا المثال، عُرِّفت الدالّة ‎foo‎ بصورة مُضمّنة inline في الترويسة، أمّا الدالّة الأكثر تعقيدًا ‎bar‎ فهي غير مُضمنة، وقد عُرِّفت في ملف التقديم. وتحتوي كل من وحدتي الترجمة foo.cpp‎ و ‎main.cpp على تعريفات لـ ‎foo‎، وهذا لن يسبّب مشكلة لأنّ ‎foo‎ مضمّنة. تكون الدوالّ المُعرَّفة داخل تعريف صنف ما -والتي قد تكون دوالًا تابعة أو صديقة- مضمنة بشكل غير صريح، لذا يمكن تعريف الدوال التابعة للفصل داخل تعريف الصنف، رغم أنّه يمكن إدراج التعاريف في وحدات ترجمة متعددة: // in foo.h class Foo { void bar() { std::cout << "bar"; } void baz(); }; // in foo.cpp void Foo::baz() { // التعريف } عُرِّفت الدالّة ‎Foo::baz‎ خارج السطر، وعليه تكون غير مُضمّنة، ويجب ألا تُعُرِّف في الترويسة. البحث باسم الوسيط Argument Dependent Name Lookup عند البحث عن الدوال، تُجمع "الأصناف المرتبطة" associated classes، و"فضاءات الأسماء المرتبطة" associated namespaces التي تحقق أحد الشروط التالية، وفقًا لنوع الوسيط ‎T‎. عند البحث عن الدوال فإننا نجدها عبر جمع "الأصناف المرتبطة" associated classes أولًا، وكذلك "فضاءات الأسماء المرتبطة" associated namespaces، التي تحقق شرطًا أو أكثر من الشروط أدناه وفقًا لنوع الوسيط ‎T‎، لكن سنعرض قبل هذا القواعد الخاصة بأسماء الأصناف والتعداد وقوالب أسماء الأصناف: إذا كان ‎T‎ صنفًا مُتشعِّبا nested class، أو عضوَ تعدادٍ member enumeration، فسيُضاف الصنف المحيطة به إلى مجموعة البحث. إذا كان ‎T‎ تعدادًا -قد يكون أيضًا عضوًا من صنف-، يُضاف أعمق فضاء اسم - innermost namespace - داخله إلى مجموعة البحث. إذا كان ‎T‎ صنفًا -قد يكون مُتشعِّبًا أيضًا-، تُضاف جميع أصنافه الأساسية، وكذلك الصنف نفسه، وأعمق فضاءات الاسم في جميع الأصناف المرتبطة. إذا كان ‎T‎ من النوع ‎ClassTemplate<TemplateArguments>‎ -وهو صنف أيضًا-، تُضاف الأصناف وفضاءات الاسم المرتبطة بوسائط نوع القالب، وفضاء الاسم الخاص بوسائط قالب القالب template template argument، والصنف المحيط بوسائط قالب القالب، إذا كان وسيط القالبِ template argument قالبَ عضوٍ member template. هناك بعض القواعد التي تخصّ الأنواع المضمّنة أيضًا: إذا كان ‎T‎ مؤشّرًا يشير إلى نوع ‎U‎، أو مصفوفةً مؤلّفة من عناصر من النوع ‎U‎، تُضاف أصناف وفضاءات الأسماء المرتبطة بـ ‎U‎ إلى مجموعة البحث. في المثال: void (*fptr)(A);f(fptr);‎، تُضاف فضاءات الأسماء والأصناف المرتبطة بـ ‎void(A)‎ (انظر القاعدة التالية). إذا كان ‎T‎ نوع دالّة، تُضاف الأصناف وفضاءات الأسماء المرتبطة بأنواع المُعاملات والقيمة المعادة. في المثال: void(A)‎، تُضاف فضاءات الأسماء والأصناف المرتبطة بـ A. إذا كان ‎T‎ مؤشّرًا يشير إلى عضو، تُضاف الأصناف وفضاءات الأسماء المرتبطة بنوع ذلك العضو (قد تنطبق على كل من مؤشّرات التوابع ومؤشّرات الحقول). في مثال ‎‎B A::*p; void (A::*pf)(B); f(p);f(pf);‎؛ تُضاف فضاءات الأسماء والأصناف المرتبطة بـ A‎ و B‎ وvoid(B)‎، والتي تطبّق القواعد أعلاه التي تخصّ أنواع الدوال. يُعثَر على جميع الدوال والقوالب داخل جميع فضاءات الأسماء المرتبطة من خلال البحث باسم الوسيط، أو البحث المعتمد على الوسيط argument dependent lookup. ويُعثَر على الدوال الصديقة friend functions لنطاق الأسماء المُصرَّح عنها في الأصناف المرتبطة، والتي عادة ما تكون غير مرئية. بالمقابل، تُتجَاهل الموجّهات directives. وتكون جميع الاستدعاءات في المثال التالي صالحة حتى دون الحاجة إلى تأهيل ‎f‎ بفضاء الاسم عند الاستدعاء. namespace A { struct Z {}; namespace I { void g(Z); } using namespace I; struct X { struct Y {}; friend void f(Y) {} }; void f(X p) {} void f(std::shared_ptr < X > p) {} } // أمثلة على الاستدعاءات f(A::X()); f(A::X::Y()); f(std::make_shared < A::X > ()); g(A::Z()); // "using namespace I;"غير صالح، يُتجاهل حزم المعاملات Parameter packs قالب بحزمة معاملات template with a parameter pack template < class...Types > struct Tuple {}; حزمة المُعاملات parameter pack هي مُعاملُ قالبٍ template parameter يقبل وسيط قالب أو أكثر، أو لا يقبل على الإطلاق، ويكون القالب متغايرًا variadic إذا كان يحتوي على حزمة مُعاملات واحدة على الأقل. توسيع حزمة معاملات يُوسَّع نمط ‎parameter_pack ...‎ إلى قائمة من البدائل لحزمة ‎parameter_pack‎ مفصولة بالفاصلة الأجنبية ,، وذلك مع كل معامل من معاملاتها: template<class T> // أساس التكرار void variadic_printer(T last_argument) { std::cout << last_argument; } template<class T, class ...Args> void variadic_printer(T first_argument, Args... other_arguments) { std::cout << first_argument << "\n"; variadic_printer(other_arguments...); // توسيع حزمة المعاملات } عند استدعاء الشيفرة أعلاه مع ‎variadic_printer(1, 2, 3, "hello");‎، ستطبع: 1 2 3 hello فواصل الأرقام Digit separators من الصعب قراءة القيم العددية المؤلّفة من من أرقام كثيرة، فمثلًا: حاول نُطق 7237498123 قارن بين 237498123 وبين 237499123 من حيث التساوي. حدّد العدد الأكبر من بين 237499123 و 20249472. وقد عرَّف الإصدار ‎C‎++ 14 علامةَ الاقتباس ‎'‎ على أساس فاصلة للأرقام، بحيث تُستخدَم داخل الأعداد والقيم مصنَّفة النوع المُعرَّفة من قبل المستخدِم user-defined literals لتسهيل قراءة الأعداد الكبيرة. انظر: الإصدار ≥ C++‎ 14 long long decn = 1'000'000'000ll; long long hexn = 0xFFFF'FFFFll; long long octn = 00'23'00ll; long long binn = 0b1010'0011ll; تُتجَاهل علامة الاقتباس المُفردة عند تحديد القيمة. مثلًا: القيم مصنَّفة النوع التالية: ‎1048576‎ و ‎1'048'576‎ و ‎0X100000‎ و ‎0x10'0000‎ و ‎0'004'000'000‎ كلها لها نفس القيمة. القيم مصنفة النوع التالية ‎1.602'176'565e-19‎ و ‎1.602176565e-19‎ لهما نفس القيمة. ولا توجد أهمية تُذكر لموضع علامات الاقتباس المُفردة، فمثلًا، كل التعابير التالية متكافئة: الإصدار ≥ C++‎ 14 long long a1 = 123456789ll; long long a2 = 123'456'789ll; long long a3 = 12'34'56'78'9ll; long long a4 = 12345'6789ll; كذلك يُسمح باستخدام الفاصلة في القيم مصنَّفة النوع والمُعرّفة من المستخدم: الإصدار ≥ C++‎ 14 std::chrono::seconds tiempo = 1'674'456s + 5'300h; موازنة مع إصدارات C++‎ المختلفة اعلم أنه يمكن تنفيذ تكرار حلقي على حاوية التسلسل c في لغة ++C باستخدام الفهارس على النحو التالي: for (size_t i = 0; i < c.size(); ++i) c[i] = 0; هذه الصيغة -رغم بساطتها- عرضة لكثير من الأخطاء الشائعة، مثل استخدام عامل مقارنة خاطئ، أو متغيّر فهرسة غير صحيح: for(size_t i = 0; i <= c.size(); ++j) c[i] = 0; ^~~~~~~~~~~~~~^ كذلك يمكن التكرار على الحاويات باستخدام المُكرّرات iterators، مع عيوب مماثلة: for(iterator it = c.begin(); it != c.end(); ++it) (*it) = 0; وقد قدّمت C++‎ 11 حلقة for النطاقية، إضافة إلى الكلمة المفتاحية ‎auto‎، واللّتان سمحتا بكتابة شيفرات مثل: for(auto& x : c) x = 0; وهنا، يكون المُعاملان الوحيدان هما الحاوية ‎c‎، ومتغيّر ‎x‎ لتخزين القيمة الحالية. هذا يمنع الأخطاء المشار إليها سابقا. ووفقًا لمعيار C++‎ 11، فإنّ التنفيذ الأساسي underlying implementation يكافئ: for(auto begin = c.begin(), end = c.end(); begin != end; ++begin) { // ... } في مثل هذا التنفيذ فإن التعبير ‎auto begin = c.begin(), end = c.end();‎ يجبر ‎begin‎ و ‎end‎ على أن يكونا من نفس النوع، بينما لا تُزاد قيمة ‎end‎ ولا تحصَّل dereferenced، ومن ثم فإنّ حلقة for النطاقية لا تعمل إلا على الحاويات المُعرَّفة بواسطة زوج مُكرّر/ مُكرّر iterator/iterator. وقد خفّف معيارُ C++‎ 17 هذا القيد عن طريق تعديل التنفيذ: auto begin = c.begin(); auto end = c.end(); for(; begin != end; ++begin) { // ... } هنا يُسمح بأن تكون ‎begin‎ و ‎end‎ من نوعين مختلفين طالما أنه يمكن مقارنتهما للتحقّق من أنّهما غير متساويين. هذا يسمح بالتكرار على العديد من الحاويات مثل الحاويات المعرّفة عبر زوج مُكرّر/حارس iterator/sentinel. هذا الدرس جزء من سلسلة دروس عن C++‎. ترجمة -بتصرّف- للفصول Chapter 64: Inline variables Chapter 117: Scopes Chapter 120: One Definition Rule (ODR)‎ Chapter 122: Argument Dependent Name Lookup Chapter 129: Parameter packs Chapter 135: Digit separators Chapter 137: Side by Side Comparisons of classic C++ examples solved via C++ vs C++11 vs C++14 vs C++1 من كتاب C++ Notes for Professionals
  5. تُعرف اللغتان C وC++‎ بانّ أداءهما عال جدًّا - ويُعزى ذلك في الغالب إلى إمكانية التخصيص المكثّف للشيفرة، إذ يُسمح للمستخدم بتحسين الأداء عبر اختيار بنية الشيفرة وكيفية تنفيذها. وإن أردت تحسين الشيفرة فمن المهم أن تفهمها وتعرف كيفية استخدامها. وتشمل بعض أخطاء التحسين الشائعة ما يلي: التحسين السابق لأوانه: قد يسوء أداء الشيفرات المعقّدة بعد التحسين ممّا يضيع الوقت ويقلل الكفاءة. يجب أن تكون الأولوية هي كتابة شيفرة صحيحة وقابلة للصيانة، وليس شيفرة مُحسَّنة. التحسين في الحالات غير المناسبة: إضافة تحميل زائد لأجل حالة احتمال حدوثها 1% لا يستحقّ أن تبطئ لأجله 99% من الحالات الأخرى. التحسين المصغّر Micro-optimization: تقوم المُصرِّفات بهذا الأمر بفعالية كبيرة، كما أن التحسين المصغَّر يمكن أن يضرّ بقدرة المُصرِّفات على زيادة تحسين الشيفرة. وبعض أهداف التحسين النموذجية ما يلي: تقليل كمية العمل المطلوبة. استخدام خوارزميات / بنيات أكثر فاعلية. استغلال العتاد بشكل أفضل. لكن قد تكون لعمليات التحسين تأثيرات جانبية سلبية، مثل: الاستخدام المكثّف للذاكرة. تعقيد الشيفرة وصعوبة قراءتها أو صيانتها. تعقيد الواجهات البرمجية API وتصميمات الشيفرة. على أي حال، غالبًا ما يُعدِّل المُصرِّفُ البرنامجَ عند التصريف لتحسين أدائه، وهذا مسموح به وفقًا لقاعدة "كما لو" as-if rule. والتي تسمح بإجراء التغييرات والتحويلات التي لا تغيّر السلوك الملاحظ للبرنامج. تحسين الصنف الأساسي الفارغ Empty Base Class Optimization لا يمكن أن يشغل كائن ما أقل من بايت واحد، وإلّا لكانت أعضاء المصفوفات التي تضم عناصر من نفس النوع تحتل العنوان نفسه. لهذا، تكون ‎sizeof(T)>=1‎ صحيحة دائما. وكذلك لا يمكن أن يكون الصنف المشتق أصغر من صنفه الأساسي base class. لكن إن كان الصنف الأساسي فارغًا، فلن يُضاف حجمه بالضرورة إلى الصنف المشتق: class Base {}; class Derived: public Base { public: int i; }; وليس من الضروري هنا تخصيص بايت للصنف ‎Base‎ داخل ‎Derived‎ حتى يحتلّ كل كائن -بغض النظر عن نوعه- عنوانًا مميّزًا. وفي حال إجراء تحسين للصنف الأساسي الفارغ -ولم تكن هناك حاجة إلى الحشو padding-، فستكون العبارة ‎sizeof(Derived) ==sizeof(int)‎ صحيحة، أي أنّه لن يُجرى أيّ تخصيص إضافي للصنف الفارغ. هذا ممكن حتّى مع الأصناف الأساسية base classes المتعددة، إذ لا يمكن لعدّة أًصناف أساسية في ++C أن يكون لها نفس النوع، وعليه فلن تنشأ مشكلة من ذلك. لاحظ أنّه لا يمكن إجراء ذلك إلا إذا كان نوع العضو الأول من ‎Derived‎ مختلفًا عن كلّ الأصناف الأساسية، وهذا يشمل كل الأصناف الأساسية المشتركة بشكل مباشر أو غير مباشر. وإذا كان له نفس نوع أحد أصنافه الأساسية -أو كانت هناك صنف أساسية مشترك)- فيلزم على الأقل تخصيص بايت واحد لضمان ألّا يحتلّ كائنان مختلفان من نفس النوع العنوان نفسه. التحسين عن طريق تقليل الشيفرة المنفذة الأسلوب الأوضح للتحسين هو تقليل الشيفرة المنفّذة، فعادة ما يسرِّع التنفيذَ دون تغيير التعقيد الزمني للشيفرة. لكن رغم أنّ هذه الطريقة تسرّع البرنامج، فلن تظهر فائدتها إلّا في الشيفرات التي تُستدعى كثيرًا. هذه بعض الأمثلة على تقليل الشيفرات المنفّذة: إزالة الشيفرات عديمة الفائدة void func(const A *a); // دالة ما // تخصيص غير ضروري للذاكرة + إلغاء تخصيص النسخة auto a1 = std::make_unique<A>(); func(a1.get()); // استخدام المكدّس يمنع auto a2 = A{}; func(&a2); الإصدار ≥ C++‎ 14 منذ الإصدار C++‎ 14، يُسمح للمُصرِّفات بتحسين هذه الشيفرة لإزالة تخصيص الذاكرة، ومطابقة التحرير matching deallocation. تنفيذ الشيفرة مرة واحدة فقط std::map<std::string, std::unique_ptr<A>> lookup; // إدارج/بحث بطيئ // داخل هذه الدالة، سنمر على القاموس مرّتين لأجل البحث عن العنصر، ومرّة ثالثة إن لم يكن موجودا const A *lazyLookupSlow(const std::string &key) { if (lookup.find(key) != lookup.cend()) lookup.emplace_back(key, std::make_unique<A>()); return lookup[key].get(); } // داخل هذه الدالة، سيحصل نفس التأثير الذي يحدث في النسخة الأبطأ لكن مع مضاعفة السرعة // لأنّنا سنمر على القاموس مرة واحدة فقط const A *lazyLookupSlow(const std::string &key) { auto &value = lookup[key]; if (!value) value = std::make_unique<A>(); return value.get(); } يمكن استخدام أسلوب مشابه للتحسين عبر تنفيذ implement إصدار مستقر من ‎unique‎: std::vector<std::string> stableUnique(const std::vector<std::string> &v) { std::vector<std::string> result; std::set<std::string> checkUnique; for (const auto &s : v) { // يُعاد الإدراج إن كان ناجحا، لذلك يمكن أن نعرف ما إذا كان العنصر موجودًا أم لا // هذا يمنع الإدراج، والذي سيمر على القاموس مرّة لكل عنصر فريد // على عناصر مكرّرة v يُكسِبنا هذا حوالي نصف الوقت إن لم تحتو if (checkUnique.insert(s).second) result.push_back(s); } return result; } منع عمليات إعادة التخصيص والنسخ أو النقل عديمة الفائدة منعنا في المثال السابق عمليّات البحث في std::set، بيْد أنّ هذا غير كافٍ، فما يزال المتجه ‎std::vector‎ يحتوي على خوارزمية مكلّفة ونامية ستحتاج إلى نقل ذاكرتها، يمكن منع هذا عبر حجز الحجم المناسب. في المثال التالي، نضمن عدم حدوث أي نسخ أو نقل في المتجه لأن سعته تساوي الحد الأقصى للأعداد التي سندرجها، وإن افترضنا عدم حدوث أي تخصيص من الحجم صفر، وأن تخصيص كتلة كبيرة من الذاكرة يأخذ نفس الوقت الذي يأخذه تخصيص حجم صغير فهذا لن يبطئ البرنامج. ملاحظة: يمكن أن تنتبه المصرفات لهذا الأمر وتحذف شيفرة التحقق من الشيفرة المولَّدة. std::vector<std::string> stableUnique(const std::vector<std::string> &v) { std::vector < std::string > result; result.reserve(v.size()); std::set < std::string > checkUnique; for (const auto &s : v) { // انظر المثال أعلاه if (checkUnique.insert(s).second) result.push_back(s); } return result; } استخدام حاويات أكفأ استخدام بنيات البيانات الصحيحة في الوقت المناسب يمكن أن يحسّن التعقيد الزمني للشيفرة، انظر المثال التالي حيث يساوي تعقيد stableUnique هنا (N log(N، حيث N أكبر من عدد العناصر في v: // log(N) > insert complexity of std::set std::vector<std::string> stableUnique(const std::vector<std::string> &v) { std::vector<std::string> result; std::set<std::string> checkUnique; for (const auto &s : v) { // انظر فقرة التحسين عبر تقليل الشيفرة المنفّذة if (checkUnique.insert(s).second) result.push_back(s); } return result; } يمكننا تخفيض تعقيد التقديم إلى N عبر استعمال حاوية تستخدم تنفيذًا مختلفًا لتخزين عناصرها، مثل قاموس hash بدلًا من شجرة tree. وهناك أثر جانبي إضافي أيضًا، وهو تقليل استدعاء مُعامل المقارنة على السلاسل النصية، لأنّنا لن نحتاج إلى استدعائها إلّا في حال كانت ستُدرج السلسلة النصية في نفس المجموعة. انظر المثال التالي حيث يكون تعقيد stableUnique مساويًا لـ (N log (N، حيث N أكبر من عدد العناصر في v: // أكبر من 1 std::unordered_set تعقيد الإدراج في std::vector<std::string> stableUnique(const std::vector<std::string> &v) { std::vector<std::string> result; std::unordered_set<std::string> checkUnique; for (const auto &s : v) { // انظر فقرة التحسين عبر تقليل الشيفرة المنفّذة if (checkUnique.insert(s).second) result.push_back(s); } return result; } تحسين الكائنات الصغيرة Small Object Optimization تحسين الكائنات الصغيرة هي تقنية تُستخدم في بنيات البيانات منخفضة المستوى، مثل ‎std::string‎ (يشار إليها أحيانًا باسم تحسين السلاسل النصية القصيرة/الصغيرة)، ويُقصد بها استخدام مساحة مكدّس كمخزن مؤقت buffer بدلًا من استخدام ذاكرة مخصّصة عندما يكون المحتوى صغيرًا بدرجة كافية لتسَعَه المساحة المحجوزة. وسنحاول منع تخصيص مساحة في الكومة heap، لكنّ ثمن هذا هو إضافة حمل إضافي overhead على الذاكرة، وإجراء بعض الحسابات الإضافية. تعتمد فوائد هذه التقنية على نوع الاستخدام، ويمكن أن تضرّ بالأداء في حال استخدامها بشكل غير صحيح. هذا مثال على ذلك، الطريقة التالية ساذجة لكتابة سلسلة نصية باستخدام هذا التحسين: #include <cstring> class string final { constexpr static auto SMALL_BUFFER_SIZE = 16; bool _isAllocated{false}; ///< تذكّر إذا كنّا قد خصّصنا الذاكرة char *_buffer{nullptr}; ///< مؤشر يشير إلى المخزن المؤقت الذي نستخدمه char _smallBuffer[SMALL_BUFFER_SIZE]= {'\0'}; ///< SMALL OBJECT مساحة المكدّس المُستخدمة لأجل الكائن الصغير OPTIMIZATION public: ~string() { if (_isAllocated) delete [] _buffer; } explicit string(const char *cStyleString) { auto stringSize = std::strlen(cStyleString); _isAllocated = (stringSize > SMALL_BUFFER_SIZE); if (_isAllocated) _buffer = new char[stringSize]; else _buffer = &_smallBuffer[0]; std::strcpy(_buffer, &cStyleString[0]); } string(string &&rhs) : _isAllocated(rhs._isAllocated) , _buffer(rhs._buffer) , _smallBuffer(rhs._smallBuffer) //< غير ضروري عند التخصيص { if (_isAllocated) { // منع الحذف المزدوج للذاكرة rhs._buffer = nullptr; } else { // نسخ البيانات std::strcpy(_smallBuffer, rhs._smallBuffer); _buffer = &_smallBuffer[0]; } } // حَذفنا التوابع الأخرى، مثل منشئ النسخ ومعامل الإسناد، لأجل تحسين القراءة والوضوح }; كما ترى في الشيفرة أعلاه، فقد أُضيفت بعض التعقيدات الإضافية لمنع تنفيذ بعض عمليات ‎new‎ و ‎delete‎. وأصبح لدى الصنف مساحة أكبر للذاكرة، وقد لا يستخدَمها إلا في بضع حالات فقط. غالبًا ما سيُحاول ترميزَ القيمة البوليانية ‎_isAllocated‎ داخل المؤشّر ‎_buffer‎ عبر معالجة البتّات من أجل تقليل حجم النُسخة (intel 64 bit: يمكن أن يقلّل الحجم بمقدار 8 بايت)، لكن هذا التحسين غير ممكن إلا إن كانت قواعد المحاذاة الخاصّة بالمنصة معروفة. حالات الاستخدام نظرًا لأنّ هذا التحسين يضيف الكثير من التعقيد، فليس من المستحسن استخدامه في كل الأصناف. وستُصادفه غالبًا في بنيات البيانات منخفضة المستوى شائعة الاستخدام. قد تجد بعض الاستخدامات في ‎std::basic_string<>‎ و ‎std::function<>‎ في تطبيقات implementations المكتبة القياسية الشائعة في C++‎ 11. وبما أن هذا التحسين لا يمنع تخصيصات الذاكرة إلا عندما تكون البيانات المُخزّنة أصغر من القيمة المضافة، فستظهر فائدته بالخصوص في الأصناف التي تُستخدم غالبًا مع البيانات الصغيرة. لكن هناك عيب فيه وهو الحاجة إلى جهد إضافي عند تحريك المخزن المؤقت، وهذا يجعل عمليّات النقل أكثر كلفة موازنة بالحالات التي لا يُستخدم المخزن المؤقّت فيها، خاصة عندما يحتوي المخزن المؤقّت على نوع بيانات غير قديم non-POD type. توسيع التضمين والتضمين توسيع التضمين Inline expansion، أو التضمين inlining وحسب، هي تقنية تحسين يستخدمها المُصرِّف تقوم على تعويض استدعاء دالة بمتن تلك الدالّة. هذا يقلّل من الحمل الزائد overhead لاستدعاء الدوالّ، ولكن على حساب الذاكرة، إذ أنّ الدالّة قد تُكرّر عدّة مرات. // :المصدر int process(int value) { return 2 * value; } int foo(int a) { return process(a); } // :البرنامج بعد التضمين int foo(int a) { return 2 * a; // foo() في process() نسخ متن } تُستخدم تقنية التضمين غالبًا مع الدوال الصغيرة، حيث يكون الحمل الزائد لاستدعاء الدالّة صغيرًا مقارنةً بحجم جسم الدالّة. تحسين الصنف الأساسي الفارغ Empty base optimization يجب أن لا يقلّ حجم أي كائن أو عضو من كائن فرعي عن القيمة 1، حتى لو كان ذلك النوع فارغًا (أي حتى لو لم يحتوٍ الصنف ‎class‎ أو البنية ‎struct‎ على أعضاء بيانات غير ساكنة)، وذلك لأجل ضمان أن تكون عناوين الكائنات المختلفة التي تنتمي لنفس النوع مختلفة دائمًا. بالمقابل، فإنّ القيود على الكائنات الفرعية من الصنف الأساسي base class subobjects أقل بكثير، ويمكن تحسينها بشكل كامل انطلاقًا من مخطط الكائن: #include <cassert> struct Base {}; // صنف فارغ struct Derived1: Base { int i; }; int main() { // حجم أيّ كائن من صنف فارغ يساوي 1 على الأقل assert(sizeof(Base) == 1); // تطبيق تحسين الصنف الفارغ assert(sizeof(Derived1) == sizeof(int)); } يُستخدم تحسين الصنف الفارغ عادةً في أصناف المكتبة القياسية الواعية بتخصيص كل من الذاكرة: std::vector‎. std::function. std::shared_ptr. أو غيرها، وذلك لتجنّب شغل أي مساحة تخزين إضافية من قبل عضو مُخصِّص allocator member إذا كان ذلك المُخصّص عديم الحالة stateless. ويمكن تحقيق ذلك عن طريق تخزين أحد أعضاء البيانات المطلوبة، مثل: مؤشّرات ‎begin‎ و ‎end‎، أو ‎capacity‎ الخاصّة بالمتجهات. التشخيص Profiling التشخيص باستخدام gcc وgprof يتيح لك برنامج التشخيص gprof الخاصّ بـ GNU gprof تشخيص شيفرتك. ولأجل لاستخدامه، عليك إنجاز الخطوات التالية: ابنِ التطبيق مع الإعدادات المناسبة لتوليد معلومات التشخيص. قم بتوليد معلومات التشخيص عن طريق تشغيل التطبيق المبني. اعرض معلومات التشخيص المُولّدة باستخدام أداة gprof. أضف راية ‎-pg‎ لأجل بناء التطبيق مع إعدادات توليد معلومات التشخيص المناسبة، انظر: $ gcc -pg *.cpp -o app أو: $ gcc -O2 -pg *.cpp -o app وهكذا دواليك. بمجرد إنشاء التطبيق (نفترض أنّ اسمه ‎app‎)، نفِّذه كالمعتاد: $ ./app من المفروض أن ينتج هذا الأمرُ ملفًّا يُسمّى ‎gmon.out‎. إن أردت رؤية نتائج التشخيص، نفّذ الأمر التالي: $ gprof app gmon.out (لاحظ أننا وقرنا كلًّا من التطبيق وكذلك الخرج الناتج). ويمكنك توجيه الخرج أو إعادة توجيهه: $ gprof app gmon.out | less يجب أن تكون نتيجة الأمر الأخير مطبوعة على شكل جدول، ستمثّل صفوفهُ الدوالَّ، أمّا أعمدته فتشير إلى عدد الاستدعاءات، وإجمالي الوقت المستغرق، والوقت الذاتي المستغرق - self time spent - (أي الوقت المستغرق داخل الدالة ما عدا استدعاءات الدوال الداخلية). إنشاء مخططات الاستدعاءات callgraph بواسطة gperf2dot بالنسبة للتطبيقات الكبيرة، فقد يصعب فهم تنسيق التشخيص المُستَوي flat execution profiles، لهذا تنتج العديد من أدوات التشخيص رسومًا تخطيطية لتسهيل قراءتها. تحوّل أداة gperf2dot خرْج النص الذي تنتجه العديد من المُشخِّصات، مثل: Linux perf، وcallgrind، وoprofile إلى رسم تخطيطي، يمكنك استخدام هذه الأداة عن طريق تشغيل المشخّص profiler الذي تعمل به مثل ‎gprof‎: # التصريف مع رايات التشخيص g++ *.cpp -pg # توليد بيانات التشخيص ./main # ترجمة بيانات التشخيص إلى نص، وإنشاء صورة gprof ./main | gprof2dot -s | dot -Tpng -o output.png تشخيص استخدام وحدة المعالجة المركزية بواسطة gcc وGoogle Perf توفّر أدوات Google Perf برنامج تشخيص لوحدة المعالجة المركزية CPU، مع واجهة استخدام سهلة نسبيًا. لاستخدامها عليك: تثبيت أدوات Google Perf . تصريف الشيفرة كالمعتاد. إضافة مكتبة التشخيص ‎libprofiler‎ إلى مسار تحميل المكتبات في وقت التشغيل. استخدم ‎pprof‎ لتوليد تشخيص نصّي، أو رسم تخطيطي. مثلّا: g++ -O3 -std=c++11 main.cpp -o main # تنفيذ المُشخِّص LD_PRELOAD=/usr/local/lib/libprofiler.so CPUPROFILE=main.prof CPUPROFILE_FREQUENCY=100000 ./main حيث: تشير ‎CPUPROFILE‎ إلى ملف بيانات التشخيص، و تشير ‎CPUPROFILE_FREQUENCY‎ إلى تردّد عيّنات المشخّص profiler sampling frequency. إن أردت إجراء المعالجة اللاحقة post-process لبيانات التشخيص، فاستخدم ‎pprof‎، ويمكنك إنشاء تشخيص استدعاءات flat call profile على هيئة نصّ: $ pprof --text ./main main.prof PROFILE: interrupts/evictions/bytes = 67/15/2016 pprof --text --lines ./main main.prof Using local file ./main. Using local file main.prof. Total: 67 samples 22 32.8% 32.8% 67 100.0% longRunningFoo ??:0 20 29.9% 62.7% 20 29.9% __memmove_ssse3_back /build/eglibc-3GlaMS/eglibc-2.19/string/../sysdeps/x86_64/multiarch/memcpy-ssse3-back.S:1627 4 6.0% 68.7% 4 6.0% __memmove_ssse3_back /build/eglibc-3GlaMS/eglibc-2.19/string/../sysdeps/x86_64/multiarch/memcpy-ssse3-back.S:1619 3 4.5% 73.1% 3 4.5% __random_r /build/eglibc-3GlaMS/eglibc-2.19/stdlib/random_r.c:388 3 4.5% 77.6% 3 4.5% __random_r /build/eglibc-3GlaMS/eglibc-2.19/stdlib/random_r.c:401 2 3.0% 80.6% 2 3.0% __munmap /build/eglibc-3GlaMS/eglibc-2.19/misc/../sysdeps/unix/syscall-template.S:81 2 3.0% 83.6% 12 17.9% __random /build/eglibc-3GlaMS/eglibc-2.19/stdlib/random.c:298 2 3.0% 86.6% 2 3.0% __random_r /build/eglibc-3GlaMS/eglibc-2.19/stdlib/random_r.c:385 2 3.0% 89.6% 2 3.0% rand /build/eglibc-3GlaMS/eglibc-2.19/stdlib/rand.c:26 1 1.5% 91.0% 1 1.5% __memmove_ssse3_back /build/eglibc-3GlaMS/eglibc-2.19/string/../sysdeps/x86_64/multiarch/memcpy-ssse3-back.S:1617 1 1.5% 92.5% 1 1.5% __memmove_ssse3_back /build/eglibc-3GlaMS/eglibc-2.19/string/../sysdeps/x86_64/multiarch/memcpy-ssse3-back.S:1623 1 1.5% 94.0% 1 1.5% __random /build/eglibc-3GlaMS/eglibc-2.19/stdlib/random.c:293 1 1.5% 95.5% 1 1.5% __random /build/eglibc-3GlaMS/eglibc-2.19/stdlib/random.c:296 1 1.5% 97.0% 1 1.5% __random_r /build/eglibc-3GlaMS/eglibc-2.19/stdlib/random_r.c:371 1 1.5% 98.5% 1 1.5% __random_r /build/eglibc-3GlaMS/eglibc-2.19/stdlib/random_r.c:381 1 1.5% 100.0% 1 1.5% rand /build/eglibc-3GlaMS/eglibc-2.19/stdlib/rand.c:28 0 0.0% 100.0% 67 100.0% __libc_start_main /build/eglibc-3GlaMS/eglibc-2.19/csu/libcstart. c:287 0 0.0% 100.0% 67 100.0% _start ??:0 0 0.0% 100.0% 67 100.0% main ??:0 0 0.0% 100.0% 14 20.9% rand /build/eglibc-3GlaMS/eglibc-2.19/stdlib/rand.c:27 0 0.0% 100.0% 27 40.3% std::vector::_M_emplace_back_aux ??:0 أو يمكنك إنشاء رسم تخطيطي في ملف pdf باستخدام الأمر: pprof --pdf ./main main.prof > out.pdf هذا الدرس جزء من سلسلة دروس عن C++‎. ترجمة -بتصرّف- للفصول: Chapter 143: Optimization in C++‎ Chapter 144: Optimization Chapter 145: Profiling من كتاب C++ Notes for Professionals
  6. يقضي مطوّرو C++‎ الكثير من وقتهم في تنقيح الأخطاء (debugging)، ويهدف هذا المقال إلى مساعدتك في هذه المهمة وإعطائك بعض التقنيات المفيدة، لكن لا تتوقع قائمة شاملة للمشاكل وحلولها التي تدعمها هذه الأدوات. اختبار الوحدات في C++‎ يسعى اختبار الوحدات (Unit testing) إلى التحقق من صحّة وسلامة وحدات الشيفرة (units of code)، ويشير مصطلح "وحدات الشيفرة" في C++‎ غالبًا إلى الأصناف أو الدوال أو مجموعات مؤلّفة منهما. ويُجرى اختبار الوحدات غالبًا باستخدام "أُطر اختبار" متخصصة أو "مكتبات اختبار"، والتي تستخدِم غالبًا صياغات أو أنماط غير اعتيادية، وسنراجع في هذا الموضوع بعض استراتيجيات ومكتبات وأطر اختبار الوحدات. اختبار جوجل اختبار جوجل (Google Test) هو إطار لاختبار C++‎ من Google، ويتطلّب استخدامُه بناء مكتبة ‎gtest‎ وربطها بإطار الاختبار الخاص بك. مثال بسيط، سنقسم المثال بداعي الشرح … // main.cpp #include <gtest/gtest.h> #include <iostream> أنشئت حالات اختبار جوجل بواسطة وحدات ماكرو خاصة بالمعالج الأولي لـ ++C، وسنوفر المعامِليْن test name و test suite، نتابع … TEST(module_name, test_name) { std::cout << "Hello world!" << std::endl; كذلك توفر Google Test وحدات ماكرو لأجل التوكيدات (assertions): ASSERT_EQ(1+1, 2); } يمكن تشغيل اختبار جوجل يدويًا من دالة ()main أو ربطها بمكتبة gtest_main لدالة ()main معدَّة سلفًا لقبول حالات اختبار جوجل، … int main(int argc, char** argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); } // أمر البناء: g++ main.cpp -lgtest catch Catch هي مكتبة ترويسة (header only library) تسمح لك باستخدام كل من نمطي الاختبار TDD و BDD. الشيفرة التالية مأخوذة من صفحة توثيق Catch: SCENARIO("vectors can be sized and resized", "[vector]") { GIVEN("A vector with some items") { std::vector v(5); REQUIRE(v.size() == 5); REQUIRE(v.capacity() >= 5); WHEN("the size is increased") { v.resize(10); THEN("the size and capacity change") { REQUIRE(v.size() == 10); REQUIRE(v.capacity() >= 10); } } WHEN("the size is reduced") { v.resize(0); THEN("the size changes but not capacity") { REQUIRE(v.size() == 0); REQUIRE(v.capacity() >= 5); } } WHEN("more capacity is reserved") { v.reserve(10); THEN("the capacity changes but not the size") { REQUIRE(v.size() == 5); REQUIRE(v.capacity() >= 10); } } WHEN("less capacity is reserved") { v.reserve(0); THEN("neither size nor capacity are changed") { REQUIRE(v.size() == 5); REQUIRE(v.capacity() >= 5); } } } } سيُبلَغ عن هذه الاختبارات على النحو التالي عند التشغيل: Scenario: vectors can be sized and resized Given: A vector with some items When: more capacity is reserved Then: the capacity changes but not the size التحليل الساكن Static analysis التحليل الساكن طريقة لتفحّص الشيفرة للبحث عن الأنماط التي ترتبط عادة بالأخطاء المشهورة وتدل عليها، هذه التقنية تستغرق وقتًا أقل من مراجعة الشيفرة، لكن تقتصر عمليات التحقق التي تقوم بها على الحالات المُبرمجة في الأداة. وتتضمّن عمليات مثل التحقّق استخدام الفاصلة المنقوطة بشكل غير صحيح بعد عبارة ‎(if (var);)‎ مثلًا، وخوارزميات الشُّعَب (graph algorithms) المتقدمة التي تحدد ما إذا كان المتغيّر مُهيَّأً أم لا. تحذيرات المُصرِّف Compiler warnings من السهل تمكين التحليل الساكن، فهناك إصدارات مُبسّطة مُضمّنة سلفًا في مصرّفك: clang++ -Wall -Weverything -Werror ...‎ g++ -Wall -Weverything -Werror ...‎ cl.exe /W4 /WX ...‎ إذا مكّنتَ هذه الخيارات، ستلاحظ أنّ كل مُصرِّف سيعثر على أخطاء لا تنتبه إليها المصرّفات الأخرى، وستحصل على أخطاء على تقنيات قد تكون صالحة في سياقات محدّدة. فمثلًا، قد تكون ‎while (staticAtomicBool);‎ مقبولة حتى لو لم تكن ‎while (localBool);‎ كذلك. لذلك، على عكس مراجعة الشيفرة، فأنت تستخدم أداة تفهم شيفرتك، وتنبّهك إلى الكثير من الأخطاء، وأحيانًا قد لا تتفق معك. قد تضطر إلى وقف التحذير محليًا في هذه الحالة نظرًا لأنّ الخيارات المذكورة أعلاه قد تُمكِّن جميع التحذيرات، بما في ذلك التحذيرات التي لا تريدها (مثلًا، لماذا يجب أن تكون شيفرتك متوافقة مع C++‎ 98؟). في هذه الحالة، يمكنك تعطيل تحذير بعينه: clang++ -Wall -Weverything -Werror -Wno-errortoaccept ...‎ ‎‎‎‎g++ -Wall -Weverything -Werror -Wno-errortoaccept ...‎‎ ‎‎‎‎cl.exe /W4 /WX /wd<no of warning>...‎‎ صحيح أنّ تحذيرات المصرّف ستساعدك أثناء التطوير، إلّا أنّها تُبطئ عملية التصريف، لهذا قد لا ترغب في تمكينها افتراضيًا في كل مرة، فإما أن تشغّل تحذيرات المصرّف افتراضيًا، أو يمكنك تمكين التكامل المستمر (continuous integration) باستخدام عمليات تحقق أكثر كلفة (أو كليهما). أدوات خارجية إذا قرّرت العمل بالتكامل المستمر (continuous integration)، فإنّ استخدام الأدوات الأخرى ليست ذات فائدة. وتحتوي أداة مثل clang-tidy على قائمة من عمليّات التحقق التي تغطي مجموعة واسعة من المشاكل، منها: الأخطاء الفعلية منع التشريح (Prevention of slicing) توكيدات (Asserts) مع آثار جانبية التحقّق من إمكانية القراءة إزاحات بادئة (indentation) غير صحيحة التحقق من تسمية المُعرِّف (Check identifier naming) التحقّق من التحديث (Modernization checks) استخدام make_unique()‎ استخدام nullptr *التحقّق من الأداء إيجاد النسخ غير الضرورية البحث عن خوارزميات الاستدعاء غير الفعّالة قد لا تكون القائمة كبيرة، إذ أنّ Clang تحتوي على الكثير من التحذيرات، إلّا أنها ستجعل شيفرتك أكثر أمانًا. أدوات أخرى توجد أدوات أخرى مماثلة بنفس الغرض، مثل: محلل visual studio الساكن كأداة خارجية clazy، إضافة خاصّة بمصرّف Clang للتحقق من شيفرات Qt الخلاصة توجد الكثير من أدوات التحليل الساكن لـ C++‎، سواء المضمّنة في المُصرِّف، أو الأدوات الخارجية، ولا تأخذ تجربتها الكثير من الوقت عند الاكتفاء بالعمل بالإعدادات السهلة، وستساعدك على رصد أخطاء قد لا تنتبه لها عند مراجعة الشيفرة. تحليل Segfault مع GDB سنستخدم نفس الشيفرة التي استخدمناها في المثال أعلاه. #include <iostream> void fail() { int *p1; int *p2(NULL); int *p3 = p1; if (p3) { std::cout << *p2 << std::endl; } } int main() { fail(); } أولًا، سنحاول تصريفها: g++ -g -o main main.cpp ثم نشغّلها بواسطة gdb: gdb ./main الآن، سنكون في صدفة gdb. اكتب الأمر run. (gdb) run The program being debugged has been started already. Start it from the beginning? (y or n) y Starting program: /home/opencog/code-snippets/stackoverflow/a.out Program received signal SIGSEGV, Segmentation fault. 0x0000000000400850 in fail () at debugging_with_gdb.cc:11 11 std::cout << *p2 << std::endl; لاحظ أنّ خطأ التجزئة (segmentation fault) قد حدث في السطر 11، لذا فالمتغيّر الوحيد المُستخدم في هذا السطر هو المؤشّر p2. يمكن التحقّق من محتواه بطباعة ما يلي: (gdb) print p2 $1 = (int *) 0x0 نلاحظ أنّ p2 هُيِّئ عند القيمة 0×0، والتي تكافئ NULL. في هذا السطر، حاولنا تحصيل (dereference) مؤشّر فارغ، لذا علينا إصلاح هذا الخلل. تنظيف الشيفرات يبدأ التنقيح بفهم الشيفرة التي تحاول تنقيحها. انظر المثال التالي عن شيفرة سيّئة: int main() { int value; std::vector < int > vectorToSort; vectorToSort.push_back(42); vectorToSort.push_back(13); for (int i = 52; i; i = i - 1) { vectorToSort.push_back(i *2); } /// مُحسّنة لاستخدامها في ترتيب المتجهات الصغيرة if (vectorToSort.size() == 1); else { if (vectorToSort.size() <= 2) std::sort(vectorToSort.begin(), std::end(vectorToSort)); } for (value: vectorToSort) std::cout << value << ' '; return 0; } هذه الشيفرة أفضل: std::vector < int > createSemiRandomData() { std::vector < int > data; data.push_back(42); data.push_back(13); for (int i = 52; i; --i) vectorToSort.push_back(i *2); return data; } /// مُحسّنة لاستخدامها في ترتيب المتجهات الصغيرة void sortVector(std::vector &v) { if (vectorToSort.size() == 1) return; if (vectorToSort.size() > 2) return; std::sort(vectorToSort.begin(), vectorToSort.end()); } void printVector(const std::vector<int> &v) { for (auto i: v) std::cout << i << ' '; } int main() { auto vectorToSort = createSemiRandomData(); sortVector(std::ref(vectorToSort)); printVector(vectorToSort); return 0; } بغض النظر عن نمط التشفير الذي تفضله، سيساعدك الثّبات على نفس النمط على فهم شيفرتك، ويمكن رصد فرصتين لتحسين قراءة الشيفرة بالنظر إلى الشيفرة أعلاه، وكذلك تسهيل تنقيحها: استخدام دوال منفصلة للإجراءات المنفصلة يتيح لك استخدام دوال منفصلة تخطّي بعض الدوال في المنقّح إذا لم تكن مهتمًّا بتفاصيلها. قد لا تكون مهتمًا بتفاصيل إنشاء البيانات أو طباعتها في هذه الحالة الخاصّة، وقد ينصبّ اهتمامك على عملية الترتيب وحَسب. هناك ميزة أخرى، وهي أنّ الشيفرة التي عليك قراءتها وحفظها ستكون أقصر عند تفحّص الشيفرة، إذ ستحتاج الآن إلى قراءة 3 أسطر فقط من الشيفرة في دالة ‎main()‎ لفهمها، بدلًا من قراءة الدالّة بأكملها. والميزة الثالثة هي أنّه نظرًا لكون الشيفرةً المُتحقَّق منها أقصر، فإن عينك ستتدرب الآن على رصد الأخطاء في غضون ثوانٍ. استخدام تنسيقات وإنشاءات متّسقة استخدامُ التنسيقات والإنشاءات المتّسقة ينظّم الشيفرةَ ويسهّل تحليلها، وهناك نقاشات كثيرة حول طريقة التنسيق "الأمثل". لكن بغضّ النظر فالمهمّ أن تلتزم بنمط برمجة واحد في شيفرتك، فذلك سيحسّن قراءتها ويسهّل التركيز على محتوى الشيفرة. ويوصى باستخدام أداة مخصّصة لتنسيق الشيفرة نظرًا لأنّ هذه المهمّة قد تستغرق وقتًا طويلاً، وتدعم معظم بيئات التطوير المتكاملة (IDEs) هذه الميزة بشكل أو بآخر. قد تلاحظ أنّ نمط البرمجة لا يقتصر على استخدام المسافات الفارغة والسطور الجديدة، فلم نعد نمزج بين النمط الحر (free-style) والدوال التابعة لبدء / إنهاء الحاويات (‎v.begin()‎ و ‎std::end(v)‎). تسليط الضوء على الأجزاء الهامّة من شيفرتك بغض النظر عن النمط الذي تستخدمه، فإنّ الشيفرة أعلاه تحتوي على عدد من الملاحظات التي قد تعطيك تلميحًا حول الأمور المهمّة في الشيفرة: تعليق يتضمّن الكلمة "مُحسَّن" ، في إشارة إلى استخدام بعض التقنيات الجذابة. بعض تعليمات return المبكّرة في ‎sortVector()‎ تشير إلى أنّنا نريد فعل شيء خاصّ. ‎std::ref()‎ - تشير إلى أنّ شيئًا ما يحدث في ‎sortVector()‎. الخلاصة الشيفرات النظيفة أسهل في الفهم وتقلل الوقت اللازم لتنقيحهَا، فقد يتمكّن مُراجِع الشيفرة من رصد الأخطاء منذ الوهلة الأولى في المثال الثاني، بينما قد تكون الأخطاء أخفَى في المثال الأوّل. (ملاحظة الخطأ موجود في: عملية الموازنة مع العدد ‎2‎.) هذا الدرس جزء من سلسلة دروس عن C++‎. ترجمة -بتصرّف- للفصل Chapter 142: C++ Debugging and Debugprevention Tools & Techniques والفصل Chapter 141: Unit Testing in C++‎ من كتاب C++ Notes for Professionals
  7. ينبغي تصريف البرامج المكتوبة بلغة C++‎ قبل أن تتمكن تلك البرامج من العمل، وستجد مجموعة كبيرة ومتنوّعة من برامج التصريف أو المصرِّفات (compilers) المتاحة والمناسبة لنظام التشغيل الذي تعمل به. التصريف بواسطة GCC التصريف دون تحسين مفيد في المراحل الأولى من التطوير والتنقيح، على الرغم من أنّ خيار ‎-Og‎ موصىً به في الإصدارات الحديثة من GCC، وبناء عليه فيمكن تصريف وربط ملف تنفيذي وغير محسَّن على افتراض أن هناك ملفًا مصدريًا واحدًا يسمى main.cpp، وذلك على النحو التالي: g++ - o app - Wall main.cpp - O0 ولإنتاج ملف مُحسّن قابل للتنفيذ لاستخدامه في الإنتاج، استخدم أحد خيارات ‎-O‎ (راجع: ‎،-O1‎، ‎-O2‎، ‎-O3‎، ‎-Os‎، ‎-‎‎Ofast‎): g++ - o app - Wall - O2 main.cpp في حال حذف الخيار ‎-O، سيُستخدَم ‎-O0 الذي يعني إلغاء التحسينات، كخيار افتراضي، واعلم أن تحديد ‎-O بدون رقم يجعله مساويًا لـ ‎-O1. وكخيار بديل، يمكن أن نستخدم رايات التحسين (optimization flags) من مجموعات ‎O‎ -أو تحسينات تجريبية أخرى- مباشرةً. انظر المثال التالي حيث يبني البرنامج مع التحسين ‎-O2‎، بالإضافة إلى راية واحدة من مستوى التحسين ‎-O3‎: g++ -o app -Wall -O2 -ftree-partial-pre main.cpp لإنتاج ملفّ تنفيذي مُحسّن لأجل منصة معيّنة -لاستخدامه على جهاز له نفس المعمارية-، استخدم: g++ -o app -Wall -O2 -march=native main.cpp سوف ينتُج عن الشيفرَتين أعلاه ملفٌّ ثنائي (binary file) يمكن تشغيله باستخدام ‎.\app.exe‎ على Windows، أو باستخدام ‎./app‎ على Linux و Mac OS، كما يمكن أيضًا تخطي الراية ‎-o‎، وفي هذه الحالة سينشئ GCC الملفّ التنفيذي الافتراضي ‎a.exe‎ على Windows و ‎a.out‎ على الأنظمة الشبيهة بيونكس (Unix-like). لأجل تصريف ملف دون ربطه، استخدم الخيار ‎-c‎: g++ -o file.o -Wall -c file.cpp سينتج عن هذا ملفٌّ يحمل الاسم ‎file.o‎، والذي يمكن ربطه لاحقًا بملفّات أخرى لإنتاج ملف ثنائي: g++ -o app file.o otherfile.o انظر gcc.gnu.org للمزيد من التفاصيل حول خيارات التحسين، خاصة ‎-Og‎، وهو التحسين مع التركيز على تجربة التنقيح (debugging)، الذي يوصى باستخدامه في دورات تحرير-تصريف-تنقيح القياسية - standard edit-compile-debug cycle)، وكذلك ‎-Ofast‎ الذي يشمل جميع التحسينات، بما في ذلك تلك التي تتجاهل الامتثال الصارم للمعايير. تُمكِّنك راية ‎-Wall‎ من إطلاق تحذيرات من بعض الأخطاء الشائعة، وينبغي أن تُستخدم دائما، ويوصى باستخدام ‎-Wextra‎ لتحسين جودة الشيفرة، وكذلك رايات التحذير الأخرى التي لا تُمكَّن تلقائيًا من قِبل ‎-Wall‎ و ‎-‎‎Wextra‎. إذا كانت الشيفرة تتوقع معيارًا محدّدًا للغة C++‎، فيمكنك تحديد المعيار الذي تريد استخدامه عن طريق تضمين راية ‎-std=‎. وتتوافق القيم المدعومة مع سنة الإصدار النهائي لمعيار ISO C++‎، واعتبارا من GCC 6.1.0 فإنّ القيم الصالحة لـ ‎std=‎ هي: ‎c++98‎ / ‎c++03‎، ‎c++11‎، ‎c++14‎، و‎c++17‎ / ‎c++1z‎. لاحظ أن القيم المفصولة بشرطةٍ مائلة / متكافئة. g++ -std=c++11 <file> يتضمّن GCC بعض الإضافات (Extensions) (extensions) الخاصة بالمُصرِّف، والتي تُعطَّل عندما تتعارض مع معيار قياسي محدّد من قبل راية ‎-std=‎، فإذا أردت التصريف مع كل الإضافات (Extensions) المُمَكَّنة، فاستخدام القيمة ‎gnu++XX‎، حيث تمثّل ‎XX‎ أيًّا من السنوات المذكورة أعلاه. ويُستخدَم المعيار الافتراضي في حالة عدم تعريف أيّ منها، وبالنسبة لإصدارات GCC التي تسبق 6.1.0 فإنّ الإعداد الافتراضي هو ‎-‎std‎ = ‎gnu‎++03، أما في GCC 6.1.0 والإصدارات الأحدث فإن الإعداد الافتراضي هو ‎-std=gnu++14‎. لاحظ أنّه نظرا لوجود بعض الأخطاء (bugs) في GCC، فيجب أن تكون راية pthread- حاضرة في التصريف والربط إن أردت أن يدعم GCC معيار الخيوط (threading) الذي أُدخل في C++‎11، مثل ‎std::thread‎ و ‎std::wait_for‎. قد لا ينتج عن حذفها عند استخدام الخيوط أيّ تحذيرات، ولكن قد تحصل على نتائج غير صحيحة في بعض المنصّات. الربط بالمكتبات Linking with libraries استخدم خيار ‎-l‎ لتمرير اسم المكتبة: g++ main.cpp -lpcre2-8 # 8bit code units (UTF-8) في وحدات الشيفرات ثمانية البتّات PCRE2 هي مكتبة pcre2-8 إذا لم تكن المكتبة موجودة في مسار المكتبة القياسية، فأضف المسار باستخدام ‎-L‎: g++ main.cpp -L/my/custom/path/ -lmylib يمكن ربط عدّة مكتبات معًا: g++ main.cpp -lmylib1 -lmylib2 -lmylib3 إذا كانت إحدى المكتبات تعتمد على مكتبة أخرى، فضع المكتبة المعتِمدة قبل المكتبة المستقلة: g++ main.cpp -lchild-lib -lbase-lib أو اترك الرابط (linker) يتكفّل بتحديد الترتيب عبر الخيارات ‎--start-group‎ و ‎--end-group‎ (ملاحظة هذا له تكلفة كبيرة على الأداء): g++ main.cpp -Wl,--start-group -lbase-lib -lchild-lib -Wl,--end-group التصريف باستخدام Visual Studio (واجهة رسومية) نزِّل Visual Studio Community 2015 وثبِّته. افتح Visual Studio Community . انقر على File، ثم NewK ثم Project. انقر على Templates ثم ++Visual C ثم Win32 Console Application، ثم ضع اسمًا للمشروع وليكن MyFirstProgram. انقر على Ok. انقر على "Next " في النافذة التالية. اختر الخانة ‎Empty project‎ تحت الخيارات الإضافية (Additional options) ثم انقر على "Finish": انقر بالزر الأيمن فوق مجلد ملف المصدر ثم ‎Add، ثم New Item: حدّد ملف C++‎ وقم بتسمية main.cpp، ثم انقر فوق Add: انسخ والصق الشيفرة التالية في الملف الجديد main.cpp: #include <iostream> int main() { std::cout << "Hello World!\n"; return 0; } ينبغي أن تبدو بيئة العمل لديك كما يلي: انقر على Debug ثم Start Without Debugging، (أو اضغط على ctrl + F5): يجب أن تحصل على الخرج التالي في سطر الأوامر: مُصرِّفات الشبكة Online Compilers توفّر العديد من المواقع مصرّفات C++‎ يمكن الوصول إليها عبر شبكة الإنترنت، وتختلف مزايا وإمكانيات مصرّفات الشبكة من موقع إلى آخر، ولكنها عادة ما تسمح بالقيام بما يلي: لصق الشيفرة في نموذج في المتصفّح. تحديد بعض خيارات المصرّف، وتصريف الشيفرة. الحصول على خرج المصرّف و/أو البرنامج. عادةً ما تكون المُصرِّفات الشبكية مقيّدة إذ أنّها تتيح لأيّ شخص تشغيل المصرّف وتنفيذ شيفرات عشوائية على الخادم، وهذا قد يكون مصدر خطر في حال تنفيذ شيفرات ضارة. قد تكون مصرّفات الشبكة مفيدة للأغراض التالية: تشغيل مقتطف صغير من شيفرة جهاز يفتقر إلى مصرّف C++‎ (مثل الهواتف الذكية والأجهزة اللوحية وما إلى ذلك). التحقّق من أنّ الشيفرة تُصرَّف بنجاح في مختلف المُصرِّفات، وتعمل بنفس الطريقة بغض النظر عن المُصرِّف الذي صُرِّفت فيه. تعلُّم أو تعليم أساسيات C++‎. تعلّم ميزات C++‎ الحديثة (C++‎ 14 و C++‎ 17 في المستقبل القريب) إذا لم يكن لديك مصرّف C++‎ حديث على جهازك. رصد الأخطاء التي يمكن أن تكون موجودة في المصرّف الذي تعمل به بمقارنته بمجموعة كبيرة من المصرّفات الأخرى. التحقق ممّا إذا كانت الإصدارات اللاحقة من المصرّف الذي تعمل قد صُحِّحت في حال لم تتوفر تلك الإصدارات على جهازك. حل المشاكل والتمارين عبر الشبكة. بالمقابل، لا ينبغي استخدام مُصرِّفات الشبكة لأجل: تطوير برامج كاملة (ولو كانت صغيرة) باستخدام C++‎، وفي العادةً فإن مصرّفات الشبكة لا تسمح بالارتباط مع مكتبات خارجية، أو تنزيل أدوات البناء. إجراء حسابات مكثفة. موارد الخادم الحسابية محدودة، لذلك سيوقَف أيّ برنامج يقدّمه المستخدم بعد بضع ثوانٍ من بدء تنفيذه، فوقت التنفيذ المسموح به يكفي عادة للاختبار والتعلم فقط. قرصنة ومهاجمة الخادم الذي يعمل عليه المُصرِّف، أو أيّ جهة خارجية مُستضافة على الشبكة. أمثلة على المصرفات الشبكية: codepad.org: مُصرِّف مع إمكانية مشاركة الشيفرة، وتحرير الشيفرة بعد التصريف، لكن التحذيرات و الأخطاء لا تعمل بشكل جيد. coliru.stacked-crooked.com: مصرّف يمكّنك من أن تحدّد سطر الأوامر، ويتيح لك اختيار أحد المُصرِّفين GCC أو Clang. cpp.sh: مُصرِّف يدعم C++‎ 14، لا يسمح لك بتحرير سطر أوامر المصرّف، ولكن يوفّر بعض الخيارات عبر عناصر التحكم في واجهة المستخدم الرسومية. gcc.godbolt.org: يوفر قائمة واسعة من إصدارات المُصرِّف، والمعماريات، وهو مفيد للغاية لمن يحتاج إلى التحقّق من عمل الشيفرة في عدّة مُصرِّفات مختلفة. والمصرفات التالية هي بعض المصرّفات المتاحة: GCC و Clang و MSVC ومصرّف Intel و ELLCC و Zapcc، مع توفّر واحد أو أكثر من هذه المصرّفات للمعماريّات ARM و ARMv8 (مثلARM64) و Atmel AVR و MIPS و MIPS64 و MSP430 و PowerPC و x86 و x64. أيضًا، يمكن تعديل وسائط سطر الأوامر الخاص بالمُصرِّف. ideone.com: يُستخدم على نطاق واسع لتوضيح سلوك الشيفرة، ويوفر كلًّا من GCC و Clang، لكنّه لا يسمح بتحرير سطر أوامر المصرّف. melpon.org/wandbox: يدعم العديد من إصدارات Clang و GNU / GCC. onlinegdb.com: بيئة تطوير متكاملة لكن محدودة، تتضمّن محررًا ومُصرِّفًا (gcc) ومنقّحًا (gdb). rextester.com: يوفر المُصرِّفات Clang و GCC و Visual Studio لكل من C و C++‎ (إضافة إلى مُصرِّفات خاصّة بلغات أخرى)، ومكتبة Boost. tutorialspoint.com/compile_cpp11_online.php: صدفة UNIX متكاملة، مع مصرّف GCC، ومستكشف مشاريع سهل الاستخدام. webcompiler.cloudapp.net: مُصرِّف لبرنامج Visual Studio 2015، مُقدّم من قبل Microsoft كجزء من RiSE4fun. التصريف باستخدام Visual C++‎ (سطر الأوامر) بالنسبة للمبرمجين الذين اعتادوا على العمل بالمصرّفَين GCC أو Clang، والذين يودّون الانتقال إلى Visual Studio، أو المبرمجين الذين يفضّلون العمل بسطر الأوامر بشكل عام، يمكن استخدام مصرّف Visual C++‎ من سطر الأوامر إلى جانب بيئة تطوير متكاملة (IDE). وإذا كنت ترغب في تصريف شيفرتك من سطر الأوامر في Visual Studio، فسيكون عليك إعداد بيئة سطر الأوامر، عن طريق فتح: Visual Studio Command Prompt/Developer Command Prompt/x86 Native Tools Command Prompt/x64 Native Tools Command Prompt أو شيئٍا من هذا القبيل (يختلف الأمر حسب إصدار Visual Studio الذي تعمل به)، أو في موجّه الأوامر، من خلال الانتقال إلى المجلّد الفرعي ‎VC‎ في مجلّد التثبيت الخاصّ بالمصرّف (يكون عادةً ‎\Program Files (x86)\Microsoft Visual Studio x\VC‎، حيث يمثّل ‎x‎ رقم الإصدار (مثل ‎10.0‎ لعام 2010، أو ‎14.0‎ لعام 2015)، ثمّ تنفيذ الملفّ ‎VCVARSALL‎ مع مُعامل سطر الأوامر المُحدّد هنا. لاحظ أنّه على عكس GCC، فإنّ Visual Studio لا يوفّر واجهة للرابط (‎link.exe‎) عبر المصرّف (‎cl.exe‎)، ولكنّها توفّر الرابط (linker) كبرنامج منفصل، ويستدعيه االمُصرِّف عند الإنهاء. ويمكن استخدام ‎cl.exe‎ و ‎link.exe‎ بشكل منفصل مع عدّة ملفّات وخيارات، أو إخبار ‎cl‎ بتمرير الملفّات والخيارات إلى ‎link‎ إذا كانت المهمّتان ستُنجزان معًا، وستُترجم خيارات الربط المُحدّدة لـ ‎cl‎ إلى خيارات خاصة بـ ‎link‎، وتُمرّر الملفات غير المُعالجة بواسطة ‎cl‎ مباشرة إلى الرابط ‎link‎. انظر هذه الصفحة للمزيد عن وسائط ‎link‎. لاحظ أنّ الوسائط التي تخصّ ‎cl‎ حسّاسة لحالة الأحرف، وذلك على خلاف وسائط ‎link‎، وأن بعض الأمثلة التالية تستخدم المتغيّر "current directory" الخاصّ بصدفة (shell) ويندوز، ‎%cd%‎، عند تحديد أسماء المسار المطلق (absolute path names)، ويُوسَّع هذا المتغيّر إلى مجلّد العمل الحالي (current working directory). وفي سطر الأوامر، سيكون هو المجلّد الذي كنت تستخدمه عند تشغيل ‎cl‎، وهو محدَّد في موجّه الأوامر - command prompt - افتراضيًا (مثلًا، في موجّه الأوامر ‎C:\src>‎، فإنّ ‎%cd%‎ سيكون ‎C:\src\‎). يمكنك تصريف وربط ملف تنفيذي غير مُحسَّن (unoptimised executable) بافتراض أنّ هناك ملفّا مصدريًا واحدًا يُسمّى ‎main.cpp‎ في المجلد الحالي، وهذا مفيد في مرحلة التطوير الأولي والتنقيح، عبر استخدام أيٍّ ممّا يلي: cl main.cpp // "main.obj" إنشاء ملف // "main.obj" إجراء الربط مع // "main.exe" إنشاء الملف القابل للتنفيذ cl /Od main.cpp في المثال السابق، سيتصرف cl /Od main.cpp كسابقه، لكن يكون "Od/" هو خيار "Optimisation: disabled"، ويكون الخيار الافتراضي عند عدم تحديد أي من خيارات "O/". بافتراض أنّ هناك ملفًّا مصدريًا إضافيًا " niam.cpp " في نفس المجلّد، فستنشئ الشيفرة التالية الملفين "main.obj" و "niam.obj"، ثم يجري الربط معهما، ومن ثم ينشئ الملف التنفيذي "main.exe": cl main.cpp niam.cpp يمكنك أيضًا استخدام محارف البدل (wildcards)، ستنشئ الشيفرة التالية ملف كائن "main.obj" إضافة إلى ملف كائن لكل ملف cpp. في المجلد التالي: "%cd%\src" ثم تُجري الربط مع "main.obj" وكل ملف كائن مُنشأ، وستكون كل ملفات الكائن في المجلد الحالي، ثم تولِّد ملف main.exe. cl main.cpp src\* .cpp لإعادة تسمية الملف القابل للتنفيذ أو نقله، استخدم أحد الخيارات التالية: cl /o name main.cpp // "name.exe" تولد ملف قابل للتنفيذ cl /o folder\ main.cpp // "%cd%\folder" في المجلد "main.exe" إنشاء ملف cl /o folder\name main.cpp // "%cd%\folder" في المجلد "main.exe" إنشاء ملف cl /Fename main.cpp // "/o name" مثل cl /Fename main.cpp // "/o folder\" مثل cl /Fefolder\name main.cpp // "/o folder\name" مثل يمرِّر كلٌّ من ‎/o‎ و ‎/Fe‎ معاملاتِهما (دعنا نسميها ‎o-param‎) إلى ‎link‎ على شكل ‎/OUT:o-param‎، مع إلحاق الامتداد المناسب (بشكل عام ‎.exe‎ أو ‎.dll‎) لتسمية ‎o-param‎ بما يُناسب، في حين أنّ لـ ‎/o‎ و ‎/Fe‎ نفس الوظيفية - على حدّ علمي - إلّا أنّ الأخير هو المفضل في Visual Studio. لقد أصبحت ‎/o‎ مهمَلة، ويبدو أنّها تًقدّم بشكل أساسي للمبرمجين الذين اعتادوا العمل بالمصرّفَين GCC و Clang. لاحظ أنّه على الرغم من أنّ المسافة الفارغة بين ‎/o‎ واسم المجلد المحدّد اختيارية، إلا أنه لا يمكن وضع مسافة بيضاء بين ‎/Fe‎ واسم المجلد المحدّد. بالمثل، يمكن إنشاء ملف تنفيذي مُحسّن (لاستخدامه في مرحلة الإنتاج) على النحو التالي: سنحسِّن حجم الملف التنفيذي في السطر الأول لكن قد يكون هذا على حساب سرعة التنفيذ، أما في السطر الثاني فيحسِّن سرعة البرنامج، لكن ربما يجعل هذا حجم الملف أكبر. cl /O1 main.cpp cl /O2 main.cpp هنا ننشئ ملفات خاصة لتحسين البرنامج، مما يسمح لسطر الأوامر أن يأخذ كل وحدات الترجمة في حسبانه خلال عملية التحسين، وسيمرر خيار توليد شيفرة الرابط-الوقت (Link-Time Code Generation) أو LTCG/، إلى الرابط LINK، ليخبره باستدعاء سطر الأوامر خلال مرحلة الربط لإجراء تحسينات إضافية. وينبغي ربط كائن الملف المُنشأ مع LTCG/ في حال عدم إجراء الربط في ذلك الوقت، كما يمكن استخدامها مع خيارات التحسين الأخرى في سطر الأوامر، … cl /GL main.cpp other.cpp أخيرًا، لإنتاج ملف تنفيذي مُحسَّن لأجل منصّة معيّنة (لاستخدامه في الإنتاج على جهاز ذي معمارية معيّنة)، فعليك اختيار موجّه الأوامر المناسب، أو مُعامل ‎VCVARSALL‎ المناسب للمنصّة المُستهدفة. ويجب أن يرصُد الرابط (‎link‎) المنصّة المطلوبة من ملفّات الكائن؛ وإلّا، فعليك استخدام الخيار ‎/MACHINE‎ لتعريف المنصّة المُستهدفة بشكل صريح. مثال: إذا أردت التصريف للتنفيذ على معمارية 64 بت، وتعذر على الرابط LINK أي يرى المنصة المستهدفة: cl main.cpp / link / machine: X64 سينتج أيًا مما سبق ملفًا تنفيذيًا يحمل الاسم المحدّد من قِبل ‎/o‎ أو ‎/Fe‎، وفي حالة عدم توفير أيّ منهما، سيحمل اسمًا مطابقًا للملف المصدري الأول أو ملفِّ الكائن المحدّد للمُصرِّف. cl a.cpp b.cpp c.cpp // "a.exe" إنشاء cl d.obj a.cpp q.cpp // "d.exe" إنشاء cl y.lib n.cpp o.obj // "n.exe" إنشاء cl / o yo zp.obj pz.cpp // "yo.exe" إنشاء لتصريف الملفّات دون ربط: cl / c main.cpp // "main.obj" إنشاء ملف تخبر الشيفرةُ السابقة سطرَ الأوامر ‎cl‎ بالخروج دون استدعاء ‎link‎، وتنتج ملفّ كائن يمكن ربطه لاحقًا بالملفّات الأخرى لإنتاج ملف ثنائي. في الشيفرة التالية، ينشئ السطر الأول ملف الكائن niam.obj، ويجري الربط مع niam.obj و main.obj، ثم ينشئ الملف التنفيذي main.exe. ويجري السطر الثاني ربطًا مع niam.obj و main.obj، ثم ينشئ الملف التنفيذي main.exe. cl main.obj niam.cpp link main.obj niam.obj هناك مُعاملات أخرى مهمّة لسطر الأوامر، ومن المفيد جدًا معرفتها، منها ما يلي: cl /EHsc main.cpp: يشير EHsc إلى أنه لن تُمسك إلا اعتراضات ++C القياسية، وأن دوال C الخارجية لن ترفع اعتراضات (exceptions). يوصى بهذا المعامل لمن يريد كتابة شيفرة محمولة ومتعددة المنصات. cl /clr main.cpp: يشير clr/ إلى أن الشيفرة يجب أن تُصرَّف لاستخدام اللغة المشتركة لوقت التشغيل، وهي آلة وهمية خاصة بإطار عمل NET.، وتسمح باستخدام لغة C++/CLI الخاصة بميكروسوفت إضافة إلى سطر أوامر ++C، وتنشئ ملفًا تنفيذيًا يتطلب NET. ليعمل. cl /Za main.cpp: يشير Za/ إلى وجوب تعطيل إضافات (Extensions) ميكروسوفت، وأن الشيفرة يجب أن تُصرَّف وفق مواصفات ISO للغة ++C حصرًا، وهذا ضروري لضمان محمولية البرنامج. cl /Zi main.cpp: يولد Zi/ ملف قاعدة بيانات PDB للبرنامج من أجل استخدامه عند تنقيح برنامج ما دون التأثير على مواصفات التحسين، ويُمرَّر خيار DEBUG/ إلى الرابط LINK. cl /LD main.cpp: يخبر LD/ سطرَ الأوامر أن يضبط LINK كي يولد ملف DLL بدلًا من ملف تنفيذي، وسينتج الرابط ملف DLL إضافة إلى ملفي EXP و LIB لاستخدامهما عند الربط. مرر ملف LIB المرتبط بملف DLL إلى سطر الأوامر أو الرابط عند تصريف تلك البرامج، وذلك لاستخدام الأخير في برامج أخرى. cl main.cpp /link /LINKER_OPTION: يمرر link/ كل ما بعده إلى الرابط مباشرة دون تحليل. استبدل LINKER_OPTION/ بخيارات الرابط التي تريد. بالنّسبة للذين لديهم خبرة في التعامل مع اليونكسات وأشباهها، و/أو GCC / Clang و ‎cl‎ و ‎link‎، وأدوات سطر أوامر Visual Studio الأخرى، يمكنهم أن يقبلوا المُعاملات المُحدّدة باستخدام الواصلة - (مثل ‎-c‎) بدلًا من الشرطة المائلة (مثل ‎/c‎). إضافة إلى ذلك، يتعرّف نظام ويندوز على الشرطة المائلة العكسية \ (backslash) أو الأمامية / (slash) ويعدّهما فواصل صالحة للمسارات، لذلك يمكن استخدام المسارات التي على نمط يونكس وما شابهه. يسهّل هذا تحويل أسطر أوامر المصرّف من ‎g++‎ أو ‎clang++‎ إلى ‎cl‎، أو العكس، بالحد الأدنى من التغييرات. g++ - o app src / main.cpp cl - o app src / main.cpp ستحتاج إلى البحث عن أوامر مكافئة في توثيق المصرّف الذي تستخدمه و/أو في مواقع توثيق أخرى، وذلك عند محاولة نقل سطور الأوامر التي تستخدم خيارات معقّدة لـ ‎g++‎ أو ‎clang++‎. وإن احتجت إلى استخدام ميزات لغة معيّنة في شيفرتك، فيلزم استخدام إصدار محدّد من MSVC. ومن الممكن في Visual C++‎ 2015 التحديث 3 اختيار إصدار المعيار الذي تريد اعتماده عند التصريف عبر الراية ‎/std‎، والقيمُ الممكنةُ هي ‎/std:c++14‎ و ‎/std:c++latest‎ (ستتبعُهما ‎/std:c++17‎ قريبًا). ملاحظة في الإصدارات القديمة من هذا المُصرِّف، كانت بعض رايات المزايا متوفرة، بيْد أنّها كانت في الغالب تُستخدم لمعاينة الميزات الجديدة. التصريف باستخدام Clang بما أن الواجهة الأمامية لمصرّف Clang مُصمّمة لتكون متوافقة مع GCC، فإنّ معظم البرامج التي يمكن تصريفها عبر GCC ستُصرَّف كذلك عند استبدال ‎clang++‎ بـ‎g++‎ في برامج البناء النصية (build scripts)، وفي حال عدم إعطاء ‎-std=version‎، فسيُستخدَم الإصدار gnu11. يمكن لمستخدمي ويندوز الذين اعتادوا على MSVC استبدال ‎clang-cl.exe‎ بـ ‎cl.exe‎. واعلم أن clang يحاول بشكل افتراضي أن يكون متوافقًا مع أحدث إصدار مُثبّت من MSVC. ويمكن استخدام clang-cl عبر تغيير ‎Platform toolset‎ في خاصّيات المشروع، وذلك عند التصريف باستخدام visual studio. وفي كلتا الحالتين، سيكون clang متوافقًا عبر واجهته الأمامية وحسب، لكنه سيحاول أيضًا بناء ملفات ثنائية متوافقة. كذلك يجب على مستخدمي clang-cl أن ينتبهوا إلى أنّ التوافق مع MSVC ليس كاملًا. لاستخدام clang أو clang-cl، يمكن استخدام التثبيت الافتراضي على توزيعات محدّدة من Linux، أو تلك المُجمَّعة في بيئات التطوير المتكاملة - IDEs - (مثل XCode على Mac). أما بالنسبة للإصدارات الأخرى من هذا المُصرِّف، أو في المنصّات التي لم يُثبّت عليها، فيمكن تنزيله من صفحة التنزيل الرسمية. إذا كنت تستخدم CMake لبناء شيفرتك، فيمكنك تبديل االمُصرِّف عادة عن طريق تعيين متغيّرَي البيئة ‎CC‎ و ‎CXX‎ على النحو التالي: mkdir build cd build CC=clang CXX=clang++ cmake .. cmake --build . عملية التصريف في C++‎ بعد تطوير برنامج بلغة C++‎، فإنّ الخطوة التالية هي تصريف ذلك البرنامج قبل تشغيله. والتصريف (compiling) هو العملية التي تحوّل البرنامج المكتوب بلغة قابلة للقراءة البشرية، مثل C و C++‎ وغيرهما، إلى شيفرة الآلة، وهي شيفرة يمكن أن تُفهمها وحدة المعالجة المركزية مباشرة. على سبيل المثال، إذا كان لديك ملفّ مصدَري مكتوب بلغة C++‎ باسم prog.cpp، وقمت بتنفيذ الأمر compile … g++ -Wall -ansi -o prog prog.cpp ستحدث 4 مراحل رئيسية عند إنشاء ملف تنفيذي من الملف المصدري. يأخذ معالج C++‎ الأولي ملفّ الشيفرة المصدرية ويتعامل مع الترويسات (include#)، ووحدات الماكرو (‎#define‎) وموجّهات المعالج الأخرى. تُصرّف شيفرة C++‎ المصدرية المُوسّعة التي تم إنتاجها من قبل معالج C++‎ الأوّلي إلى لغة التجميع (assembly) المناسبة للمنصّة. تُصرَّف الشيفرة المجمّعة التي تم إنتاجها بواسطة المصرّف إلى كائن شيفرة (object code) مناسب للمنصة. يُربط ملفّ كائن الشيفرة المُنتج من قبل المجمّع مع ملفّات كائنات الشيفرة الخاصة بمكتبات الدوال المُستخدمة، وذلك لإنتاج مكتبة أو ملف قابل للتنفيذ. المعالجة الأولية Preprocessing يُعالج المعالج الأولي تعليمات المعالج، مثل ‎#include و ‎#define، وهو غير واعٍ بصيغة C++‎، لذا يجب توخّي الحذر عند استخدامه. يعمل المعالج الأوّلي على ملفّ ++C مصدري واحد عن طريق استبدال محتوى الملفّات المناسبة (والتي تكون عادة مجرّد تصريحات) بتعليمات ‎#include، ويستبدل وحدات الماكرو (‎#define)، ويختار عدة أجزاء من النص بحسب تعليمات ‎#if و ‎#ifdef و ‎#fndef. يعمل المعالج الأوّلي على مجموعة من وحدات المعالجة الأوّلية (preprocessing tokens)، ويُعُرِّف استبدال الماكرو على أنّه استبدال مقاطع بمقاطع أخرى (يتيح المُعامل ## دمجَ شيفرَتين إذا كان ذلك مناسبًا). بعد كل هذا، يُنتج المعالج الأوّلي خرجًا واحدًا يمثّل مجرى من المقاطع الناتجة عن التحويلات الموضّحة أعلاه، ويضيف أيضًا بعض العلامات/الوسوم الخاصة التي تخبر المُصرِّف بالسطر الذي جاء منه كل مقطع، حتّى يتمكّن من صياغة رسائل الخطأ المناسبة. قد تنتج بعض الأخطاء في هذه المرحلة عند استخدام التعليمتين #if و ‎#error. ونستطيع إيقاف العملية في مرحلة المعالجة الأولية باستخدام راية المصرّف أدناه: g++ -E prog.cpp التصريف تُجرى خطوة التصريف على كل خرج من مخرجات المعالج الأوّلي (Preprocessor)، ويحلّل االمُصرِّف شيفرة C++‎ المصدرية الخالصة - دون أي تعليمات للمعالج الأولي الآن- ثمّ يحوّلها إلى شيفرة المجمّع، ثم يستدعي الواجهة الخلفية الأساسية (underlying backend)، وهي المجمّع في سلسلة الأدوات، التي تجمّع تلك الشيفرة وتحوّلها إلى شيفرة الآلة، وتنتج ملفًّا ثنائيًا وفق تنسيق معيّن (مثل ELF أو COFF أو a.out …). تحتوي ملفات الكائنات على الشيفرة المُصرَّفة (في شكل ثنائي) للرموز المُعرَّفة في المدخلات، ويشار إلى الرموز الموجودة في ملف الكائن بالاسم. وتستطيع ملفات الكائنات أن تشير إلى الرموز التي لم تُعرِّف بعد، كأن تستخدم تصريحًا بدون توفير تعريف له، فلا يمانع المُصرِّف ذلك، وسينتج ملفَّ الكائن بلا مشاكل طالما أنّ الشيفرة المصدرية مصاغة بشكل جيّد. تسمح لك المصرّفات عادةً بإيقاف عملية التصريف في هذه المرحلة، وهو أمر مفيد للغاية إذ يُمكِّنك من تصريف كل ملفّ مصدري بشكل منفصل، وعليه فلن تكون بحاجة إلى إعادة تصريف كل شيء إن لم تكن غيرت إلا ملفًّا واحدًا فقط. يمكن وضع ملفات الكائنات الناتجة في أرشيفات خاصة تسمّى المكتبات الساكنة (static libraries)، لتسهيل استخدامها لاحقًا، ويُبلَّغ في هذه المرحلة عن أخطاء المصرّف "المعتادة"، مثل أخطاء الصيغة، أو أخطاء فشل تحليل التحميل الزائد. لإيقاف العملية بعد خطوة التصريف، يمكن استخدام الخيار ‎-S: g++ -Wall -ansi -S prog.cpp التجميع Assembling ينشئ المجمّع شيفرة الكائن، وقد ترى على نظام UNIX ملفّات ذات لاحقة ‎.o (أو .OBJ على MSDOS) للدلالة إلى ملفات كائنات الشيفرَة (object code files). في هذه المرحلة، يحوّل المجمّع تلك الملفّات من شيفرة مُجمَّعة (assembly code) إلى تعليمات على مستوى الآلة، كما سيكون الملفُّ المُنشأ كائنَ شيفرة قابلة للنقل (relocatable object code)، ومن ثم ستنشئ مرحلةُ التصريف برنامجَ الكائن القابل للنقل، ويمكن استخدام هذا البرنامج في مواضع أخرى دون الحاجة إلى إعادة تصريفه مرّة أخرى. لإيقاف العملية بعد خطوة التجميع، استخدم الخيار‎-c: g++ -Wall -ansi -c prog.cpp الربط Linking الرابط (Linker) هو الذي ينتج مخرجات التصريف النهائية من كائنات الملفّات التي أنتجها المجمّع (Assembler)، وقد تكون المُخرجات إمّا مكتبة مشتركة (أو ديناميكية، رغم تشابه الأسماء، إلا أنّها تختلف عن المكتبات الساكنة المذكورة آنفًا)، أو يمكن أن تكون ملفّا تنفيذيًا. ويربط جميع الكائنات عن طريق استبدال العناوين الصحيحة بالمراجع التي تشير إلى رموز غير مُعرَّفة (undefined symbols). يمكن تعريف تلك الرموز في ملفّات كائنات أخرى أو في مكتبات أخرى، وإذا عُرِّفت في مكتبات غير المكتبة القياسية، فعليك إخبار الرابط بذلك. وتكون الأخطاء الأكثر شيوعًا في هذه المرحلة هي التعاريف المفقودة أو التعاريف المكرّرة، والأولى تعني أنّه إمّا أنّ التعاريف غير موجودة (أي أنّها غير مكتوبة)، أو أنّ ملفات الكائنات أو المكتبات التي عُرِّفت فيها لم تُعط للرابط، أمّا الأخطاء الأخيرة فهي واضحة: إذ أنّها تعني أنّ نفس الرمز قد عُرِّف في ملفّين أو مكتبتين مختلفتين. التصريف عبر Code::Blocks (واجهة رسومية) نزِّل Code::Blocks وثبِّته، وإذا كنت تستخدم ويندوز فاحرص على اختيار الملفّ الذي يحتوي اسمُه على ‎mingw‎، إذ لا تثبّت الملفّات الأخرى أي مُصرِّفات. افتح Code::Blocks وانقر على "Create a new project": اختر "Console application" وانقر على "Go": انقر على"Next"، واختر "C++‎"، انقر على "Next"، اختر اسمًا لمشروعك، واختر مجلدًا لحفظه، ثمّ انقر على "Next" ثمّ على "Finish". يمكنك الآن تعديل وتصريف شيفرتك، ستجد شيفرة افتراضية تطبع السلسلة النصية "Hello world!" في سطر الأوامر. اضغط على أحد الأزرار الثلاثة compile/run في شريط الأدواتل تصريف و/أو تشغيل البرنامج: للتصريف دون تشغيل، اضغط على ، وللتشغيل دون التصريف مرّة أخرى، اضغط على ، وللتصريف ثمّ التشغيل اضغط على . تصريف وتشغيل البرنامج الافتراضي "مرحبا العالم!" سيُنتِج الخرج التالي: مواصفات الربط Linkage specifications تخبر مواصفات الربط (linkage specification) المُصرِّفَ بأن يصرِّف التصريحات بطريقة تسمح بربطها بالتصريحات المكتوبة بلغة أخرى، مثل C. معالج الإشارات الخاصّ بأنظمة التشغيل المشابهة ليونيكس بما أن معالج الإشارة (signal handler) يُستدعى عن طريق النواة (kernel) باستخدام صيغة الاستدعاء في لغة C، فيجب أن نطلب من االمُصرِّف أن يستخدم تلك الصيغة عند تصريف الدالّة. volatile sig_atomic_t death_signal = 0; extern "C" void cleanup(int signum) { death_signal = signum; } int main() { bind(...); listen(...); signal(SIGTERM, cleanup); while (int fd = accept(...)) { if (fd == -1 && errno == EINTR && death_signal) { printf("Caught signal %d; shutting down\n", death_signal); break; } // ... } } إنشاء مكتبة بلغة C متوافقة مع C++‎ يمكن في العادةً تضمين ترويسات مكتبة C في برامج C++‎، لأنّ معظم التصريحات صالحة في كلا اللغتين. على سبيل المثال، انظر ملف ‎foo.h‎ التالي: typedef struct Foo { int bar; } Foo; Foo make_foo(int); سيصرَّف تعريف ‎make_foo‎ بشكل منفصل ويُوزّع مع الترويسة على هيئة كائن. يمكن لبرنامج C++‎ أن يُضمِّن ملف الترويسة ‎foo.h‎ عبر التعبير ‎#include <foo.h>‎، لكن لن يستطيع المُصرِّف أن يعرف أنّ الدالّة ‎make_foo‎ قد عُرِّفت وفق صيغة مكتوبة بلغة C، وسيحاول البحث عنه باسم آخر، وسيفشل في إيجاده. وحتى لو استطاع إيجاد تعريف ‎make_foo‎ في المكتبة، فليست كل المنصّات تستخدم نفس صيغات الاستدعاء في C و C++‎، وسوف يستخدم مصرّف C++‎ صيغة الاستدعاء الخاصة بـ C++‎ عند استدعاء ‎make_foo‎، وهذا من شأنه أن يتسبّب في حدوث خطأ تجزئة (segmentation fault) في حال كان يُتوقّع أن تُستدعى ‎make_foo‎ وفق صيغة الاستدعاء في C. يمكن حلّ هذه المشكلة عبر تغليف (wrap) كافة التصريحات في ترويسة في كتلة ‎extern "C"‎، انظر .. #ifdef __cplusplus extern "C" { #endif typedef struct Foo { int bar; } Foo; Foo make_foo(int); #ifdef __cplusplus } /* "extern C" نهاية الكتلة */ #endif والآن، عند تضمين ‎foo.h‎ من برنامج C، سيظهر كتصريح عادي، أمّا عند تضمين ‎foo.h‎ من برنامج مكتوب بلغة C++‎، فسيكون ‎make_foo‎ داخل كتلة ‎extern "C"‎، وسيعرف المصرّفُ كيف يبحث عن الأسماء، وسيستخدم صيغة الاستدعاء في C. هذا الدرس جزء من سلسلة دروس عن C++‎. ترجمة -بتصرّف- للفصل Chapter 138: Compiling and Building والفصل Chapter 134: Linkage specifications من كتاب C++ Notes for Professionals
  8. محدّدات أصناف التخزين هي كلمات مفتاحية يمكن استخدامها في التصريحات، ولا تؤثر على نوع التصريح لكنها تعدّل الطريقة التي تُخزّن بها الكيانات. extern محدّدُ صنف التخزين ‎extern‎ يستطيع التصريحَ بإحدى الطرق الثلاث التالية، وذلك وفقًا للسياق، فمثلًا: يمكن استخدامه للتصريح عن متغيّر دون تعريفه، يُستخدَم عادة في ملفات الترويسات الخاصة بالمتغيّرات التي تُعرّف في ملف تنفيذ منفصل. // نطاق عام int x; // بالقيمة الافتراضية x تعريف: سيهيأ extern int y; // معرَّف في مكان آخر، غالبًا في وحدة ترجمة أخرى y :تصريح. extern int z = 42; // أيّ تأثير هنا "extern" تعريف: ليس للكلمة المفتاحية إعطاء ارتباط خارجي (external linkage) لمتغيّر في نطاق فضاء الاسم، حتى لو كانت الكلمتان المفتاحيتان ‎const‎ و ‎constexpr‎ تتسبّبان في إنشاء ارتباط داخلي. // النطاق العام const int w = 42; // C وخارجي في ،C++ ارتباط داخلي في static const int x = 42; // C++ و C ارتباط داخلي في كل من extern const int y = 42; // C++ و C ارتباط خارجي في كل من namespace { extern const int z = 42; // ارتباط داخلي لأنّه في فضاء اسم غير مُسمّى } إعادة التصريح (redeclaring) عن متغيّر في نطاق الكتلة إن صُرِّح عنه مسبقًا عبر الارتباط (linkage). أما خلاف ذلك، سيُصرّح عن متغيّر جديد عبر الارتباط، وسيكون عضوًا في أقرب فضاء اسم محيط. // النطاق العام namespace { int x = 1; struct C { int x = 2; void f() { extern int x; // x إعادة التصريح عن std::cout << x << '\n'; // طباعة 1 وليس 2 } }; } void g() { extern int y; // العامّة المُعرّفة في موضع آخر y ارتباط خارجي، ويشير إلى قيمة y لدى } يمكن أيضًا التصريح عن الدالّة على أنها ‎extern‎، لكنّ هذا ليس له أيّ تأثير وإنّما يُستخدم في العادةً كتلميح للقارئ بأنّ الدالّة المُصرَّح عنها هنا ستعُرَّف في وحدة ترجمة أخرى. ففي المثال التالي، يكون ()void f تصريحًا لاحقًا يعني أن f ستُعرَّف في وحدة الترجمة هذه، أما ()extern void g ليس تصريحًا لاحقًا، مما يعني أن g تُعرَّف في وحدة ترجمة أخرى، انظر: void f(); extern void g(); في الشيفرة أعلاه، في حال التصريح عن ‎f‎ مع ‎extern‎ والتصريح عن ‎g‎ بدون ‎extern‎، لن يؤثر ذلك على صحة أو دلالات البرنامج، ولكن من المحتمل أن يربك القارئ. register الإصدار < C++‎ 17 ++‎> register هو محدّد صنف تخزين يخبر االمُصرِّف أنّ المتغيّر سيُستخدَم بكثرة، وتأتي الكلمة "register" من حقيقة أن المُصرِّف قد يختار تخزين هذا المتغيّر في سجل وحدة المعالجة المركزية (CPU register) لتسريع الوصول إليه. وقد أُهمِلت بدءًا من الإصدار C++‎ 11. register int i = 0; while (i < 100) { f(i); int g = i * i; i += h(i, g); } يُمكن تعريف كلٍّ من المتغيّرات المحلية ومُعامِلات الدوالّ بالكلمة المفتاحية ‎register‎، ولا تضع C++‎ قيودًا على ما يمكن فعله بالمتغيّرات المعرّفة بـ ‎register‎، على عكس لغة C. فكثلًا، يجوز أخذ عنوان متغيّر مصرّح عنه بـ ‎register‎، لكنّ هذا قد يمنع المُصرِّف من تخزين مثل هذا المتغيّر في السجل (register). الإصدار ≥ C++‎ 17 الكلمة المفتاحية ‎register‎ غير مستخدمة ومحفوظة، ولا ينبغي استخدامها. static لدى محدّد التخزين ‎static‎ ثلاث معانٍ مختلفة. يعطي ارتباطًا داخليًا لمتغيّر أو دالّة مُصرَّح عنها في نطاق فضاء الاسم. // دالة داخلية، لا يمكن الارتباط بها static double semiperimeter(double a, double b, double c) { return (a + b + c) / 2.0; } // التصدير إلى العميل double area(double a, double b, double c) { const double s = semiperimeter(a, b, c); return sqrt(s * (s - a) * (s - b) * (s - c)); } تصرّح بأنّ المتغيّر له مدة تخزين ساكنة - static storage duration - (ما لم تكن خيطًا محليًا ‎thread_local‎)، ومتغيرات نطاق فضاء الاسم تكون ساكنة ضمنيًا، وتُهيّأ المتغيّرات المحلية الساكنة مرّة واحدة فقط، إذ يمرّ التحكّم في المرّة الأولى عبر تعريفها، ولا تُدمَّر بعد الخروج من نطاقها. void f() { static int count = 0; std::cout << "f has been called " << ++count << " times so far\n"; } عند تطبيقها على تصريح عن عضو صنف (class member)، فإنّها تجعل ذلك العضو ساكنًا. struct S { static S* create() { return new S; } }; int main() { S* s = S::create(); } لاحظ أنّه إن كان العضو ساكنًا، ستَنطبق النقطتان 2 و 3 في نفس الوقت، إذ تضع الكلمة المفتاحية ‎static‎ عضو الصنف في عضو بيانات ساكن (static data member)، كما تجعله أيضًا في متغيّر ذي مدة تخزين ساكنة (static storage duration). auto الإصدار ≤ C++‎ 03 يصرّح هذا المحدّد بأنّ المتغيّر لديه مدة تخزين آلية (automatic storage duration)، وهو غير ضروري نظرًا لأنّ مدة التخزين التلقائي هي الإعداد الافتراضي في نطاق الكتلة، كما لا يُسمح باستخدام المحدّد auto في نطاق فضاء الاسم. void f() { auto int x; auto y; auto int z; في المثال السابق، auto int x تكافئ int x، ولا تجوز auto y في ++C لكنها جائزة في C89، كذلك لا تجوز auto int z، إذ لا يمكن أن تكون متغيرات نطاق فضاء الاسم آلية. تغيّر معنى ‎auto‎ في الإصدار C++‎ 11 بشكل كامل، ولم تعد محدّدًا للتخزين، وإنّما صارت تُستخدم في استنتاج الأنواع. mutable mutable هو محدّدٌ يمكن تطبيقه على تصريح عضو بيانات من صنف غير ساكن (non-static) وغير مرجعي (non-reference)، والعضو القابل للتغيير في صنفٍ لا يكون غيرَ ثابتٍ حتى لو كان الكائن ثابتًا. class C { int x; mutable int times_accessed; public: C(): x(0), times_accessed(0) {} int get_x() const { ++times_accessed; // لا بأس: يمكن للدوال التابعة الثابتة أن تعدّل أعضاء البيانات غير الثابتة return x; } void set_x(int x) { ++times_accessed; this -> x = x; } }; الإصدار ≥ C++‎ 11 أضيف معنى ثانٍ للكلمة المفتاحية ‎mutable‎ في C++‎ 11، فعندما تتبع قائمة المُعاملات الخاصة بتعبير لامدا، فإنها تقمع المؤهّل الثباتي ‎const‎ الضمني الموجود في مُعامل استدعاء لامدا (lambda's function call operator). ولذلك يمكن لتعابير لامدا القابلة للتغيير (mutable) أنّ تعدّل قيم الكيانات التي حصلت عليها عن طريق النسخ. std::vector < int > my_iota(int start, int count) { std::vector < int > result(count); std::generate(result.begin(), result.end(), [start]() mutable { return start++; }); return result; } لاحظ أنّ ‎mutable‎ لا تُعدُّ محدّدَ صنف تخزين إذا استخدِمَت بهذه الطريقة لتشكيل تعابير لامدا تقبل للتغيير (أي mutable). هذا الدرس جزء من سلسلة دروس عن C++‎. ترجمة -بتصرّف- للفصل Chapter 133: Storage class specifiers من كتاب C++ Notes for Professionals
  9. إدارة الموارد هي إحدى أصعب الأشياء في C و C++‎، لكن C++‎ توفّر لنا العديد من الطرق التي نصمم بها إدارة الموارد في برامجنا، وسنحاول في هذا المقال أن نشرح بعض هذه الطرق. تقنية RAII: تخصيص الموارد يكافئ التهيئة تقنية RAII وتدعى اكتساب أو تخصيص الموارد هي تهيئة (Resource Acquisition Is Initialization) هو مصطلح شائع في إدارة الموارد، ويستخدم المؤشّرات الذكية (smart pointer) لإدارة الموارد في حالة الذاكرة الديناميكية، وتُمنح الموارد المكتسبة ملكية مؤشر ذكي أو مدير موارد مكافئ مباشرة عند استخدام أسلوب RAII. ولا يمكن الوصول إلى المورد إلا من خلال ذلك المدير، مما يمكِّن المدير من تتبّع مختلف العمليات الجارية على المورد. على سبيل المثال، تحرِّر الكائنات من النوع ‎std::auto_ptr‎ مواردها تلقائيًا عندما تخرج عن النطاق، أو عندما تُحذف بطريقة أخرى. #include <memory> #include <iostream> using namespace std; int main() { { auto_ptr ap(new int(5)); cout << *ap << endl; // تطبع 5 } } في المثال السابق، كان المورد هو الذاكرة الديناميكية، ودُمِّر auto-ptr ثم حُرِّر مورده تلقائيًا. الإصدار ≥ C++‎ 11 كانت المشكلة الرئيسية في كائنات std::auto_ptr أنّها لا يمكن أن تُنسخ دون نقل الملكية: #include <memory> #include <iostream> using namespace std; int main() { auto_ptr ap1(new int(5)); cout << *ap1 << endl; // تطبع 5 auto_ptr ap2(ap1); cout << *ap2 << endl; // تطبع 5 cout << ap1 == nullptr << endl; } تفسير الشيفرة السابقة: (auto_ptr ap2(ap1: تنسخ ap2 من ap1 وتنتقل الملكية إلى ap2. cout << ap1 == nullptr << endl: تطبع القيمة 1، ويخسر ap1 ملكيته للمورد. وبسبب دلالات النسخ الغريبة تلك فإنّ هناك قيودًا على استخدام ‎std::auto_ptr‎، مثل أنها لا يمكن أن تُستخدم في الحاويات، وذلك لمنع حذف الذاكرة مرتين: فإذا كان لدينا كائنين من النوع ‎auto_ptrs‎ يملكان نفس المورد، سيحاول كلا الكائنين تحريره عند تدميرهما. ومن المهم منع تحرير المورد الذي سبق تحريره من قبل، إذ سيؤدي ذلك التحرير الثاني إلى حدوث مشاكل، لكن بأي حال فإن std::shared_ptr لديه طريقة لتجنب ذلك، مع عدم نقل الملكية أثناء النسخ. #include <memory> #include <iostream> using namespace std; int main() { shared_ptr sp2; { shared_ptr sp1(new int(5)); // sp1 إعطاء الملكيّة إلى cout << *sp1 << endl // تطبع 5 sp2 = sp1; // يملك كلاهما المورد ،sp1 من sp2 نسخ cout << *sp1 << endl; // تطبع 5 cout << *sp2 << endl; // تطبع 5 } // الملكية الحصرية للمورد sp2 عن النطاق وتدميره، أصبح لـ sp1 خروج cout << *sp2 << endl; } // عن النطاق، وتحرير المورد sp2 خروج القفل Locking هذا مثال عن قفل سيّء: std::mutex mtx; void bad_lock_example() { mtx.lock(); try { foo(); bar(); if (baz()) { mtx.unlock(); // ينبغي فتح القفل عند كل نقطة خروج return; } quux(); mtx.unlock(); // يحدث فتح القفل العادي هنا } catch (...) { mtx.unlock(); // ينبغي فرض فتح القفل في حال طرح اعتراض throw; // والسماح للاعتراض بالاستمرار } } تلك طريقة خاطئة لتنفيذ عمليتي القفل والفتح لكائنات المزامنة (mutex)، ولا بدّ أن يتحقّق المُبرمِجُ من أنّ جميع التدفّقات (flows) الناتجة عن إنهاء الدالّة تؤدّي إلَى استدعاء ‎unlock()‎، وذلك للتأكد أنّ فتح القفل باستخدام ‎unlock()‎ سيحرّر الكائن المزامنة الصحيح. وهذه عمليات هشة كما وضحنا أعلاه، لأنّها تتطّلب من المطوّرين متابعة النمط يدويًا. ويمكن حلّ هذه المشكلة باستخدام صنف مُصمّم خصّيصًا لتنفيذ تقنية RAII: std::mutex mtx; void good_lock_example() { std::lock_guard<std::mutex > lk(mtx); // المنشئ يقفل. // المدمِّر يفتح! // تضمن اللغة استدعاء المدمر. foo(); bar(); if (baz()) { return; } quux(); } ‎lock_guard‎ هو قالب صنف بسيط للغاية يستدعي ‎lock()‎ ويمرّر إليه الوسيط المُمرّر إلى مُنشئه ويحتفظ بمرجع إلى ذلك الوسيط، ثمّ يستدعي ‎unlock()‎ على الوسيط في مدمّره، ممّا يعني ضمان فتح قفل mutex عند خروج ‎lock_guard‎ عن النطاق. ولا يهم سبب الخروج عن النطاق سواءً كان ذلك بسبب اعتراض أو عودة مبكّرة، ذلك أن جميع الحالات سيتمّ التعامل معها، وسيفتح القفل بشكل صحيح بغض النظر عن سير البرنامج. ScopeSuccess الإصدار ≥ C++‎ 17 نستطيع باستخدام ‎int std::uncaught_exceptions()‎ أن ننفذ إجراءً لم يكن لينفَّذ إلّا في حالة النجاح (عدم رفع اعتراض في النطاق). وقد كانت ‎bool std::uncaught_exception()‎ فيما سبق تسمح برصد العمليّات الجارية لفكّ المكدّس (stack unwinding). انظر: #include <exception> #include <iostream> template < typename F> class ScopeSuccess { private: F f; int uncaughtExceptionCount = std::uncaught_exceptions(); public: explicit ScopeSuccess(const F &f): f(f) {} ScopeSuccess(const ScopeSuccess &) = delete; ScopeSuccess &operator=(const ScopeSuccess &) = delete; // f() might throw, as it can be caught normally. ~ScopeSuccess() noexcept(noexcept(f())) { if (uncaughtExceptionCount == std::uncaught_exceptions()) { f(); } } }; struct Foo { ~Foo() { try { ScopeSuccess logSuccess { []() { std::cout << "Success 1\n"; } }; // نجاح النطاق // أثناء فكّ المكدّس Foo حتى في حال تدمير // 0 < std::uncaught_exceptions() // std::uncaught_exception() == true } catch (...) {} try { ScopeSuccess logSuccess { []() { std::cout << "Success 2\n"; } }; تزيد القيمة المعادة من std::uncaught_exceptions، … throw std::runtime_error("Failed"); } وتنقص القيمة المعادة من std::uncaught_exceptions … catch (...) { } } }; int main() { try { Foo foo; throw std::runtime_error("Failed"); // std::uncaught_exceptions() == 1 } catch (...) { // std::uncaught_exceptions() == 0 } } سيكون الخرج: Success 1 ScopeFail ‏(C++‎ 17) الإصدار ≥ C++‎ 17 يمكننا تنفيذ إجراء معيّن لا يُنفَّذ إلّا عند الفشل (رفع اعتراض في النطاق) بفضل ‎int std::uncaught_exceptions()‎، وكانت ‎bool std::uncaught_exception()‎ تسمح سابقًا برصد إن كانت هناك عملية جارية لفك المكدّس. #include <exception> #include <iostream> template < typename F> class ScopeFail { private: F f; int uncaughtExceptionCount = std::uncaught_exceptions(); public: explicit ScopeFail(const F &f): f(f) {} ScopeFail(const ScopeFail &) = delete; ScopeFail &operator=(const ScopeFail &) = delete; يجب ألا ترفع ()f وإلا فستستدعى std::terminate، نتابع المثال … ~ScopeFail() { if (uncaughtExceptionCount != std::uncaught_exceptions()) { f(); } } }; struct Foo { ~Foo() { try { ScopeFail logFailure { []() { std::cout << "Fail 1\n"; } }; // نجاح النطاق // أثناء فكّ المكدّس Foo حتى في حال تدمير // 0 < std::uncaught_exceptions() في حال // std::uncaught_exception() == true أو سابقا } catch (...) {} try { ScopeFail logFailure { []() { std::cout << "Failure 2\n"; } }; تزيد القيمة المعادة من std::uncaught_exceptions …. throw std::runtime_error("Failed"); } تقل القيمة المعادة من std::uncaught_exceptions …. catch (...) { } } }; int main() { try { Foo foo; throw std::runtime_error("Failed"); // std::uncaught_exceptions() == 1 } catch (...) { // std::uncaught_exceptions() == 0 } } سيكون الخرج: Failure 2 Finally/ScopeExit إذا لم ترد كتابة أصناف خاصّة للتعامل مع بعض الموارد، فاكتب صنفًا عامًا على النحو التالي: template < typename Function> class Finally final { public: explicit Finally(Function f): f(std::move(f)) {} ~Finally() { f(); } // (1) انظر أدناه Finally(const Finally &) = delete; Finally(Finally &&) = default; Finally &operator=(const Finally &) = delete; Finally &operator=(Finally &&) = delete; private: Function f; }; // عندما يخرج الكائن المُعاد عن النطاق f تنفيذ الدالة template < typename Function> auto onExit(Function && f) { return Finally<std::decay_t < Function>> { std::forward<Function> (f) }; } وهذا مثال على استخدام ذلك الصنف: void foo(std::vector<int> &v, int i) { // ... v[i] += 42; auto autoRollBackChange = onExit([ &]() { v[i] -= 42; }); // ... `foo(v, i + 1)` شيفرة تكرارية } ملاحظة (1): يجب أخذ الملاحظات التالية حول تعريف المدمّر في الاعتبار عند التعامل مع الاعتراضات: Finally() noexcept { f(); }: std::terminate // تُستدعى في حال رفع اعتراض Finally() noexcept(noexcept(f())) { f(); } // إلّا في حال رفع اعتراض أثناء فك المكدّس terminate() لا تُستدعى Finally() noexcept { try { f(); } catch (...) { /* ignore exception (might log it) */} } ) // لا تُستدعى std::terminate، لكن لا نستطيع معالجة الخطأ (حتى في حالة عدم فك المكدّس كائنات المزامنة وأمان الخيوط Mutexes & Thread Safety قد تحدث مشكلة إن حاولت عدّة خيوط الوصول إلى نفس المورد، فلنفترض مثلًا أنّ لدينا خيطًا يضيف العدد 1 إلى متغيّر. فأوّل شيء يفعله هو أنّه يقرأ المتغيّر، ثم يضيف واحدًا إليه، ثم يعيد تخزينه مرّة أخرى. لنفترض الآن أنّنا هيّأنا ذلك المتغيّر عند القيمة 1، ثم أنشأنا نُسختين من ذلك الخيط، فبعد أن ينتهي كلا الخيطين، نتوقّع أن تكون قيمة ذلك المتغيّر هي 3، لكن هناك بعض المشاكل التي قد تحدث هنا، سنفصلها في الجدول التالي: الخيط 1 الخيط 2 المرحلة 1 قراءة 1 من المتغير المرحلة 2 قراءة 1 من المتغير المرحلة 3 إضافة 1 إلى 1 للحصول على 2 المرحلة 4 إضافة 1 إلى 1 للحصول على 2 المرحلة 5 تخزين 2 في المتغير المرحلة 6 تخزين 2 في المتغير table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; } في نهاية العملية، تُخزّن القيمة 2 في المتغيّر بدلًا من 3، ذلك أن الخيط 2 يقرأ المتغيّر قبل أن يُحدِّث الخيط 1 ذلك المتغيّر. ما الحلّ إذن؟ يكون الحل في كائنات المزامنة … . كائن المزامنة (mutex) هو كائن لإدارة الموارد، ومُصمّم لحل هذا النوع من المشاكل. فعندما يحاول خيط ما الوصول إلى مورد، فإنّه يستحوذ على كائن المزامنة لذلك المورد (resource's mutex). ويحرّر ذلك (releases) الخيطُ كائن المزامنة بمجرّد الانتهاء من العمل على المورد. وعند استحواذ خيط على كائن مزامنةٍ فإنّ كلّ الاستدعاءات للاستحواذ على ذلك الكائن لن تعود إلى أن يُحرَّر. لفهم ذلك بشكل أفضل، فكّر في كائن المزامنة كطابور انتظار في متجر كبير، إذ تنتقل الخيوط في الطابور لمحاولة الاستحواذ على كائن المزامنة، ثم تنتظر الخيوط التي تسبقُها حتى تنتهي، ثم تستخدم المورد، ثم تخرج عن الطابور عن طريق تحرير كائن المزامنة. فلو حَاول الجميع الوصول إلى المورد في نفس الوقت ستحدث فوضى كبيرة. الإصدار ≥ C++‎ 11 المكتبة std::mutexهي تطبيق كائن المزامنة في C++‎ 11. #include <thread> #include <mutex> #include <iostream> using namespace std; void add_1(int& i, const mutex& m) { // الدالة التي ستُنفّذ في الخيط m.lock(); i += 1; m.unlock(); } int main() { int var = 1; mutex m; cout << var << endl; // تطبع 1 thread t1(add_1, var, m); // إنشاء خيط مع وسائط thread t2(add_1, var, m); // إنشاء خيط آخر t1.join(); t2.join(); // انتظار انتهاء الخيطين cout << var << endl; // تطبع 3 } هذا الدرس جزء من سلسلة دروس عن C++‎. ترجمة -بتصرّف- للفصل Chapter 83: RAII: Resource Acquisition Is Initialization والفصل Chapter 132: Resource Management من كتاب C++ Notes for Professionals
  10. لغة C++‎، على غرار C، لها تاريخ طويل ومتنوّع بخصوص سير العمل التصريفي وعمليات البناء. واليوم، لدى لغة C++‎ العديد من أنظمة البناء التي تُستخدَم لتصريف البرامج لعدة منصات أحيانًا داخل نظام بناء واحد. وسننظر في هذا الدرس في بعض أنظمة البناء ونحلّلها. إنشاء بيئة بناء باستخدام CMake ينشئ CMake بيئات البناء للمُصرِّفات أو بيئات التطوير المتكاملة (IDE) انطلاقًا من تعريف مشروع واحد، وسوف توضّح الأمثلة التالية كيفية إضافة ملف CMake إلى برنامج "Hello World" متعدّد المنصات. تأخذ ملفات CMake دائمًا الاسم "CMakeLists.txt"، ويجب أن تكون موجودة في مجلّد الجذر لكل مشروع، وربما في المجلدات الفرعية أيضا. هذا مثال على ملف CMakeLists.txt: cmake_minimum_required(VERSION 2.4) project(HelloWorld) add_executable(HelloWorld main.cpp) يمكنك رؤية البرنامج يعمل مباشرة من هنا. هذا الملفُّ يخبر أداةَ CMake باسم المشروع، وإصدار الملف المتوقع، وبعض التعليمات الضرورية لتوليد ملف تنفيذي (executable) يُسمّى "HelloWorld"، والذي يتطلب ملف ‎main.cpp‎. في المثال التالي: أنشئ بيئة بناء للمُصرِّف أو بيئة تطوير متكاملة من سطر الأوامر: > cmake . ابن التطبيق بما يلي: > cmake --build . تولّد هذه الشيفرة بيئة البناء الافتراضية للنظام وفق نظام التشغيل والأدوات المُثبّتة، حافظ على الشيفرة المصدرية نظيفة من آثار البناء (build artifacts) باستخدام نمط تصميم "خارج المصدر" (out-of-source): > mkdir build > cd build > cmake .. > cmake --build . يمكن لـ CMake أيضًا تجريد (abstract) الأوامر الأساسية للصدفة (shell) الخاصة بالمنصة من المثال السابق: > cmake -E make_directory build > cmake -E chdir build cmake .. > cmake --build build يتضمّن CMake مولدّات لعدد من أدوات البناء الشائعة وبيئات التطوير المتكاملة، ولإنشاء ملفات makefiles لأجل nmake الخاصة بـ Visual Studio: > cmake -G "NMake Makefiles" .. > nmake التصريف باستخدام GNU GNU Make هو برنامج مخصّص لأتمتة تنفيذ أوامر الصدفة، وينتمي لعائلة برامج Make، ويتمتع Make بشعبية كبيرة لدى مستخدمي أنظمة التشغيل الشبيهة بيونكس وتلك الموافقة لمعايير POSIX، بما في ذلك الأنظمة المشتقة من نواة لينُكس ونظام Mac OSX ونظام BSD، ويتميّز GNU Make بأنّه مرتبط بمشروع جنو الذي يرتبط بدوره بأنظمة التشغيل المبنية على جنو\لينكس الشهير. هناك إصدارات أخرى من GNU Make متوافقة مع أنظمة تشغيل أخرى، مثل ويندوز و Mac OS X، وهذا البرنامج مستقر للغاية وله أثر تاريخي يبقيه مشهورًا، لهذا يُدرَّس GNU Make إلى جانب لغتي C و C++‎. القواعد الأساسية يجب أن تنشئ ملف Makefile في مجلّد المشروع لأجل التصريف بواسطة make. هذا مثال بسيط على ملف Makefile: سنقسم الملف أثناء سرد المثال للشرح …. Makefile # إعداد بعض المتغيرات لاستخدامها مع الأمر # g++ أولا، سنحدد المصرف CXX=g++ # وغيرها g++ ثم نصرّف مع التنبيهات الموصى بها في CXXFLAGS=-Wall -Wextra -pedantic # هذا هو ملف الخرج EXE=app SRCS=main.cpp يُستدَعى هذا الهدف عند استدعاء make في سطر الأوامر، وتقول (EXE)$ الموجودة على اليمين أن الهدف all يعتمد على الهدف (EXE)$. لاحظ أنه بما أن هذا هو الهدف الأول، فسيكون الهدف الافتراضي إن استُدعيَت make بدون هدف، نتابع المثال … all: $(EXE) # هذا يكافئ # app: $(SRCS) (SRCS)$ يمكن فصلها، مما يعني أن هذا الهدف سيعتمد على كل الملفات. لاحظ أن هذا الهدف له متن تابع (method body)، وهو الجزء المزاح بمسافة جدول واحدة (tab) وليس أربع مسافات فارغة. وسينفذ make الأمر التالي عند بناء هذا الهدف: g++ -Wall -Wextra -pedantic -o app main.cpp والذي يعني تصريف main.cpp مع التحذيرات، والإخراج إلى الملف app/.، نتابع … $(EXE): $(SRCS) @$(CXX) $(CXXFLAGS) -o $@ $(SRCS) هذا الهدف ينبغي له أن يعكس الهدف all، وإن استدعيْتَ make مع وسيط مثل make clean فسيُستَدعى الهدف gets الموافق له، … clean: @rm -f $(EXE) ملاحظة: تأكد من أن الإزاحات البادئة قد تمت بزر الجدول (tab)، ولا تستخدم 4 مسافات بيضاء من زر Space. وإلا فسيُطلق الخطأ ‎Makefile:10: *** missing separator. Stop.‎. لتنفيذ هذه الشيفرة من سطر الأوامر، عليك كتابة ما يلي: $ cd ~/Path/to/project $ make $ ls app main.cpp Makefile $ ./app Hello World! $ make clean $ ls main.cpp Makefile عمليات البناء التزايدية التزايدي Incremental builds تظهر فائدة make عندما تكثر الملفات، فماذا لو عدّلت الملف a.cpp دون الملف b.cpp؟ لن يكون من الحكمة إذًا أن تعيد تصريف b.cpp، لأنّ ذلك سيُهدر الوقت. في المثال أدناه، لنفرض أن لدينا الهيكل التالي لمجلد: . +-- src | +-- a.cpp | +-- a.hpp | +-- b.cpp | +-- b.hpp +-- Makefile فسيكون هذا ملف Makefile جيد: CXX=g++ CXXFLAGS=-Wall -Wextra -pedantic EXE=app SRCS_GLOB=src/*.cpp SRCS=$(wildcard $(SRCS_GLOB)) OBJS=$(SRCS:.cpp=.o) all: $(EXE) $(EXE): $(OBJS) @$(CXX) -o $@ $(OBJS) depend: .depend .depend: $(SRCS) @-rm -f ./.depend @$(CXX) $(CXXFLAGS) -MM $^>>./.depend clean: -rm -f $(EXE) -rm $(OBJS) -rm *~ -rm .depend include .depend مرّة أخرى، لاحظ إزاحات الجدول البادئة. سيضمن ملفّ Makefile الجديد ألّا تصرّف إلّا الملفات التي عُدِّلت، وهذا سيقلّل وقت التصريف. التوثيق للحصول على المزيد من المعلومات عن make، راجع التوثيق الرسمي -بالإنجليزية- من مؤسسة البرمجيات الحرة وإجابة dmckee التفصيلية على موقع stackoverflow. البناء بواسطة SCons يمكنك بناء شيفرة "Hello World" متعدّدة المنصات باستخدام أداة Scons، وهي أداة بناء برامج تستخدم لغة بايثون. أولاً، عليك إنشاء ملف يُسمّى ‎SConstruct‎ -سيبحث SCons عن هذا الملف افتراضيًا-، وينبغي أن يكون الملف -للآن- في مجلّد مع الملف ‎hello.cpp‎. اكتب السطر التالي في الملف الجديد: Program('hello.cpp') الآن، من سطر الأوامر، نفّذ الأمر ‎scons‎، ينبغي أن ترى شيئًا من هذا القبيل: $ scons scons: Reading SConscript files ... scons: done reading SConscript files. scons: Building targets ... g++ -o hello.o -c hello.cpp g++ -o hello hello.o scons: done building targets. رغم أن التفاصيل ستختلف وفقًا لنظام التشغيل والمُصرِّف المُستخدم. سيساعدك الصنفان ‎Environment‎ و ‎Glob‎ على إعداد ما يجب بناؤه. على سبيل المثال، سيبني الملف ‎SConstruct‎ الملف التنفيذي hello باستخدام جميع ملفات cpp في مجلد src، كما أن CPPPATH الخاص به سيكون ‎/usr/include/boost‎، كما يحدّد معيار C++‎ 11، انظر: env=Environment(CPPPATH='/usr/include/boost/', CPPDEFINES=[], LIBS=[], SCONS_CXX_STANDARD="c++11" ) env.Program('hello', Glob('src/*.cpp')) Autotools هي مجموعة من البرامج التي تُستخدَم لإنشاء نظام بناء جنو (GNU Build System) لأجل حزمة برامج معيّنة، وهي حزمة من الأدوات تعمل معًا لإنتاج العديد من موارد البناء، مثل ملفات Makefile -لتُستخدَم مع برنامج GNU Make، لهذا تُعدُّ هذه الحزمة المولّد المعياري لأنظمة البناء. فيما يلي بعض برامج Autotools الأساسية: Autoconf. Automake (لا تخلط بينها وبين ‎make‎). والهدف من حزمة Autotools بشكل عام، هو توليد برنامج نصّي متوافق مع يونيكس (Unix-compatible script)، إضافة إلى ملفّ Makefile للسماح للأمر التالي بإنجاز عملية البناء -وكذلك تثبيت- معظم الحُزم: ./configure && make && make install وترتبط حزمة Autotools على هذا النحو أيضًا ببعض مدراء الحزم (package managers)، خاصة تلك المرتبطة بأنظمة التشغيل التي تتوافق مع POSIX. Ninja يوصف نظام بناء Ninja في موقعه بأنه "نظام بناء صغير يركّز على السرعة"، وصُمِّم هذا النظام لكي تُبنى ملفاته بواسطة مولدات ملفات بناء النظام (build system file generators)، ويعتمد أسلوبًا منخفض المستوى (low-level) لبناء الأنظمة، وذلك على عكس مدراء أنظمة البناء عالية المستوى مثل CMake أو Meson. كُتِب نظام Ninja أساسًا بلغة C++‎ و Python، وقد أُنشِئ كبديل لنظام البناء SCons في مشروع Chromium. NMAKE (أداة صيانة برامج Microsoft) NMAKE هي أداة سطر أوامر طوّرتها ميكروسوفت لاستخدامها مع Visual Studio و / أو أدوات سطر أوامر Visual C++‎. ويُنظر إليها على أنها نظام بناء يندرج ضمن مجموعة عائلة Make لأنظمة البناء، ولكنّه يتميّز ببعض الميزات الخاصّة التي تميّزه عن برامج Make الشبيهة بيونكس الأخرى، مثل دعم صياغة مسارات ملفات Windows (والتي تختلف عن مسارات الملفات في يونيكس). أخطاء المصرف الشائعة (GCC) مرجعّ غير معرَّف إلى "***" تحدث أخطاء الرابط (linker) عندما يعجز عن العثور عن رمز مُستخدم، ويحدث هذا غالبًا عند عدم ربط إحدى المكتبات المستخدمة. qmake: LIBS += nameOfLib cmake: TARGET_LINK_LIBRARIES(target nameOfLib) استدعاء g++‎: g++ -o main main.cpp -Llibrary/dir -lnameOfLib قد ينسى المرء أيضًا تصريف وربط جميع ملفّات ‎.cpp‎ المستخدمة (تحدّد functionModule.cpp الدالّة المطلوبة): g++ -o binName main.o functionsModule.o error: '***' was not declared in this scope يحدث هذا الخطأ في حال استخدام كائن غير معروف. أخطاء متعلقة بالمتغيّرات لن تُصرّف الشيفرة التالية، لأن المتغير i غير موجود في نطاق الدالة main: #include <iostream> int main(int argc, char *argv[]) { { int i = 2; } std::cout << i << std::endl; return 0; } الحل: #include <iostream> int main(int argc, char *argv[]) { { int i = 2; std::cout << i << std::endl; } return 0; } أخطاء متعلقة بالدوال يحدث هذا الخطأ غالبًا في حال عدم تضمين الترويسة المطلوبة (على سبيل المثال استخدام ‎std::cout‎ دون ‎#include <iostream>‎). ففي المثال التالي، لن تُصرّف الشيفرة التالية: #include <iostream> int main(int argc, char *argv[]) { doCompile(); return 0; } void doCompile() { std::cout << "No!" << std::endl; } الحلّ: #include <iostream> void doCompile(); // تصريح لاحق للدالة int main(int argc, char *argv[]) { doCompile(); return 0; } void doCompile() { std::cout << "No!" << std::endl; } أو: #include <iostream> void doCompile() // تعريف الدالة قبل استخدامها { std::cout << "No!" << std::endl; } int main(int argc, char *argv[]) { doCompile(); return 0; } ملاحظة: يفسر المُصرِّف الشيفرة من الأعلى إلى الأسفل (للتبسيط فقط)، لذا يجب التصريح عن كل شيء (أو تعريفه) قبل استخدامه. fatal error: ***: No such file or directory يحدث هذا الخطأ عندما يعجز المُصرِّف عن العثور عن ملفّ معيّن (ملف مصدري يستخدم ‎#include "someFile.hpp"‎). qmake: INCLUDEPATH += dir / Of / File cmake: include_directories(dir/Of/File) استدعاء g++‎: g++ -o main main.cpp -Idir/Of/File عدم التوافقية مع لغة C سنتحدّث في هذا القسم عن شيفرات C غير المتوافقة مع لغة C++‎ والتي ستعطل مصرّفاتها‎. الكلمات المفتاحية المحجوزة هناك كلمات مفتاحية لها غرض خاص في C++‎، فالشيفرة التالية مثلًا جائزة في C، لكن غير جائزة في C++‎. int class = 5 وإصلاح هذه الأخطاء سهل، فكل ما عليك هو إعادة تسمية المتغيّر. المؤشّرات ذات النوعية الضعيفة Weakly Typed Pointers يمكن تحويل المؤشّرات في C إلى النوع ‎void*‎، أما في C++‎ فستحتاج إلى تحويل صريح explicit cast. انظر المثال التالي حيث تكون الشيفرة غير جائزة في C++‎، ولكن جائزة في C: void* ptr; int* intptr = ptr; إضافة تحويل صريح (explicit cast) ستحل المشكلة لكن قد يسبّب مشاكل أخرى. goto أو switch لا يجوز في ++C أن تتخطي التهيئة باستخدام ‎goto‎ أو ‎switch‎. انظر المثال التالي حيث تكون الشيفرة صحيحة في C، وغير صحيحة في C++‎: goto foo; int skipped = 1; foo; قد يتطلب إصلاح هذه الأخطاء إعادة تصميم البرنامج. هذا الدرس جزء من سلسلة دروس عن C++‎. ترجمة -بتصرّف- للفصول Chapter 130: Build Systems Chapter 139: Common compile/linker errors (GCC)‎ Chapter 136: C incompatibilities من كتاب C++ Notes for Professionals
  11. الصحة الثباتية (Const Correctness) أسلوب لتصميم الشيفرات، يعتمد على فكرة أنّه لا ينبغي أن تُتاح إمكانية تعديل نسخة معيّنة -على سبيل المثال الحق في الكتابة- إلّا للشيفرَات التي تحتاج إلى تعديل تلك النسخة، وبالمقابل، فإنّ أيّ شيفرة لا تحتاج إلى تعديل نُسخة ما لن تملك القدرة على تعديلها، أي سيكون لها حق القراءة فقط. تصون هذه المقاربة النُسخة من أن تُعدَّل عن غير قصد، لأنّ ذلك قد يجعل الشيفرة غير موثوقة، كما يوضّح للمبرمجين الذي يقرؤون الشيفرة ما إذا كانت الشيفرة تهدف إلى تغيير حالة النُسخة أم لا. وتسمح أيضًا بالتعامل مع النسخ ككائنات ثابتة (‎const‎) عندما لا تكون هناك حاجة إلى تعديلها، أو تعريفها ككائنات ثابتة إذا لم تكن بحاجة إلى تغييرها بعد التهيئة، وذلك دون التأثير على وظيفتها. يتم ذلك عن طريق إعطاء التوابعِ مؤهلات (const CV-qualifiers)، وعن طريق وسم معاملات المؤشّرات / المراجع بالكلمة المفتاحية ‎const‎. class ConstCorrectClass { int x; public: int getX() const { return x; } // الدالة ثابتة: أي أنها لن تعدل النسخة void setX(int i) { x = i; } // غير ثابتة: أي أنها ستعدل النسخة }; // المعامل ثابت، أي أنّه لن يُعدّل int const_correct_reader(const ConstCorrectClass& c) { return c.getX(); } // المعامل غير ثابت: سيُعدَّل void const_correct_writer(ConstCorrectClass& c) { c.setX(42); } const ConstCorrectClass invariant; // النسخة ثابتة: لن تُعدّل ConstCorrectClass variant; // النسخة غير ثابتة: يمكن أن تُعدَّل // … // صحيح: استدعاء دالة ثابتة غير مُعدِّلة على نسخة ثابتة const_correct_reader(invariant); // صحيح: استدعاء دالة ثابتة غير مُعدِّلة على نسخة قابة للتعديل const_correct_reader(variant); // صحيح: استدعاء دالة ثابتة مُعدِّلة على نسخة قابلة للتعديل const_correct_writer(variant); // خطأ: استدعاء دالة ثابتة مُعدِّلة على نسخة ثابتة const_correct_writer(invariant); نظرًا لطبيعة الصحة الثباتية، فإنّها تبدأ بتوابع الصنف، ثمّ تستمر إلى الخارج، وسيطلق المصرِّف خطأً إذا حاولت استدعاء تابع غير ثابت من نُسخة ثابتة، أو من نُسخة غير ثابتة تُعامل كنسخة ثابتة. الصحة الثباتية في تصميم الأصناف جميع الدوال التابعة في صنف صحيح ثباتيًا (const-correct class)، التي لا تُغيّر الحالة المنطقية، يكون مؤشّر ‎this‎ الخاص بها ثابتًا، للدلالة على أنّها لن تُعدّل الكائن، لكن هذا لا يمنع أن تكون هناك حقول متغيرة - ‎mutable‎ - والتي يمكن تعديلها حتى لو كانت الكائنات التي تنتمي إليها ثابتة. عندما تعيد دالةٌ تأهيلها ثابت (‎const‎) مرجعًا، فينبغي أن يكون ذلك المرجع ثابتًا. يتيح هذا لها إمكانية أن تُستدعى على كلٍّ من النُسخ الثابتة، وكذلك النسخ غير المؤهّلة (non-cv-qualified)، إذ ستكون ‎const T*‎ قادرة على الارتباط بـ ‎T*‎ أو ‎const T*‎. هذا يسمح بدوره للدوالّ بأن تصرّح عن المُعاملات المُمرّة بالمرجع (passed-by-reference) على أنّها ثابتة عندما لا تحتاج إلى تعديل، وذلك دون التأثير على وظيفتها. علاوة على ذلك، في الأصناف الصحيحة ثباتيًا، تكون جميع مُعاملات الدالة المُمرّة بالمرجع صحيحة ثباتيًا، إذ لن يمكن تعديلها إلّا عندما تحتاج الدالّة إلى تعديلها صراحة. أولاً، لننظر إلى مؤهِّلات للمؤشّر ‎this‎ : لنفرض أن لدينا صنف Field، له الدالة التابعة ;(void insert_value(int class ConstIncorrect { Field fld; public: ConstIncorrect(Field& f); // يعدِّل Field& getField(); // قد يعدِّل، كما تُكشَف الأعضاء كمراجع غير ثابتة وذلك لإتاحة التعديل غير // المباشر void setField(Field& f); // يعدِّل void doSomething(int i); // قد يعدِّل void doNothing(); // قد يعدِّل }; ConstIncorrect::ConstIncorrect(Field& f): fld(f) {} // يعدِّل Field& ConstIncorrect::getField() { return fld; } // لا يعدِّل void ConstIncorrect::setField(Field& f) { fld = f; } // يعدِّل void ConstIncorrect::doSomething(int i) { // يعدِّل fld.insert_value(i); } void ConstIncorrect::doNothing() {} // لا يعدِّل class ConstCorrectCVQ { Field fld; public: ConstCorrectCVQ(Field& f); // يعدِّل const Field& getField() const; // لا يعدِّل، تُكشف الأعضاء كمراجع ثابتة لمنع التعديل غير المباشر void setField(Field& f); // يعدِّل void doSomething(int i); // يعدِّل void doNothing() const; // لا يعدِّل }; ConstCorrectCVQ::ConstCorrectCVQ(Field& f): fld(f) {} Field& ConstCorrectCVQ::getField() const { return fld; } void ConstCorrectCVQ::setField(Field& f) { fld = f; } void ConstCorrectCVQ::doSomething(int i) { fld.insert_value(i); } void ConstCorrectCVQ::doNothing() const {} هذا لن يعمل إذا لا يمكن استدعاء الدوال التابعة على نُسخ ConstIncorrect، نتابع … void const_correct_func(const ConstIncorrect& c) { Field f = c.getField(); c.do_nothing(); } أما هذا فيعمل إذ يمكن استدعاء ()doNothing و ()getField على نسخ ConstCorrectCVQ … void const_correct_func(const ConstCorrectCVQ& c) { Field f = c.getField(); c.do_nothing(); } يمكننا بعد ذلك دمج هذا مع معاملات الدوال الصحيحة ثباتيا (‎Const Correct Function Parameters‎)، وسيجعل هذا الصنفَ صحيحًا ثباتيًا بشكل كامل. class ConstCorrect { Field fld; public: ConstCorrect(const Field& f); // تعدّل النسخة، ولكن لا تعدّل المعامل const Field& getField() const; // لا تعدِّل، وتكشف العضو كمرجع ثابت لمنع التعديل غير المباشر void setField(const Field& f); // تعدّل النسخة، ولكن لا تعدّل المعامل void doSomething(int i); // passed by value تعدّل، لكن لا تعدّل المعامل - ممرر بالقيمة void doNothing() const; // لا تعدّل }; ConstCorrect::ConstCorrect(const Field& f) : fld(f) {} Field& ConstCorrect::getField() const { return fld; } void ConstCorrect::setField(const Field& f) { fld = f; } void ConstCorrect::doSomething(int i) { fld.insert_value(i); } void ConstCorrect::doNothing() const {} يمكن أيضًا دمج هذا مع التحميل الزائد على أساس الصحة الثباتية إذا أردنا سلوك ما عندما تكون النُسخة ثابتة، وسلوكًا آخر مختلفًا في الحالات الأخرى. وإحدى الاستخدامات الشائعة لهذا هي الحاويات (constainers)، إذ توفّر دوال وصول (accessors) لا تسمح بالتعديل إلّا إن كانت الحاوية نفسها غير ثابتة. class ConstCorrectContainer { int arr[5]; public: // معامل تسجيل يوفر إمكانية القراءة إن كانت النسخة ثابتة، أو إمكانية القراءة/الكتابة خلاف ذلك int& operator[](size_t index) { return arr[index]; } const int& operator[](size_t index) const { return arr[index]; } // ... }; يُستخدم هذا كثيرًا في المكتبة القياسية، إذ توفّر معظم الحاويات تحميلات زائدة لأخذ الثباتية في الحسبان. معاملات الدوال الصحيحة ثباتيًا إذا كانت دالّة ما صحيحة ثباتيًا، فإنّ جميع المُعاملات المُمرَّرة إليها بالمرجع ستكون ثابتة، ما لم تعدّلها الدالّة بشكل مباشر أو غير مباشر. يفيد هذا في منع المبرمج من إجراء تعديل غير مقصود، ويسمح للدالّة بأن تقبل النُسخ الثابتة، وكذلك النسخ غير المؤهّلة، وتجعل مؤشّر النسخة ‎this‎ من النوع ‎const T*‎ عندما تُستدعى دالة تابعة ما، حيث ‎T‎ يمثّل هنا نوع الصنف. struct Example { void func() { std::cout << 3 << std::endl; } void func() const { std::cout << 5 << std::endl; } }; void const_incorrect_function(Example& one, Example* two) { one.func(); two->func(); } void const_correct_function(const Example& one, const Example* two) { one.func(); two->func(); } int main() { Example a, b; const_incorrect_function(a, &b); const_correct_function(a, &b); } هذا هو خرج البرنامج 3 3 5 5 تمسك الدوال الصحيحة ثباتيًا الكثير من الأخطاء التي قد لا تمسكها الدوال غير الصحيحة ثباتيًا كما هو مبيّن أدناه، وذلك رغم أنّ تأثيرات هذا السلوك أقل وضوحًا من التأثيرات الناجمة عن تصميم الأصناف الصحيحة ثباتيًا التي رأيناها من قبل، في أنّ الدوال الصحيحة ثباتيا والأصناف غير الصحيحة ثباتيًا تتسبّب في حدوث أخطاء تصريفية، في حين أنّ الأصناف الصحيحة ثباتيًا والدوال غير الصحيحة ثباتيًا تُصرَّف بلا مشاكل. لاحظ أنّ الدوالّ غير الصحيحة ثباتيًا تتسبب في حدوث أخطاء تصريفية في حالة تمرير نُسخة ثابتة إليها. // قراءة القيمة من المتجهة ثم حساب وإعادة قيمة // إمساك القيمة المعادة لأجل تسريع البرنامج template<typename T> const T& bad_func(std::vector<T>& v, Helper<T>& h) { // إمساك القيم لإستخدامها مستقبلا // بعد حساب القيمة المعادة، تُخزّن ويُسجل فهرسها static std::vector<T> vals = {}; int v_ind = h.get_index(); // v الفهرس الحالي في سيكون السطر التالي مساويًا لـ 1- إن كان فهرس المخزن المؤقت غير مسجلًا … int vals_ind = h.get_cache_index(v_ind); if (vals.size() && (vals_ind != -1) && (vals_ind < vals.size()) && !(h.needs_recalc())) { return vals[h.get_cache_index(v_ind)]; } T temp = v[v_ind]; temp -= h.poll_device(); temp *= h.obtain_random(); temp += h.do_tedious_calculation(temp, v[h.get_last_handled_index()]); if (vals_ind != -1) { vals[vals_ind] = temp; } else { v.push_back(temp); // لا ينبغي الدخول إلى القيم vals_ind = vals.size() - 1; h.register_index(v_ind, vals_ind); } return vals[vals_ind]; } // النسخة الصحيحة ثباتيا تماثل النسخة أعلاه، لذا سنتجاوز معظمها template<typename T> const T& good_func(const std::vector<T>& v, Helper<T>& h) { // ... if (vals_ind != -1) { vals[vals_ind] = temp; } else { v.push_back(temp); // Error: discards qualifiers. vals_ind = vals.size() - 1; h.register_index(v_ind, vals_ind); } return vals[vals_ind]; } الصحة الثباتية كأداة للتوثيق من فوائد مفهوم الصحة الثباتية (const correctness) هي أنّه يمكن استخدامها لتوثيق الشيفرات، عبر توفير ضمانات وتوجيهات للمبرمجين والمستخدمين الآخرين. هذه الضمانات يفرضها المُصرِّف بسبب الثباتية، مع غيابٍ للثباتية يوحي أن الشيفرة لا توفرها. التوابع المؤهّلة ثباتيًا يمكن أن نفترض أنّ المبرمج الذي كتب الشيفرة يقصد أنّ الدوال التابعة الثابتة ستقرأ النسخة، و: لن تعدّل الحالة المنطقية للنُسخة التي استُدعيت عليها. نتيجة لذلك، فلن تعدّل أيّ حقل من حقول النُسخة التي استُدعيت عليها، باستثناء المتغيّرات القابلة للتغيير ‎mutable‎. لن تستدعي دوال أخرى من شأنّها تعديل أيّ حقل من حقول النُسخة، باستثناء المتغيّرات القابلة للتغيير ‎mutable‎. بالمقابل، يمكن افتراض أنّ أيّ دالة تابعة غير ثابتة سوف تسعى إلى تعديل النُسخة، و: قد تعدّل الحالة المنطقية أو لا. قد تستدعي دوال أخرى يمكن أن تعدّل الحالة المنطقية أو لا. يمكن استخدام هذا لأجل بناء افتراضات حول حالة الكائن بعد استدعاء دالة تابعة معيّنة عليه دون الحاجة إلى رؤية تعريف تلك الدالّة: // ConstMemberFunctions.h class ConstMemberFunctions { int val; mutable int cache; mutable bool state_changed; public: // يعدّل المنشئ الحالة المنطقية، لذا لا ضرورة لأيّ افتراضات ConstMemberFunctions(int v = 0); يمكن أن نفترض أن الدالة التالية لن تعدل الحالة المنطقية، ولن تستدعي ()set_val، ويمكن كذلك أن تستدعي ()bad_func أو ()squared_calc … int calc() const; يمكن أن نفترض أن الدالة التالية لن تعدل الحالة المنطقية، ولن تستدعي ()set_val، ويمكن كذلك أن تستدعي ()bad_func أو ()calc … int squared_calc() const; يمكن أن نفترض أن الدالة التالية لن تعدل الحالة المنطقية، ولن تستدعي ()set_val، ويمكن كذلك أن تستدعي ()calc أو ()squared_calc … void bad_func() const; يمكن أن نفترض أن الدالة التالية لن تعدل الحالة المنطقية، ولن تستدعي ()set_val، ويمكن كذلك أن تستدعي ()bad_func أو ()squared_calc أو ()calc … void set_val(int v); }; وفقًا لقواعد الصحة الثباتية، ستُفرض هذه الافتراضات في الواقع بواسطة المُصرِّف. // ConstMemberFunctions.cpp ConstMemberFunctions::ConstMemberFunctions(int v /* = 0*/) : cache(0), val(v), state_changed(true) {} // لقد كان افتراضنا صحيحا int ConstMemberFunctions::calc() const { if (state_changed) { cache = 3 * val; state_changed = false; } return cache; } // لقد كان افتراضنا صحيحا int ConstMemberFunctions::squared_calc() const { return calc() * calc(); } // لقد كان افتراضنا غير صحيح // للمؤهّلات `this` فشل تصريف الدالة بسبب فقدان void ConstMemberFunctions::bad_func() const { set_val(863); } // لقد كان افتراضنا صحيحا void ConstMemberFunctions::set_val(int v) { if (v != val) { val = v; state_changed = true; } } مُعاملات الدالة الثابتة يمكن أن نفترض أنّ الدوالّ التي تقبل مُعاملًا واحدًا ثابتًا أو أكثر تهدف إلى قراءة تلك المُعاملات، و: أنّها لن تعدّل تلك المُعاملات، أو تستدعي عليها تابعًا يمكن أن يُعدّلها. لن تمرّر تلك المُعاملات إلى أيّ دالّة أخرى من شأنها تعديلها و/أو استدعاء أي توابع أخرى من شأنها تعديلها. بالمقابل، يمكن افتراض أنّ أي دالّة تقبل مُعاملًا واحدًا غير ثابت أو أكثر ستسعى لتعديل تلك المُعاملات، و: قد تعدِّل أو لا تعدّل تلك المُعاملات، أو قد تستدعي عليها توابع يمكن أن تعدّلها. قد تمرّر أو قد لا تمرّر تلك المُعاملات إلى دوال أخرى يمكن أن تعدّلها و / أو تستدعي عليها توابع قد تعدّلها. يمكن استخدام هذا الأمر لأجل بناء افتراضات حول الحالة التي ستكون عليها المُعاملات بعد تمريرها إلى دالّة معيّنة، حتى دون النظر في تعريف تلك الدالّة. فيما يلي، يمكن أن نفترض أن c لم تُعدَّل وأن ()c.set_val لم تُستدعى ولم تمرَّر إلى ()non_qualified_function_parameter، وإذا مُرِّرَت إلى ()one_const_one_not فستكون أول المعامِلات … // function_parameter.h void const_function_parameter(const ConstMemberFunctions& c); يمكن أن نفترض أن c عُدِّلَت و/أو أن ()c.set_val استدعيَت، وقد تكون مرِّرَت إلى أي من تلك الدوال، وإن مُرِّرَت إلى ()one_const_one_not فقد تكون أيًا من المعامِلات … void non_qualified_function_parameter(ConstMemberFunctions& c); نستطيع افتراض أن: l لم تُعدَّل، وأن ()l.set_val لن تُستدعى. l قد تُمرَّر أو لا إلى ()const_function_parameter. تم تعديل r و/أو قد تستدعى ()r.set_val. قد تُمرَّر r أو لا إلى أي من الدوال السابقة. نتابع … void one_const_one_not(const ConstMemberFunctions& l, ConstMemberFunctions& r); يمكن أن نفترض أن c لم تُعدَّل وأن ()c.set_val لم تُستدعى، وأنما لم تُمرَّر إلى ()non_qualified_function_parameter، وإن مُرِّرَت إلى ()one_const_one_not فقد تكون أيًا من المعامِلات … void bad_parameter(const ConstMemberFunctions& c); وفقًا لقواعد الصحة الثباتية، ستُفرض هذه الافتراضات من قِبل المُصرِّف. // function_parameter.cpp // افتراضنا كان صحيحا void const_function_parameter(const ConstMemberFunctions& c) { std::cout << "With the current value, the output is: " << c.calc() << '\n' << "If squared, it's: " << c.squared_calc() << std::endl; } // افتراضنا كان صحيحا void non_qualified_function_parameter(ConstMemberFunctions& c) { c.set_val(42); std::cout << "For the value 42, the output is: " << c.calc() << '\n' << "If squared, it's: " << c.squared_calc() << std::endl; } // افتراضنا كان صحيحا // لاحظ أنّ الصحة الثباتية لا تحصِّن التغليف من أن يُكسر، وإنما تمنع الحق في الكتابة // إلا عند الحاجة إليها void one_const_one_not(const ConstMemberFunctions& l, ConstMemberFunctions& r) { struct Machiavelli { int val; int unimportant; bool state_changed; }; reinterpret_cast<Machiavelli&>(r).val = l.calc(); reinterpret_cast<Machiavelli&>(r).state_changed = true; const_function_parameter(l); const_function_parameter(r); } افتراضنا فيما يلي كان خطأ، وتفشل الدالة في التصريف لأن this فقد مؤهلاتٍ في ()c.set_val، … void bad_parameter(const ConstMemberFunctions& c) { c.set_val(18); } رغم أنّه يمكن التحايل على الصحة الثباتية وكسر الضمانات التي تقدّمها، إلا أنّه ينبغي توخّي الحذر عند فعل ذلك، ويجب أن يكون المبرمج على علم بما يفعل، فمن المحتمل أن يتسبّب في حدوث سلوك غير معرَّف. class DealBreaker : public ConstMemberFunctions { public: DealBreaker(int v = 0); // اسم غير مسموح به، لكنه ثابت void no_guarantees() const; } DealBreaker::DealBreaker(int v /* = 0 */) : ConstMemberFunctions(v) {} // افتراضنا كان خاطئا // الصحة الثباتية، وتجعل المصرف يعتقد أننا على علم بتبِعات ما نفعل const_cast تحذف void DealBreaker::no_guarantees() const { const_cast<DealBreaker*>(this)->set_val(823); } // ... const DealBreaker d(50); d.no_guarantees(); // ثابتة، وقد تُعدّل d سلوك غير معرَّف، إذ أنّ على أي حال، ولأن هذا يتطلب من المبرمج أن يخبر المصرِّف أنه ينوي تجاهل الثباتية (Constness)، ولعدم التماثل (Consistency) بين المصرِّفات، فمن الآمن افتراض أن الشيفرة الصحيحة ثباتيًا لن تتجاهل الصحة الثباتية أو التماثل بين المصرِّفات إلا إن حُدِّد خلاف ذلك. هذا الدرس جزء من سلسلة دروس عن C++‎. ترجمة -بتصرّف- للفصل Chapter 128: Const Correctness من كتاب C++ Notes for Professionals
  12. سوف نستعرض في هذا الدرس بعض الأمثلة على كيفية التعامل مع خادم العميل (Client server). مثال Hello TCP Client هذا البرنامج مكمّل لبرنامج Hello TCP Server، ويمكنك تشغيل أي منهما للتحقق من صلاحيتهما. انظر شيفرة المثال فيما يلي: #include <cstring> #include <iostream> #include <string> #include <arpa/inet.h> #include <netdb.h> #include <sys/socket.h> #include <sys/types.h> #include <unistd.h> int main(int argc, char *argv[]) { سنأخذ هنا عنوان IP ورقم بوابة كوسائط لبرنامجنا، نتابع … if (argc != 3) { std::cerr << "Run program as 'program <ipaddress> <port>'\n"; return -1; } auto &ipAddress = argv[1]; auto &portNum = argv[2]; addrinfo hints, *p; memset(&hints, 0, sizeof(hints)); hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; hints.ai_flags = AI_PASSIVE; int gAddRes = getaddrinfo(ipAddress, portNum, &hints, &p); if (gAddRes != 0) { std::cerr << gai_strerror(gAddRes) << "\n"; return -2; } if (p == NULL) { std::cerr << "No addresses found\n"; return -3; } ينشئ استدعاء ()socket قناة socket جديدة ويعيد الواصف الخاص بها … int sockFD = socket(p->ai_family, p->ai_socktype, p->ai_protocol); if (sockFD == -1) { std::cerr << "Error while creating socket\n"; return -4; } لاحظ عدم وجود استدعاء ()bind كما في برنامج Hello TCP Server، فذلك غير ضروري رغم أنك تستطيع استدعاءها، لأنه لا يلزم العميل أن يكون له رقم منفذ (port number) ثابت، لذا فإن الاستدعاء التالي سيربطه برقم عشوائي. سيحاول استدعاء ()connect أن ينشئ اتصال TCP بالخادم المحدد … int connectR = connect(sockFD, p -> ai_addr, p -> ai_addrlen); if (connectR == -1) { close(sockFD); std::cerr << "Error while connecting socket\n"; return -5; } std::string reply(15, ' '); هنا يحاول استدعاء ()recv الحصول على إجابة من الخادم، لكن الإجابة قد تحتاج إلى عدة استدعاءات لـ ()recv قبل أن تُستقبَل بالكامل، سنحاول حل هذا لاحقًا … auto bytes_recv = recv(sockFD, &reply.front(), reply.size(), 0); if (bytes_recv == -1) { std::cerr << "Error while receiving bytes\n"; return -6; } std::cout << "\nClient recieved: " << reply << std::endl; close(sockFD); freeaddrinfo(p); return 0; } مثال Hello TCP Server نقترح أن تلقي نظرة سريعة على كتاب Beej's Guide to Network Programming، والذي سيفسر لك معظم المفاهيم المُستخدَمة هنا. سننشئ خادم TCP بسيطًا هنا يعرض الجملة "Hello World" لكل الاستدعاءات الواردة، ثم يُغلقها. كما أنّ الخادم سيتواصل مع العملاء تكراريًا (iteratively)، أي عميلًا واحدًا في كل مرّة. سنشغّل الخادم عبر منفذ محدّد، لذلك سنأخذ رقم المنفذ كوسيط. انظر: #include <cstring> // sizeof() #include <iostream> #include <string> الترويسات التالية من أجل ()getaddrinfo و ()socket والدوال الصديقة … #include <arpa/inet.h> #include <netdb.h> #include <sys/socket.h> #include <sys/types.h> #include <unistd.h> // close() int main(int argc, char *argv[]) { هنا نتحقق مما إذا كان رقم المنفذ قد أُعطي أم لا … if (argc != 2) { std::cerr << "Run program as 'program <port>'\n"; return -1; } auto &portNum = argv[1]; في السطر التالي، عدد الاتصالات المسموح بها في طابور الوارد … const unsigned int backLog = 8; نحتاج إلى مؤشرين، res سيأخذ القيمة وp سيكرر عليها … addrinfo hints, *res, *p; memset( &hints, 0, sizeof(hints)); // راجع الكتاب لمزيد من التوضيحات hints.ai_family = AF_UNSPEC; // التي ستستخدم IP لا تحدّد بعدُ نسخة في السطر التالي، يشير SOCK_STREAM إلى TCP … hints.ai_socktype = SOCK_STREAM; hints.ai_flags = AI_PASSIVE; int gAddRes = getaddrinfo(NULL, portNum, &hints, &res); if (gAddRes != 0) { std::cerr << gai_strerror(gAddRes) << "\n"; return -2; } std::cout << "Detecting addresses" << std::endl; unsigned int numOfAddr = 0; يحرص طول ipv6 على إمكانية تخزين عنواني ipv4/6 في هذا المتغير، وبما أن ()getaddrinfo قد أعطتنا قائمة من العناوين، فسنكرر عليها ونسأل المستخدم أن يختار أحدها لربطها بالبرنامج … char ipStr[INET6_ADDRSTRLEN]; for (p = res; p != NULL; p = p -> ai_next) { void *addr; std::string ipVer; // ipv4 إن كان العنوان من النوع if (p -> ai_family == AF_INET) { ipVer = "IPv4"; sockaddr_in *ipv4 = reinterpret_cast < sockaddr_in *> (p -> ai_addr); addr = &(ipv4 -> sin_addr); ++numOfAddr; } // ipv6 إن كان العنوان من النوع else { ipVer = "IPv6"; sockaddr_in6 *ipv6 = reinterpret_cast < sockaddr_in6 * > (p -> ai_addr); addr = &(ipv6 -> sin6_addr); ++numOfAddr; } // من الشكل الثنائي إلى الشكل النصي IPv4/6 تحويل عناوين inet_ntop(p -> ai_family, addr, ipStr, sizeof(ipStr)); std::cout << "(" << numOfAddr << ") " << ipVer << " : " << ipStr << std::endl; } // في حال عدم العثور على أيّ عنوان if (!numOfAddr) { std::cerr << "Found no host address to use\n"; return -3; } // اسأل المستخدم أن يختار عنوانا std::cout << "Enter the number of host address to bind with: "; unsigned int choice = 0; bool madeChoice = false; do { std::cin >> choice; if (choice > (numOfAddr + 1) || choice < 1) { madeChoice = false; std::cout << "Wrong choice, try again!" << std::endl; } else madeChoice = true; } while (!madeChoice); p = res; // كواصف socketFD لننشئ قناة جديدة، ستُعاد // تعيد هذه الاستدعاءات في العادة القيمة -1 في حال وقوع خطأ ما int sockFD = socket(p->ai_family, p->ai_socktype, p->ai_protocol); if (sockFD == -1) { std::cerr << "Error while creating socket\n"; freeaddrinfo(res); return -4; } // لنربط العنوان بالقناة التي أنشأناها للتو int bindR = bind(sockFD, p->ai_addr, p->ai_addrlen); if (bindR == -1) { std::cerr << "Error while binding socket\n"; // في حال وقوع خطأ، احرص على إغلاق القناة وتحرير الموارد close(sockFD); freeaddrinfo(res); return -5; } // وأخيرا، ابدأ بالإنصات إلى الاتصالات الواردة عبر قناتنا int listenR = listen(sockFD, backLog); if (listenR == -1) { std::cerr << "Error while Listening on socket\n"; // في حال وقوع خطأ، احرص على إغلاق القناة وتحرير الموارد close(sockFD); freeaddrinfo(res); return -6; } // البنية كبيرة كفاية لتحمل عنوان العميل sockaddr_storage client_addr; socklen_t client_addr_size = sizeof(client_addr); const std::string response = "Hello World"; فيما يلي حلقة لا نهائية للتواصل مع الاتصالات الواردة، وستتعامل معها بالتتابع أي واحدة في كل مرة. كذلك فيما يلي من الأمثلة سنستدعي ()fork لكل اتصال مع العملاء … while (1) { // يعيد واصف قناة جديد accept استدعاء int newFD = accept(sockFD, (sockaddr *) &client_addr, &client_addr_size); if (newFD == -1) { std::cerr << "Error while Accepting on socket\n"; continue; } فيما يلي، يعيد استدعاء send البيانات التي مرَّرتها أنت كمعامِل ثاني، وطولها كمعامل ثالث، ويعيد عدد البِتَّات المرسلة فعليًا … auto bytes_sent = send(newFD, response.data(), response.length(), 0); close(newFD); } close(sockFD); freeaddrinfo(res); return 0; } سيعمل البرنامج على النحو التالي: Detecting addresses (1) IPv4 : 0.0.0.0 (2) IPv6 : :: Enter the number of host address to bind with: 1 هذا الدرس جزء من سلسلة دروس عن C++‎. ترجمة -بتصرّف- للفصل Chapter 127: Client server examples من كتاب C++ Notes for Professionals
  13. تطبيقات على التعاود يمكن استعمال التعاود في الكثير من التطبيقات المفيدة إذ يساعدنا على تبسيط الشيفرة ويعطيها قوة وفعالية على عكس لو لم نعتمد على مفهوم التعاود وإليك بعض هذه التطبيقات مع شيفراتها. حساب تسلسلات فيبوناتشي Fibonnaci sequence هذه هي الطريقة الأبسط لاستخدام التكرارية للحصول على العنصر رقم N من سلسلة Fibonnaci: int get_term_fib(int n) { if (n == 0) return 0; if (n == 1) return 1; return get_term_fib(n - 1) + get_term_fib(n - 2); } لكن المشكلة في هذه الطريقة أنّها غير مناسبة للأعداد الكبيرة، فكلما زادت قيمة ‎n‎، زاد عدد استدعاءات الدالة بشكل أسي (exponentially). يمكن الاستعاضة عن هذه الطريقة بالتكرارية المُذيّلة (tail recursion) على النحو التالي: int get_term_fib(int n, int prev = 0, int curr = 1) { if (n == 0) return prev; if (n == 1) return curr; return get_term_fib(n - 1, curr, prev + curr); } سيحسُب كل استدعاء للدالّة الحدّ التالي في تسلسل Fibonnaci، وعليه فإنّ عدد استدعاءات الدالّة يزيد خطيًا مع ‎n‎. التكرارية مع التذكّر Recursion with memoization قد تصبح الدوال التكرارية مكلفة للغاية، ويمكن جعلها أسرع بكثير عن طريق تخزين القيم التي سبق حسابها، إذا كانت دوالًّا خالصة -أي دوالًّا تُعيد دائمًا نفس القيمة عند استدعائها مع نفس الوسائط، ولا تعتمد على الحالة الخارجية ولا تعدّلها-، لكن هذا سيكون على حساب استخدام مزيد من الذاكرة. يوضّح المثال التالي كيفية حساب تسلسل فيبوناتشي باستخدام التكرارية التذكيرية: #include <map> int fibonacci(int n) { static std::map < int, int > values; if (n == 0 || n == 1) return n; std::map<int,int>::iterator iter = values.find(n); if (iter == values.end()) { return values[n] = fibonacci(n - 1) + fibonacci(n - 2); } else { return iter->second; } } لاحظ أنّه على الرغم من استخدام صيغة التكرارية البسيطة، إلّا أنّ تعقيد الدالّة يساوي O(n)‎ في الاستدعاء الأول، وسيكون التعقيد ثابتًا في الاستدعاءات اللاحقة مع نفس القيمة، أي O(1)‎. واعلم أنّ هذا التنفيذ ليس متعدّد المداخل (reentrant)، كما لا يسمح بالتخلص من القيم المُخزّنة. هناك تقديم بديل يعتمد على السماح بتمرير قاموس كوسيط إضافي: #include <map> int fibonacci(int n, std::map<int, int> values) { if (n==0 || n==1) return n; std::map<int,int>::iterator iter = values.find(n); if (iter == values.end()) { return values[n] = fibonacci(n-1) + fibonacci(n-2); } else { return iter->second; } } في هذا المثال، يُطلَب من المُستدعي أن يحفظ القاموس الذي يحتوي القيم المُخزّنة. ميزة هذا أنّ الدالّة أصبحت الآن متعدّدة المداخل (reentrant)، وأنّه المستدعي يستطيع إزالة القيم التي لم تعد مطلوبة، وهذا يحسّن استخدام الذاكرة. بالمقابل، يعيب هذه الطريقة أنّها تكسر التغليف (breaks encapsulation). إذ يمكن للمُستدعي تغيير الخرج عن طريق ملء القاموس بقيم غير صحيحة. الكائنات القابلة للاستدعاء Callable Objects الكائنات القابلة للاستدعاء هي هياكل C++‎ يمكن استخدامها كدوال، وتدخل فيها كل الأشياء التي يمكنك تمريرها إلى دالة المكتبة القياسية في C++‎ 17‏ invoke()‎ أو التي يمكن استخدامها في مُنشئ std::function، وهذا يشمل: مؤشّرات الدوال، والأصناف التي تحتوي على operator()‎، والأصناف ذات التحويلات الضمنية، ومراجع الدوال، ومؤشّرات الدوال التابعة ومؤشّرات الحقول (Pointers to member data)، وتعابير لامبدا. تُستخدَم الكائنات القابلة للاستدعاء في العديد من خوارزميات مكتبة القوالب القياسية STL كدوال شرطية (predicate). مؤشّرات الدوال Function Pointers تعدّ مؤشّرات الدوال الطريقة الأبسط لتمرير الدوال، ويمكن استخدامها أيضًا في لغة C، وإن أردت استخدامها ككائنات قابلة للاستدعاء، فيمكنك تعريف مؤشّر الدالّة على النحو التالي: typedef returnType(*name)(arguments); // All using name = returnType(*)(arguments); // <= C++11 using name = std::add_pointer<returnType(arguments)>::type; // <= C++11 using name = std::add_pointer_t<returnType(arguments)>; // <= C++14 يستخدم المثال التالي مؤشّر دالة لكتابة دالة مخصّصة لترتيب المتجهات: using LessThanFunctionPtr = std::add_pointer_t<bool(int, int)>; void sortVectorInt(std::vector<int>&v, LessThanFunctionPtr lessThan) { if (v.size() < 2) return; if (v.size() == 2) { if (!lessThan(v.front(), v.back())) // استدعاء مؤشر الدالة std::swap(v.front(), v.back()); return; } std::sort(v, lessThan); } bool lessThanInt(int lhs, int rhs) { return lhs < rhs; } sortVectorInt(vectorOfInt, lessThanInt); // تمرير المؤشر إلى دالة حرّة struct GreaterThanInt { static bool cmp(int lhs, int rhs) { return lhs > rhs; } }; sortVectorInt(vectorOfInt, &GreaterThanInt::cmp); // تمرير المؤشر إلى دالة تابعة ساكن كان بإمكاننا استدعاء مؤشّر الدالّة بدلًا من ذلك، بإحدى الطرق التالية : (*lessThan)(v.front(), v.back()) std::invoke(lessThan, v.front(), v.back()) // <= C++17 الأصناف ذات التابع operator()‎‏ (الكائنات الداليّة Functors) يمكن استخدام كل الأصناف التي تحمّل ‎operator()‎ تحميلًا زائدًا ككائنات دوال، ويمكن كتابة هذه الأصناف يدويًا (يشار إليها غالبًا باسم الكائنات الدالية - functors)، أو إنشاؤها تلقائيًا بواسطة المُصرِّف عن طريق كتابة تعابير لامدا (منذ C++‎ 11 وما بعده). struct Person { std::string name; unsigned int age; }; // كائن دالي، يعثر على الشخص باسمه struct FindPersonByName { FindPersonByName(const std::string &name) : _name(name) {} // التابع المزاد تحميله والذي سيُستدعى bool operator()(const Person &person) const { return person.name == _name; } private: std::string _name; }; std::vector<Person> v; // نفترض أنّه يحتوي البيانات std::vector<Person>::iterator iFind = std::find_if(v.begin(), v.end(), FindPersonByName("Foobar")); // ... بحكم أنّ للكائنات الدالية هويّتها الخاصة، فلا يمكن وضعها في التعريفات النوعية typedef، ويجب قبولها عبر وسيط القالب. يمكن أن يكون تعريف ‎std::find_if‎ على الشكل التالي: template < typename Iterator, typename Predicate > Iterator find_if(Iterator begin, Iterator end, Predicate &predicate) { for (Iterator i = begin, i != end, ++i) if (predicate( *i)) return i; return end; } يمكن استدعاء الدالة الشرطية عبر استدعاء: ‎std::invoke(predicate, *i)‎، وذلك منذ الإصدار C++ 17. هذا الدرس جزء من سلسلة دروس عن C++‎. ترجمة -بتصرّف- للفصل Chapter 124: Recursion in C++‎ والفصل Chapter 126: Callable Objects من كتاب C++ Notes for Professionals
  14. السمة [[fallthrough]] الإصدار ≥ C++‎ 17 عند إنهاء تعليمة case بكلمة switch ستنفَّذ الشيفرة التي تليها، وإن أردت منع هذا السلوك، فاستخدم تعليمة ‎‎break‎‎. يُسمّى هذا السلوك بـ "السقطة" (fallthrough)، وقد ينجم عنه أخطاء غير مقصودة، لذا تطلق العديد من المُصرِّفات والمحلّلات الساكنة (static analyzers) تحذيرًا منه. وقد أُدخلت سمة قياسية (standard attribute) في C++ 17 من أجل الإشارة إلى أنّ التحذير ليس ضرويًا إذا كانت السقطة مقصودة في الشيفرة، ويمكن للمصرّفات إعطاء تحذيرات بأمان إذا أنهيت case بالكلمةُ المفتاحيةُ ‎break‎ أو ‎[[fallthrough]]‎، وكانت مؤلفة من تعليمة واحد على الأقل. switch (input) { case 2011: case 2014: case 2017: std::cout << "Using modern C++" << std::endl; [[fallthrough]]; // > لا يوجد تحذير case 1998: case 2003: standard = input; } راجع هذا الاقتراح لمزيد من الأمثلة حول كيفية استخدام ‎[[fallthrough]]‎. السمة [[nodiscard]] الإصدار ≥ C++‎ 17 يمكن استخدام السمة ‎[[nodiscard]]‎ للإشارة إلى أنّه لا ينبغي تجاهل القيمة المُعادة من دالة عند استدعاء دالة، ويجب على االمُصرِّف في حال تجاهلها إعطاء تحذير بهذا الشأن. يمكن إضافة هذه السمة إلى: تعريف دالّة نوع ينتج عن إضافة السمة إلى نوع ما نفسُ السلوكِ الناتج عن إضافة السمة إلى كل الدوالّ التي تُعيد ذلك النوع. template<typename Function> [[nodiscard]] Finally<std::decay_t<Function>> onExit(Function &&f); void f(int &i) { assert(i == 0); // لجعل التعليقات واضحة ++i; // i == 1 auto exit1 = onExit([&i]{ --i; }); // f() التخفيض بـ 1 عند الخروج من ++i; // i == 2 onExit([&i]{ --i; }); // خطأ: محاولة التخفيض بـ 1 مباشرة // يُتوقع أن يصدر المصرف تنبيها std::cout << i << std::end; // القيمة المتوقعة 2، لكن القيمة الحقيقية هي 1 } راجع هذا الاقتراح لمزيد من الأمثلة حول كيفية استخدام ‎[[nodiscard]]‎. السمة [[deprecated]] والسمة [[deprecated("reason")‎]] الإصدار ≥ C++‎ 14 قدَّمت C++‎ 14 طريقة قياسية لإهمال (deprecating) الدوال عبر السمات. يمكن استخدام ‎[[deprecated]]‎ للإشارة إلى إهمال الدالّة. ويسمح التعبير ‎[[deprecated("reason")]]‎بتحديد سبب للإهمال يمكن أن نجعل المُصرِّف يظهِره. void function(std::unique_ptr<A> &&a); // توفير رسالة محدّدة تساعد المبرمجين الآخرين على تصحيح شيفراتهم [[deprecated("Use the variant with unique_ptr instead, this function will be removed in the next release")]] void function(std::auto_ptr<A> a); // في حال عدم وجود رسالة، سيُطلق تنبيه عام عند الاستدعاء [[deprecated]] void function(A *a); يُمكن تطبيق هذه السمة على: تصريح صنف. اسم typedef. متغيّر. حقل غير ساكن (non-static data member). دالة. تعداد (enum). تخصيص قالب (template specialization). (المرجع: c++14 standard draft: 7.6.5 Deprecated attribute) السمة [[maybe_unused]] تُستعمل السمة ‎[[maybe_unused]]‎ للدلالة على احتمال عدم استخدام منطق بعينه داخل الشيفرة، وترتبط غالبًا بشروط المعالج الأَولي (preprocessor). ويمكن استخدام هذه السمة لمنع هذا السلوك من خلال الإشارة إلى أنّ هذا الأمر مقصود، نظرًا لأنّ المُصرِّفات قد تبعث تحذيرات بشأن المتغيّرات غير المستخدمة. وتُعد القيم المعادة التي تشير إلى نجاح عملية التنقيح من الأمثلة التطبيقية للمتغيّرات التي تُستخدم أثناء عمليات تنقيح بنيات الشيفرة (debug builds)، ولكن لا تبقى إليها حاجة في مرحلة الإنتاج. ويجب التحقق من شرط النجاح أثناء تنقيح أخطاء البُنيات، رغم أن تلك التحققات ستُحذف في مرحلة الإنتاج لعدم الحاجة إليها. [[maybe_unused]] auto mapInsertResult = configuration.emplace("LicenseInfo", stringifiedLicenseInfo); assert(mapInsertResult.second); // لا تُستدعى إلا خلال الإطلاق، لذا لا ينبغي أن تكون في القاموس هناك مثال أكثر تعقيدًا، وهي الدوال المساعدة التي توضع في فضاء اسم غير مُسمّى (unnamed namespace)، وتلك الدوال إن لم تُستخدَم أثناء التصريف، فقد يطلق االمُصرِّف تحذيرًا بهذا الشأن، لذا قد ترغب في حمايتها باستخدام نفس وسوم المعالج الأولي التي تُستخدَم مع المُستدعي (caller)، لكن قد يكون هذا معقدًا، لهذا يُعدُّ استخدام السمة ‎[[maybe_unused]]‎ بديلاً أسهل في الصيانة. namespace { [[maybe_unused]] std::string createWindowsConfigFilePath(const std::string &relativePath); // TODO: … BSD, MAC أعد استخدام هذا على [[maybe_unused]] std::string createLinuxConfigFilePath(const std::string &relativePath); } std::string createConfigFilePath(const std::string &relativePath) { #if OS == "WINDOWS" return createWindowsConfigFilePath(relativePath); #elif OS == "LINUX" return createLinuxConfigFilePath(relativePath); #else #error "OS is not yet supported" #endif } راجع هذا الاقتراح لمزيد من الأمثلة حول كيفية استخدام ‎[[maybe_unused]]‎. السمة [[noreturn]] الإصدار ≥ C++‎ 11 قدّمت C++‎ 11 سمةَ ‎[[noreturn]]‎ التي يمكن استخدامها مع دالّة للإشارة إلى أنّ تلك الدالّة لا تُعاد إلى المُستدعي، سواء عبر تعليمة return، أو عند الوصول إلى نهاية متنها -من المهم الإشارة إلى أن هذا لا ينطبق على الدوال الفارغة ‎void‎، نظرًا لأنّها تعود إلى المُستدعي، ولكن لا تعيد أيّ قيمة-. قد تنتهي مثل هذه الدوالّ عبر استدعاء ‎std::terminate‎ أو ‎std::exit‎، أو عبر رفع اعتراض (throwing an exception). تجدر الإشارة أيضًا إلى أنّ مثل هذه الدوالّ يمكن أن تُعاد عبر تنفيذ ‎longjmp‎. على سبيل المثال، الدالّة أدناه إمّا أن ترفع اعتراضًا أو تستدعي ‎std::terminate‎، لذلك فهي مرشح جيد لاستخدام ‎[[noreturn]]‎: [[noreturn]] void ownAssertFailureHandler(std::string message) { std::cerr << message << std::endl; if (THROW_EXCEPTION_ON_ASSERT) throw AssertException(std::move(message)); std::terminate(); } يسمح هذا السلوك للمُصرِّف بإنهاء الدوال التي لا تحتوي على تعليمة return إذا كان يعلم أنّ الشيفرة لن تُنفَّذ. هنا، لن يحتاج المُصرِّف إلى إضافة الشيفرة الموجودة أسفل ذلك الاستدعاء نظرًا لأنّ استدعاء ‎ownAssertFailureHandler‎ في الشيفرة أدناه لن يعود أبدًا: std::vector < int > createSequence(int end) { if (end > 0) { std::vector < int > sequence; sequence.reserve(end + 1); for (int i = 0; i <= end; ++i) sequence.push_back(i); return sequence; } ownAssertFailureHandler("Negative number passed to createSequence()"s); // return std::vector<int>{}; //< Not needed because of [[noreturn]] } سيكون السلوك غير معرَّف إذا كانت الدالّة ستعاد، ونتيجة لذلك لا يُسمح بما يلي: [[noreturn]] void assertPositive(int number) { if (number >= 0) return; else ownAssertFailureHandler("Positive number expected"s); //< [[noreturn]] } لاحظ أنّ ‎[[noreturn]]‎ تُستخدم في الغالب في الدوال الفارغة، لكنها ليست ضرورية، وهذا يسمح باستخدام الدوال في البرمجة العامة (generic programming): template<class InconsistencyHandler> double fortyTwoDivideBy(int i) { if (i == 0) i = InconsistencyHandler::correct(i); return 42. / i; } struct InconsistencyThrower { static [[noreturn]] int correct(int i) { ownAssertFailureHandler("Unknown inconsistency"s); } } struct InconsistencyChangeToOne { static int correct(int i) { return 1; } } double fortyTwo = fortyTwoDivideBy<InconsistencyChangeToOne>(0); double unreachable = fortyTwoDivideBy<InconsistencyThrower>(0); تحتوي دوال المكتبة القياسية التالية على هذه السمات: std::abort std::exit std::quick_exit std::unexpected std::terminate std::rethrow_exception std::throw_with_nested std::nested_exception::rethrow_nested هذا الدرس جزء من سلسلة دروس عن C++‎. ترجمة -بتصرّف- للفصل Chapter 123: Attributes من كتاب C++ Notes for Professionals
  15. قاعدة الصفر الإصدار ≥ 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
  16. كيفية إنشاء خيط std::thread تُنشَأ الخيوط في C++‎ باستخدام الصنف std::thread، والخيط (thread) هو مسار تنفيذ منفصل أشبه بمساعد يساعدك على أداء مهمة فرعية أثناء إنجازك لمهمة أخرى، ثم يتوقف عند اكتمال تنفيذ الشيفرة في الخيط. يجب أن تمرر شيفرة ما للخيط عند إنشائه لينفذها، مثل: الدوالّ الحرّة (Free functions). الدوال التابعة. الكائنات الدالية (Functor). تعبيرات لامدا. تكون المعاملات المُمرَّرة: المعامل التفاصيل other تأخذ ملكية ‎other‎، بحيث أنّ ‎other‎ تفقد ملكيّة الخيط (thread) func دالّة لأجل استدعائها في خيط منفصل args وسائط لـ ‎func‎ table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; } هذا مثال على تمرير دالّة حرّة لتُنفّذ في خيط منفصل: #include <iostream> #include <thread> void foo(int a) { std::cout << a << '\n'; } int main() { // إنشاء وتنفيذ الخيط تكون foo هنا هي الدالة محل التنفيذ، و10 هو الوسيط الممرر إليها، وسينفَذ الخيط الآن بشكل منفصل ويُنتَظر هنا حتى تمام تنفيذه، انظر: std::thread thread(foo, 10); thread.join(); return 0; } هذا مثال على استخدام تابع ليُنفّذ في خيط منفصل: #include <iostream> #include <thread> class Bar { public: void foo(int a) { std::cout << a << '\n'; } }; int main() { Bar bar; // إنشاء وتنفيذ الخيط std::thread thread(&Bar::foo, &bar, 10); // Pass 10 to member function التابع سيُنفَّذ الآن بشكل منفصل، سننتظر هنا حتى تمام تنفيذ الخيط، فهذه عملية معطِّلة (Blocking)، نتابع: thread.join(); return 0; } مثال على استخدام كائن دالّي: #include <iostream> #include <thread> class Bar { public: void operator()(int a) { std::cout << a << '\n'; } }; int main() { Bar bar; // انشاء الخيط وتنفيذه std::thread thread(bar, 10); // مرر 10 إلى الكائن الدالي. سيُنفَّذ الكائن الدالي الآن بشكل منفصل، سننتظر هنا حتى تمام تنفيذ الخيط، فهذه عملية معطِّلة (Blocking)، نتابع: thread.join(); return 0; } مثال على تمرير تعبير لامدا: #include <iostream> #include <thread> int main() { auto lambda =[](int a) { std::cout << a << '\n'; }; // انشاء الخيط وتنفيذه std::thread thread(lambda, 10); // تمرير 10 إلى تعبير لامدا سيُنفَّذ تعبير لامدا الآن بشكل منفصل، وسننتظر اكتمال تنفيذ الخيط، فهي عملية معطِّلة، نتابع: thread.join(); return 0; } تمرير مرجع إلى خيط لا يمكنك تمرير مرجع -أو مرجع ثابت ‎const‎- مباشرةً إلى خيط، لأنّ الخيط سينسخها/ينقلها، بل استخدم ‎std::reference_wrapper‎: void foo(int & b) { b = 10; } int a = 1; std::thread thread { foo, std::ref(a) }; الآن، a ممرَّرة على شكل مرجع، تابع المثال: thread.join(); std::cout << a << '\n'; // 10 void bar(const ComplexObject &co) { co.doCalculations(); } ComplexObject object; std::thread thread { bar, std::cref(object) }; أيضًا، object ممرَّر الآن على شكل &const، تابع: thread.join(); std::cout << object.getResult() << '\n'; استخدام std::async بدلاً من std::thread تستطيع ‎std‎::async أن تنشئ خيوطًا رغم أنها أضعف من ‎std‎::thread، لكنّها تتميّر بأنّها أسهل في حال كنت تريد تنفيذ دالة بشكل غير متزامن (asynchronously). استدعاء دالّة بشكل غير متزامن #include <future> #include <iostream> unsigned int square(unsigned int i) { return i * i; } int main() { auto f = std::async (std::launch::async, square, 8); std::cout << "square currently running\n"; // square افعل شيئا ما أثناء تنفيذ std::cout << "result is " << f.get() << '\n'; // square الحصول على النتيجة من } أخطاء شائعة تعيد std::async كائن std::future يحتوي القيمة المُعادة التي ستحسُبها الدالّة، وعند تدمير ‎future‎ فإنّها تنتظر حتى يكتمل الخيط ممّا يجعل الشيفرة أحادية الخيوط (single threaded). يُمكن تجاهَل هذا السلوك إذا لم تكن بحاجة إلى القيمة المُعادة: std::async (std::launch::async, square, 5); في الشيفرة السابقة، انتهى تنفيذ الخيط لأن قيمة future قد دُمِّرت. تعمل std::async بدون سياسة إطلاق (launch policy)، لذا فإنّ التعبير ‎std::async(square, 5);‎ سيُصرَّف. عندئذ يقرر النظام إن كان سينشئ خيطًا أم لا. والفكرة أنّ النظام سيختار إنشاء خيط إن لم يكن عدد الخيوط قيد التنفيذ أكبر ممّا يمكنه التعامل معه. لكن عادة ما تختار التنفيذات (implementations) عدم إنشاء خيط في مثل هذه المواقف، لذا ستحتاج إلى إعادة تعريف هذا السلوك باستخدام ‎std::launch::async‎، التي تجبر النظام على إنشاء الخيط. أساسيات التزامن بين الخيوط يمكن تحقيق تزامن الخيوط باستخدام كائنات المزامنة (mutexes)، وتوفّر المكتبة القياسية العديد من أنواع كائنات المزامنة تلك لكن أبسطها هو ‎std::mutex‎ وسنتحدث عن تلك الكائنات بالتفصيل في القسم التالي. ولقفل كائن مزامنة ستحتاج إلى إنشاء قفل (lock) خاصّ به، وأبسط أنواع الأقفال هو ‎std::lock_guard‎: std::mutex m; void worker() { std::lock_guard<std::mutex > guard(m); // يحصل على قفلٍ على كائن المزامنة // الشيفرة المُزامَنة هنا } // سيُحرَّر كائن المزامنة عندما يخرج الدرع عن النطاق سيُقفل كائن المزامنة باستخدام ‎std::lock_guard‎ طول العمر الافتراضي لكائن القفل، وإن أردت التحكم في المناطق المقفلة يدويًا، فاستخدم ‎std::unique_lock‎: std::mutex m; void worker() { افتراضيًا، إنشاء unique_lock من كائن مزامنة سيقفل ذلك الكائن، ونستطيع إنشاء درع في حالة مفتوحة عبر تمرير std::defer_lock كوسيط ثاني ثم نقفل يدويًا فيما بعد، تابع المثال: std::unique_lock<std::mutex > guard(m, std::defer_lock); // لم يُقفَل كائن المزامنة بعد guard.lock(); // شيفرة خاصة guard.unlock(); // تحرير كائن المزامنة مجددا } كائنات المزامنة Mutexes كائنات المزامنة هي بنيات مزامنة بسيطة غير تكرارية (non-recursive) تُستخدَم لحماية البيانات التي يمكن الوصول إليها من خيوط متعددة (multiple threads). std::atomic_int temp { 0 }; std::mutex _mutex; std::thread t([ &]() { while (temp != -1) { std::this_thread::sleep_for(std::chrono::seconds(5)); std::unique_lock<std::mutex > lock(_mutex); temp = 0; } }); while (true) { std::this_thread::sleep_for(std::chrono::milliseconds(1)); std::unique_lock<std::mutex > lock(_mutex, std::try_to_lock); if (temp < INT_MAX) temp++; cout << temp << endl; } أنواع كائنات المزامنة توفّر الإصدارات C++1x عددًا من أصناف كائنات المزامنة: std::mutex - توفّر وظائف القفل الأساسية. std::timed_mutex - توفّر وظائف try_to_lock std::recursive_mutex - تتيح القفل التكراري من قِبل نفس الخيط std::shared_mutex و std::shared_timed_mutex - توفّران وظائف قفل مشتركة وحصرية الأقفال std::lock تستخدِم الأقفال std::lock خوارزميات لتجنب الشلل الوظيفي (deadlock) من أجل قفل كائنات المزامنة. وعند رفع اعتراض أثناء استدعاء لقَفل عدة كائنات فستفتح ‎std::lock‎ الكائنات المُقفلة قبل إعادة رفع الاعتراض. std::lock(_mutex1, _mutex2); الأقفال الحصرية (std::unique_lock) والأقفال المشتركة (std::shared_lock) والأقفال المُؤمّنة ( std::lock_guard) تُستخدم هذه الأقفال مع آلية RAII للحصول على أقفال المحاولة (try locks)، وأقفال المحاولة الموقوتة (timed try locks)، والأقفال التكرارية (recursive locks). std::unique_lock - تسمح بالملكية الحصرية لكائنات المزامنة. std::shared_lock - تسمح بالملكية المشتركة لكائنات المزامنة، إذ يمكن لعدّة خيوط أن تحتفظ بقفل مشتركtd::shared_locks خاصّ بكائن مزامنة مشترك std::shared_mutex. وقد أتيح منذ C++‎ 14 std::lock_guard - بديل خفيف للأقفال الحصرية std::unique_lock والأقفال المشتركة std::shared_lock. #include <unordered_map> #include <mutex> #include <shared_mutex> #include <thread> #include <string> #include <iostream> class PhoneBook { public: std::string getPhoneNo(const std::string &name) { std::shared_lock<std::shared_timed_mutex > l(_protect); auto it = _phonebook.find(name); if (it != _phonebook.end()) return (*it).second; return ""; } void addPhoneNo(const std::string &name, const std::string &phone) { std::unique_lock<std::shared_timed_mutex > l(_protect); _phonebook[name] = phone; } std::shared_timed_mutex _protect; std::unordered_map<std::string, std::string > _phonebook; }; استراتيجيات قفل الأصناف: std::try_to_lock و std::adopt_lock و std::defer_lock لدينا ثلاث استراتيجيات لتختار منها عند إنشاء قفل حصري: ‎std::try_to_lock‎ و std::defer_lock و std::adopt_lock: std::try_to_lock - تسمح بمحاولة القفل (trying a lock) بدون تعطيل: { std::atomic_int temp { 0 }; std::mutex _mutex; std::thread t([ &]() { while (temp != -1) { std::this_thread::sleep_for(std::chrono::seconds(5)); std::unique_lock<std::mutex > lock(_mutex, std::try_to_lock); if (lock.owns_lock()) { //افعل شيئًا temp = 0; } } }); while (true) { std::this_thread::sleep_for(std::chrono::seconds(1)); std::unique_lock<std::mutex > lock(_mutex, std::try_to_lock); if (lock.owns_lock()) { if (temp < INT_MAX) { ++temp; } std::cout << temp << std::endl; } } } std::defer_lock - تسمح بإنشاء بنية قفل دون الحصول على القفل. ذلك أنه عند قفل أكثر من كائن مزامنة، فهناك إمكانية لحدوث شلل وظيفي إذا حاولت دالتان الحصول على الأقفال في نفس الوقت: { std::unique_lock<std::mutex > lock1(_mutex1, std::defer_lock); std::unique_lock<std::mutex > lock2(_mutex2, std::defer_lock); lock1.lock() lock2.lock(); // شلل وظيفي هنا std::cout << "Locked! << std::endl; //... } يمكن الحصول على الأقفال وإصدارها بالترتيب المناسب مع الشيفرة التالية، بغض النظر عما يحدث في الدالة: { std::unique_lock<std::mutex > lock1(_mutex1, std::defer_lock); std::unique_lock<std::mutex > lock2(_mutex2, std::defer_lock); std::lock(lock1, lock2); // لن يحدث شلل وظيفي. std::cout << "Locked! << std::endl; //... } std::adopt_lock - لا تحاول القفل مرّة ثانية إذا كان الخيط المُستدعي يملك القفل حاليًا. { std::unique_lock<std::mutex > lock1(_mutex1, std::adopt_lock); std::unique_lock<std::mutex > lock2(_mutex2, std::adopt_lock); std::cout << "Locked! << std::endl; //... } تذكّر أنّ std::adopt_lock ليست بديلاً عن استخدام كائنات المزامنة التكرارية، فسيحرَّر كائن المزامنة عند خروج القفل عن النطاق. الأقفال النطاقية std::scoped_lock ‏(C++ 17) توفّر الأقفال النطاقية std::scoped_lock دلالات RAII لامتلاك كائن مزامنة أو أكثر، وتُستخدم مع خوارزميات تجنّب الشلل الوظيفي التي تستخدمها الأقفال العادية ‎std::lock‎. وعندما تُدمَّر ‎std::scoped_lock‎، فإن كائنات المزامنة تُحرّر بالترتيب العكسي لترتيب الحصول عليها. { std::scoped_lock lock { _mutex1, _mutex2 }; // افعل شيئا ما } كائنات المزامنة التكرارية Recursive Mutex تسمح كائنات المزامنة التكرارية لخيط ما بقفل أحد الموارد بدون حد معين، ولا توجد مبررات كثيرة لاستخدام هذه التقنية، لكن قد تحتاج بعض التنفيذات (implementations) المعقّدة إلى استدعاء نسخة مُحمّلة تحميلا زائدًا (overloaded) من دالّة دون تحرير القفل. انظر المثال التالي: std::atomic_int temp { 0 }; std::recursive_mutex _mutex; تطلق launch_deferred مهامًا غير متزامنة على نفس معرِّف الخيط، تابع … auto future1 = std::async ( std::launch::deferred, [ &]() { std::cout << std::this_thread::get_id() << std::endl; std::this_thread::sleep_for(std::chrono::seconds(3)); std::unique_lock<std::recursive_mutex > lock(_mutex); temp = 0; }); auto future2 = std::async ( std::launch::deferred, [ &]() { std::cout << std::this_thread::get_id() << std::endl; while (true) { std::this_thread::sleep_for(std::chrono::milliseconds(1)); std::unique_lock<std::recursive_mutex > lock(_mutex, std::try_to_lock); if (temp < INT_MAX) temp++; cout << temp << endl; } }); future1.get(); future2.get(); هياكل مزامنة الخيوط قد يتطلّب استعمال الخيوط أن تستخدم بعض تقنيات المزامنة إذا كانت الخيوط تتفاعل مع بعضها، وسنتحدث في هذا الموضوع عن عدد من الهياكل التي توفّرها المكتبة القياسية لحل هذه المشكلة. std::condition_variable_any و std::cv_status ‎std::condition_variable_any‎ هي تعميم لـ‎std::condition_variable‎، ويمكن أن تعمل مع أيّ نوع من الهياكل الأساسية القابلة للقفل (BasicLockable structure). وstd::cv_status كقيمة مُعادة من متغيّر شرطي يكون لها رمزا إعادة (return codes) محتملان: std::cv_status::no_timeout: إن لم تكن هناك مهلة (timeout)، وتمّ إشعارالمتغيّر الشرطي. std::cv_status::timeout: عند انتهاء مهلة المتغيّر الشرطي. الأقفال المشتركة std::shared_lock يمكن استخدام الأقفال المشتركة مع قفل حصريّ (unique lock) من أجل السماح بعدّة قارئات (readers)، لكن مع كاتبات (writers) حصرية. #include <unordered_map> #include <mutex> #include <shared_mutex> #include <thread> #include <string> #include <iostream> class PhoneBook { public: string getPhoneNo(const std::string &name) { shared_lock<shared_timed_mutex> r(_protect); auto it = _phonebook.find(name); if (it == _phonebook.end()) return (*it).second; return ""; } void addPhoneNo(const std::string &name, const std::string &phone) { unique_lock<shared_timed_mutex> w(_protect); _phonebook[name] = phone; } shared_timed_mutex _protect; unordered_map<string, string> _phonebook; }; std::call_once و std::once_flag تضمن std::call_once ألّا تُنفّذ دالّة معيّنة إلّا مرّة واحدة فقط من قبل الخيوط المتنافسة (competing threads). وتطلق خطأ نظامي std::system_error في حال حدث ذلك. كذلك فإن std::call_once تُستخدم مع ‎td::once_flag‎. #include <mutex> #include <iostream> std::once_flag flag; void do_something(){ std::call_once(flag, [](){std::cout << "Happens once" << std::endl;}); std::cout << "Happens every time" << std::endl; } قفل الكائنات لتحسين كفاءة الوصول قد ترغب في قفل الكائن بالكامل أثناء إجراء عمليات متعدّدة عليه، كأن تريد فحصه أو تعديله باستخدام المُكرّرات. وإن كنت بحاجة إلى استدعاء عدّة توابع، فمن الأفضل عمومًا قفله بالكامل بدلاً قفل التوابع فرديّا، انظر: class text_buffer { // لأجل تحسين القراءة والصيانة using mutex_type = std::shared_timed_mutex; using reading_lock = std::shared_lock<mutex_type> ; using updates_lock = std::unique_lock<mutex_type> ; public: يعيد هذا قفلًا نطاقيًا (scoped lock) تستطيع عدة قارئات أن تتشاركه مع استثناء الكاتبات في نفس الوقت، تابع المثال … [[nodiscard]] reading_lock lock_for_reading() const { return reading_lock(mtx); } يعيد هذا قفلًا نطاقيًا خاصًا بكاتب واحد، مع منع القارئات، تابع … [[nodiscard]] updates_lock lock_for_updates() { return updates_lock(mtx); } char* data() { return buf; } char const* data() const { return buf; } char* begin() { return buf; } char const* begin() const { return buf; } char* end() { return buf + sizeof(buf); } char const* end() const { return buf + sizeof(buf); } std::size_t size() const { return sizeof(buf); } private: char buf[1024]; mutable mutex_type mtx; // للكائنات الثابتة بأن تُقفَل mutable يسمح }; يسمح mutable في السطر الأخير الشيفرة أعلاه بأن تُقفَل الكائنات الثابتة، ويُقفل الكائن عند حساب المجموع (checksum) من أجل القراءة، وهذا سيفسح المجال أمام الخيوط الأخرى التي ترغب في القراءة من الكائن في نفس الوقت بأن تقرأ منه. std::size_t checksum(text_buffer const &buf) { std::size_t sum = 0xA44944A4; // قفل الكائن لأجل القراءة auto lock = buf.lock_for_reading(); for (auto c: buf) sum = (sum << 8) | (((unsigned char)((sum & 0xFF000000) >> 24)) ^ c); return sum; } ويؤدّي مسح الكائن إلى تحديث بياناته الداخلية، لذا يجب فعل ذلك باستخدام قفل حصري. void clear(text_buffer & buf) { auto lock = buf.lock_for_updates(); // قفل حصري std::fill(std::begin(buf), std::end(buf), '\0'); } يجب توخي الحذر دائمًا عند الحصول على أكثر من قفل، والحرص على الحصول على الأقفال بنفس الترتيب لجميع الخيوط. void transfer(text_buffer const &input, text_buffer &output) { auto lock1 = input.lock_for_reading(); auto lock2 = output.lock_for_updates(); std::copy(std::begin(input), std::end(input), std::begin(output)); } ملاحظة: من الأفضل إنجاز ذلك باستخدام std::deferred::lock ثمّ استدعاء std::lock متغير تقييد الوصول متغير تقييد الوصول (Semaphore) غير متاح حاليًا في C++‎، ولكن يمكن تنفيذه بسهولة باستخدام كائنات المزامنة والمتغيّرات الشرطية. هذا المثال مأخوذ من: C++0x has no semaphores? How to synchronize threads متغيرات تقييد الوصول في C++‎ 11 انظر المثال التوضيحي التالي: #include <mutex> #include <condition_variable> class Semaphore { public: Semaphore(int count_ = 0): count(count_) {} inline void notify(int tid) { std::unique_lock<std::mutex > lock(mtx); count++; cout << "thread " << tid << " notify" << endl; //أشعِر الخيط المنتظِر. cv.notify_one(); } inline void wait(int tid) { std::unique_lock<std::mutex > lock(mtx); while (count == 0) { cout << "thread " << tid << " wait" << endl; // notify انتظر كائن المزامنة إلى حين استدعاء cv.wait(lock); cout << "thread " << tid << " run" << endl; } count--; } private: std::mutex mtx; std::condition_variable cv; int count; }; مثال على استخدام متغير تقييد الوصول تضيف الدالّة التالية أربعة خيوط، تتنافس ثلاثة منها على متغير تقييد الوصول الذي يُضبط عدّاده عند القيمة 1. وسيستدعي الخيط الأبطأ ‎notify_one()‎، ممّا يسمح لأحد الخيوط المنتظِرة بالمتابعة. ونتيجة لهذا تبدأ ‎s1‎ على الفور، مما سيُبقي عدّاد متغير تقييد الوصول ‎count‎ دون القيمة 1، وستنتظر الخيوط الأخرى دورها في المتغيّر الشرطي حتى استدعاء notify()‎‎. int main() { Semaphore sem(1); thread s1([ &]() { while (true) { this_thread::sleep_for(std::chrono::seconds(5)); sem.wait(1); } }); thread s2([ &]() { while (true) { sem.wait(2); } }); thread s3([ &]() { while (true) { this_thread::sleep_for(std::chrono::milliseconds(600)); sem.wait(3); } }); thread s4([ &]() { while (true) { this_thread::sleep_for(std::chrono::seconds(5)); sem.notify(4); } }); s1.join(); s2.join(); s3.join(); s4.join(); ... } إنشاء ساحة خيوط بسيطة خيوط C++‎ 11 الأساسية منخفضة المستوى نسبيًا، لذا يمكن استخدامها لكتابة كائنات عالية المستوى مثل ساحة الخيوط (thread pool): الإصدار ≥ C++‎ 14 يشكل كائن المزامنة والمتغير الشرطي وكائن deque طابورًا من المهام آمن على الخيوط (thread-safe): struct tasks { std::mutex m; std::condition_variable v; … لاحظ أن <packaged_task<void تستطيع تخزين <packaged_task<R … : std::deque< std::packaged_task < void() >> work; // هذا سيمثل العمل الذي أنجزته الخيوط std::vector<std::future < void>> finished; (queue (lambda سيضيف لامدا إلى قائمة المهام التي سينفذها الخيط …: template < class F, class R = std::result_of_t < F &() >> std::future<R> queue(F && f) { // لتقسيم التنفيذ - packaged task - تغليف كائن الدالة في مهمة محزومة std::packaged_task < R() > p(std::forward<F> (f)); auto r = p.get_future(); // الحصول على القيمة من المهمة قيد تنفيذ { std::unique_lock<std::mutex > l(m); سنخزن المهمة <()R> على شكل <()void>، تابع: work.emplace_back(std::move(p)); } v.notify_one(); // إيقاظ الخيط ليعمل على المهمة return r; // إعادة النتيجة المستقبلية للمهمة } والآن، نبدأ عدد N من الخيوط في ساحة الخيوط، نتابع المثال: void start(std::size_t N = 1) { for (std::size_t i = 0; i < N; ++i) { كل الخيوط الآن غير متزامنة std::async، وتنفذ ()this->thread_task، تابع: finished.push_back( std::async ( std::launch::async, [this] { thread_task(); } ) ); } } تلغي ()abort كل المهام التي لم تنطلق بعد، وإخطار كل الخيوط العاملة أن تتوقف، وتنتظرهم حتى ينتهوا، تابع: void abort() { cancel_pending(); finish(); } تلغي ()cancel_pending المهام التي لم تنطلق بعد: void cancel_pending() { std::unique_lock<std::mutex > l(m); work.clear(); } هنا نرسل رسالة "stop the thread" إلى جميع الخيوط، ثم نتظرها، تابع: void finish() { { std::unique_lock<std::mutex > l(m); for (auto && unused: finished) { work.push_back( {}); } } v.notify_all(); finished.clear(); }~tasks() { finish(); } private: //: العمل الذي يقوم به الخيط قيد التنفيذ void thread_task() { while (true) { // سحب مهمة من الطابور std::packaged_task < void() > f; { std::unique_lock<std::mutex > l(m); if (work.empty()) { v.wait(l, [& ] { return !work.empty(); }); } f = std::move(work.front()); work.pop_front(); } // إذا كانت المهمة غير صالحة، فسيكون علينا إلغاؤها if (!f.valid()) return; // خلاف ذلك، ينبغي تنفيذ المهمة f(); } } }; تعيد الدالة التالية: tasks.queue( []{ return "hello world"s; } ) ‎‎قيمة من النوع std::future<std::string>‎، والتي ستساوي عند تنفيذ كائن المهام السلسلة النصية ‎hello world‎. كذلك يمكنك إنشاء الخيوط عن طريق تنفيذ ‎tasks.start(10)‎ (والتي تطلق 10 خيوط). إن سبب استخدام ‎packaged_task<void()>‎هو أنّه لا يوجد قالب صنف ‎std::function‎‏ مكافئ ومشطوب النوع (type-erased)، ولا يخزّن إلّا أنواع النقل فقط (move-only types). أيضًا، قد تكون كتابة نوع مخصّص أسرع من استخدام ‎packaged_task<void()>‎. انظر هذا المثال الحيّ على ذلك. الإصدار = C++‎ 11 في C++‎ 11، استبدل ‎result_of_t<blah>‎ بـ ‎typename result_of<blah>::type‎. التحقق من أنّ الخيط مضموم دائمًا عند استدعاء مدمَّر ‎std::thread‎، يجب استدعاء ‎join()‎ أو ‎detach()‎. وإذا لم يُضمّ (joined) الخيط أو يُفصل (detached)، فستُستدعى ‎std::terminate‎ افتراضيًا. يمكن تسهيل هذا عبر استخدام RAII: class thread_joiner { public: thread_joiner(std::thread t): t_(std::move(t)) {} ~thread_joiner() { if (t_.joinable()) { t_.join(); } } private: std::thread t_; } ثم يمكن كتابة ما يلي: void perform_work() { // إنجاز عمل ما } void t() { thread_joiner j { std::thread(perform_work) }; // تنفيذ بعض الحسابات أثناء تنفيذ الخيط } // يُضمّ الخيط هنا تلقائيا يوفّر هذا أيضًا أمان الاعتراضات (exception safety)؛ ذلك أنّه إذا أنشأنا الخيط بشكل طبيعي ثمّ تسبّب العمل المُنجَز في ‎t()‎ برفع اعتراض، فلن تُستدعى ‎join()‎ على خيطنا، ولن تكتمل العملية. إجراء عمليات على الخيط الحالي std::this_thread هي فضاء اسم (namespace) يحتوي بعض الدوالّ التي يمكن استخدامها لإجراء عمليات معيّنة على الخيط الحالي من الدالّة التي استُدعِي منها. الدالة الوصف get_id تعيد معرِّف الخيط. sleep_for تجعل الخيط ينام لفترة محددة. sleep_until تجعل الخيط ينام "حتى" وقت محدد. yield إعادة جدولة الخيوط العاملة وإعطاء الأولوية لخيوط أخرى. يمكنك الحصول على معُرّف الخيط الحالي باستخدام ‎std::this_thread::get_id‎، انظر: void foo() { // اطبع معرّف الخيط std::cout << std::this_thread::get_id() << '\n'; } std::thread thread { foo }; thread.join(); // 12556 طُبِع معرّف الخيط الآن، وسيكون شيئا يشبه foo(); // 2420 طُبِع معرّف الخيط الرئيسي الآن، وسيكون شيئا يشبه النوم لمدة 3 ثوانٍ باستخدام ‎std::this_thread::sleep_for‎: void foo() { std::this_thread::sleep_for(std::chrono::seconds(3)); } std::thread thread { foo }; foo.join(); std::cout << "Waited for 3 seconds!\n"; النوم إلى أن تنقضي 3 ساعات باستخدام ‎std::this_thread::sleep_until‎: void foo() { std::this_thread::sleep_until(std::chrono::system_clock::now() + std::chrono::hours(3)); } std::thread thread { foo }; thread.join(); std::cout << "We are now located 3 hours after the thread has been called\n"; منح الأولوية لخيوط أخرى باستخدام ‎std::this_thread::yield‎: void foo(int a) { for (int i = 0; i<al++i) std::this_thread::yield(); // ستأخذ خيوط أخرى الأولوية الآن، لأنّ هذا الخيط لا يفعل أي شيء مهم std::cout << "Hello World!\n"; } std::thread thread { foo, 10 }; thread.join(); استخدام المتغيرات الشرطية Using Condition Variables المتغيّر الشرطي (condition variable) هو كائن أوّلي (primitive) يُستخدم مع كائن مزامنة (mutex) لتنظيم الاتصالات بين الخيوط. ورغم أنّها ليست الطريقة الوحيدة لفعل ذلك ولا الأكثر فعالية إلا أنّها تتميّز بالبساطة والسهولة. و يمكن انتظار المتغيرات الشرطية ‎std::condition_variable‎ عبر ‎std::unique_lock<std::mutex>‎. فهذا يسمح للشيفرة بفحص الحالة المشتركة (shared state) بأمان قبل تقرير ما إذا كان يجب متابعة عملية الحصول (acquisition) على القفل أم لا. يستخدم المثال أدناه ‎std::thread‎ و ‎std::condition_variable‎ و ‎std::mutex‎. #include <condition_variable> #include <cstddef> #include <iostream> #include <mutex> #include <queue> #include <random> #include <thread> int main() { std::condition_variable cond; std::mutex mtx; std::queue<int> intq; bool stopped = false; std::thread producer { [ &]() { جهّز مولد الأعداد العشوائية، وسيدفع هذا المولد أعدادًا عشوائية إلى intq، تابع المثال: std::default_random_engine gen {}; std::uniform_int_distribution<int> dist {}; std::size_t count = 4006; while (count--) { لابد من القفل قبل تغيير الحالة التي يحميها كائن المزامنة والمتغير الشرطي condition_variable، تابع: std::lock_guard<std::mutex > L { mtx }; // وضع العدد العشوائي في الطابور intq.push(dist(gen)); cond.notify_one(); } الآن تم كل شيء، احصل على القفل وعين راية الإيقاف stopped ثم نبه المستخدم، تابع … : std::lock_guard<std::mutex > L { mtx }; std::cout << "Producer is done!" << std::endl; stopped = true; cond.notify_one(); } }; std::thread consumer { [ &]() { do { std::unique_lock<std::mutex > L { mtx }; cond.wait(L, [& ]() { // الاستحواذ على القفل في حال الانتهاء أو في حال لم يكن الطابور فارغا return stopped || !intq.empty(); }); // نحن نملك كائن المزامنة هنا // سحب العناصر من الطابور إلى أن يصبح فارغا while (!intq.empty()) { const auto val = intq.front(); intq.pop(); std::cout << "Consumer popped: " << val << std::endl; } if (stopped) { std::cout << "Consumer is done!" << std::endl; break; } } while (true); } }; consumer.join(); producer.join(); std::cout << "Example Completed!" << std::endl; return 0; } عمليات الخيوط Thread operations عندما يبدأ تنفيد خيط معيّن، فسيُنفّذ إلى أن يكتمل، لكن قد تحتاج أحيانًا إلى انتظار اكتمال تنفيذ خيط ما إن كنت تريد استخدام النتيجة التي يعيدها. على سبيل المثال: int n; std::thread thread { calculateSomething, std::ref(n) }; // افعل أشياء أخرى … نحن نحتاج n الآن، انتظر الخيط إلى أن ينتهي، إن لم يكن قد انتهى فعلًا، وستكون قيمة n بعدها هي النتيجة المحسوبة في خيط آخر، تابع: thread.join(); std::cout << n << '\n'; يمكنك أيضًا فصل (‎detach‎) الخيط، والسماح بأن يُنفّذ بحرّية: std::thread thread { doSomething }; //فصل الخيط، فنحن لا نريده بعد الآن thread.detach(); // سيتم إنهاء الخيط عند اكتمال تنفيذه، أو عند عودة الخيط الرئيسي التخزين المحلي للخيوط يمكن إنشاء خيوط مُخزّنة محليًا باستخدام الكلمة المفتاحية ‎thread_local‎، والمتغيّرات التي يُصرّح عنها بالمحدّد ‎thread_local‎ يُقال أنّ لها مدة تخزين خيطية (thread storage duration). كل خيط في البرنامج له نسخته الخاصّة من كل متغيّر محلي في الخيط (thread-local variable). سيُهيّأ متغيّر الخيط المحلي الموجود في نطاق دالة (محلّية) بمجرّد تمرير التحكّم إلى تعريفها. هذا المتغيّر سيكون ساكنًا ضمنيًا ما لم يُصرّح عنه عبر ‎extern‎. ستُهيّأ متغيّرات الخيط المحلي الموجودة في نطاق فضاء اسم أو نطاق صنف -غير محلي- عند بدء تنفيذ الخيط. تُدمّر متغيّرات الخيط المحلي عند اكتمال تنفيذ الخيط. لا يمكن لأعضاء صنف معيّن أن تكون محلية في الخيط (thread-local) إلا إن كانت ساكنة، وعندها ستكون هناك نسخة واحدة من ذلك المتغيّر لكل خيط، بدلاً من نسخة واحدة لكل زوج (نُسخة، خيط) [(thread, instance)]. انظر: void debug_counter() { thread_local int count = 0; Logger::log("This function has been called %d times by this thread", ++count); } إعادة إسناد كائنات الخيوط يمكننا إنشاء كائنات خيوط فارغة (empty thread objects)، وإسناد مهامّ معيّنة إليها لاحقًا. وإذا أسندت كائن خيط إلى خيط آخر نشط وقابل للضمّ ‎joinable‎، فستُستدعى ‎std::terminate‎ تلقائيًا قبل استبدال الخيط. انظر: #include <thread> void foo() { std::this_thread::sleep_for(std::chrono::seconds(3)); } // إنشاء 100 كائن خيطي فارغ std::thread executors[100]; // شيفرة هنا // إنشاء بعض الخيوط for (int i = 0; i < 100; i++) { // إذا لم يُسنَد خيط إلى هذا الكائن if (!executors[i].joinable()) executors[i] = std::thread(foo); } الآجال والوعود (Futures and Promises) تُستخدم الآجال (Futures) والوعود لنقل كائن من خيط إلى آخر. يُضبط كائن الوعود ‎std::promise‎ من قِبل الخيط الذي يولّد النتيجة. يُستخدم كائن ‎std::future‎ لاسترداد قيمة، أو التحقّق من إتاحة قيمة ما، أو لإيقاف التنفيذ إلى حين إتاحة القيمة. أصناف العمليات غير المتزامنة std::async : تنفذ عملية غير متزامنة. std::future : توفّر وصولًا إلى نتيجة عملية غير متزامنة. std::promise : تحزِم نتيجة العملية غير المتزامنة. std::packaged_task : تربط دالّة مع الوعد المرتبط بها في نوع القيمة المُعادة. ينشئ المثال التالي وعدًا لكي يستخدمه خيط آخر: { auto promise = std::promise<std::string > (); auto producer = std::thread([ &] { promise.set_value("Hello World"); }); auto future = promise.get_future(); auto consumer = std::thread([ &] { std::cout << future.get(); }); producer.join(); consumer.join(); } مثال مؤجل غير متزامن تقدّم الشيفرة التالية نسخة من ‎std::async‎، إلّا أنّها تتصرف كما لو كانت ‎async‎ تُستدعى عبر سياسة الإطلاق المؤجّلة ‎deferred‎. ليس لهذه الدالّة أيضًا سلوك الأجل (‎future‎) الخاص بـ ‎async‎، ويمكن تدمير القيمة المستقبلية (‎future‎) المُعادة قبل الحصول على قيمتها. template < typename F> auto async_deferred(F&& func)->std::future < decltype(func()) > { using result_type = decltype(func()); auto promise = std::promise<result_type> (); auto future = promise.get_future(); std::thread(std::bind([=](std::promise<result_type>& promise) { try { promise.set_value(func()); لاحظ أن هذا لن يعمل مع <std::promise <void إذ يحتاج برمجة القوالب الوصفية (meta-template programming)، تابع المثال … which is out of scope for this example. } catch (...) { promise.set_exception(std::current_exception()); } }, std::move(promise))).detach(); return future; } std::packaged_task و std::future تحزم std::packaged_task دالّة مع الوعد المرتبط بها في نوع القيمة المُعادة: template < typename F> auto async_deferred(F&& func)->std::future < decltype(func()) > { auto task = std::packaged_task < decltype(func())() > (std::forward<F> (func)); auto future = task.get_future(); std::thread(std::move(task)).detach(); return std::move(future); } يبدأ الخيط في العمل فورًا، ونستطيع فصله أو ضمّه في نهاية النطاق، وتكون النتيجة جاهزة عند انتهاء استدعاء الدالّة لـ std::thread finishes. لاحظ أنّ هذا يختلف قليلاً عن ‎std::async‎، إذ أنّه عند تدمير الأجل ‎std::future‎ المُعاد، فسيُعطّل (block) إلى أن ينتهي الخيط. std::future_error و std::future_errc إذا لم تُستوفى قيود الوعود (std::promise) والآجال (std::future)، فسيُطرَح استثناء من النوع std::future_error. وسيكون رمز الخطأ في الاستثناء من النوع std::future_errc، وستكون القيم على النحو التالي: enum class future_errc { broken_promise = /* لم تعُد المهمّة مُشتركة */, future_already_retrieved = /* تم استرداد القيمة المُعادة سلفا */, promise_already_satisfied = /* الإجابة خُزِّنت سلفا */, no_state = /* محاولة الدخول إلى وعد في حالة غير مشتركة */ }; انظر الأمثلة التوضيحية التالية: الوعود غير النشطة (Inactive promise) int test() { std::promise<int> pr; return 0; // ok تعيد } الوعود النشطة غير المستخدمة: int test() { std::promise<int> pr; auto fut = pr.get_future(); // تعطيل إلى أجل غير مسمى. return 0; } الاسترجاع المزدوج Double retrieval int test() { std::promise<int> pr; auto fut1 = pr.get_future(); try { auto fut2 = pr.get_future(); // future محاولة ثانية للحصول على return 0; } catch (const std::future_error& e) { cout << e.what() << endl; // Error: "The future has already been retrieved from the promise or packaged_task." return -1; } return fut2.get(); } تعيين قيمة الوعد مرتين: int test() { std::promise<int> pr; auto fut = pr.get_future(); try { std::promise<int> pr2(std::move(pr)); pr2.set_value(10); pr2.set_value(10); // محاولة ثانية لتعيين قيمة الوعد، وهذا سيؤدي إلى رفع اعتراض. } catch (const std::future_error& e) { cout << e.what() << endl; // Error: "The state of the promise has already been // set." return -1; } return fut.get(); } std::future و std::async تُستخدَم ‎std::async‎ في مثال الترتيب المتوازي التالي لإطلاق عدّة مهام merge_sort متوازية، وتُستخدَم std::future لانتظار النتائج ومزامنتها: #include <iostream> using namespace std; void merge(int low, int mid, int high, vector<int> &num) { vector<int> copy(num.size()); int h, i, j, k; h = low; i = low; j = mid + 1; while ((h <= mid) && (j <= high)) { if (num[h] <= num[j]) { copy[i] = num[h]; h++; } else { copy[i] = num[j]; j++; } i++; } if (h > mid) { for (k = j; k <= high; k++) { copy[i] = num[k]; i++; } } else { for (k = h; k <= mid; k++) { copy[i] = num[k]; i++; } } for (k = low; k <= high; k++) swap(num[k], copy[k]); } void merge_sort(int low, int high, vector<int> &num) { int mid; if (low < high) { mid = low + (high - low) / 2; auto future1 = std::async (std::launch::deferred, [& ]() { merge_sort(low, mid, num); }); auto future2 = std::async (std::launch::deferred, [& ]() { merge_sort(mid + 1, high, num); }); future1.get(); future2.get(); merge(low, mid, high, num); } } ملاحظة: في المثال أعلاه، تُطلَق ‎std::async‎ وفق سياسة ‎std::launch_deferred‎، وذلك لتجنّب إنشاء خيط جديد في كل استدعاء، فتُجرى في مثالنا السابق استدعاءات ‎std::async‎ دون ترتيب، إذ أنّها تُزامن في استدعاءات ‎std::future::get()‎. بالمقابل، تفرض std::launch_async إنشاء خَيط جديد في كل استدعاء. السياسة الافتراضية هي ‎std::launch::deferred| std::launch::async‎، ممّا يعني أن التقديم سيكون هو المسؤول عن تحديد سياسة إنشاء الخيوط الجديدة. التزامن عبر OpenMP يغطي هذا الموضوع أساسيات التزامن في C++‎ باستخدام OpenMP، والتوازي أو التزامن يعني تنفيذ الشيفرة في نفس الوقت. OpenMP: الأقسام المتوازية يوضّح هذا المثال أساسيات تنفيذ أقسام (sections) من الشيفرة بالتوازي. ستعمل ميزة OpenMP على كل المصرِّفات المدعومة دون الحاجة إلى تضمين مكتبات بما أنها مبنية مسبقًا في المصرفات، لكن قد ترغب في تضمين ‎omp.h‎ إذا كنت تريد استخدام ميزات الواجهة البرمجية لـ openMP. انظر المثال التوضيحي التالي: ستلمِّح تعليمة pragna للمصرف أن المحتوى الموجود داخل { } سينفَّذ في أقسام متوازية باستخدام OpenMP، وسيولِّد المصرِّف هذه الشيفرة من أجل تنفيذها بالتوازي ... std::cout << "begin "; #pragma omp parallel sections { وهنا تلمِّح تعليمة pragma للمصرف أن هذا القسم يمكن تنفيذه بالتوازي مع بقية الأقسام، وسينفَّذ كل قسم في خيط منفصل. سيولِّد المصرِّف هذه الشيفرة لتنفيذها بالتوازي. لاحظ الفرق بين كلمة "قسم" هنا، و"أقسام" في الجزء السابق أعلاه من المثال، نتابع ... #pragma omp section { std::cout << "hello " << std::endl; /** افعل شيئا ما **/ } #pragma omp section { std::cout << "world " << std::endl; /** افعل شيئا ما **/ } } // لن يُنفّذ هذا السطر حتى تنتهي كل الأقسام أعلاه std::cout << "end" << std::endl; الخرج: هناك نتيجتان محتمَلتان يمكن أن تنتُجا عن هذا المثال، وذلك اعتمادًا على نظام التشغيل والأجهزة المستخدمة. يوضّح الخرج أيضًا مشكلة حالة التسابق (race condition problem) التي قد تحدث نتيجة لمثل هذا التقديم. الخرج أ الخرج ب begin hello world end begin world hello end OpenMP: الأقسام المتوازية يوضّح هذا المثال كيفية تنفيذ أجزاء من الشيفرة بالتوازي: std::cout << "begin "; // بداية الأقسام المتوازية #pragma omp parallel sections { // تنفيذ الأقسام بالتوازي #pragma omp section { ... do something ... std::cout << "hello "; } #pragma omp section { ... افعل شيئا ما ... std::cout << "world "; } #pragma omp section { ... افعل شيئا ما ... std::cout << "forever "; } } // نهاية الأقسام std::cout << "end"; نظرًا لأنّ ترتيب التنفيذ غير مضمون، فقد ترى أيًّا من المخرجات التالية: begin hello world forever end begin world hello forever end begin hello forever world end begin forever hello world end OpenMP: التوازي في الحلقات يوضّح هذا المثال كيفية تقسيم حلقة إلى أجزاء متساوية وتنفيذها بشكل متوازي، سنقسم متجه العنصر إلى ()element.size و Thread Qty ثم نخصص نطاق كل خيط على حدة، انظر ... #pragma omp parallel for for (size_t i = 0; i < element.size(); ++i) element[i] = ... مثال على تخصيص 100 عنصر لكل خيط: خيط 1: 0~99. خيط 1: 100~199. خيط 1: 200~299. وهكذا .. تستمر العملية كلها عند إكمال الخيوط كلها للمهمة التي أوكلت إليها من الحلقة. يرجى توخي الحذر الشديد وعدم تعديل حجم المتجه المستخدم في حلقة for المتوازية لأنّ فهارس النطاق المُخصّص لا تُحدَّث تلقائيًا. OpenMP: التجميع / الاختزال المتوازي يوضّح هذا المثال مفهوم الاختزال (reduction) أو التجميع (gathering) باستخدام المتجهات (‎std::vector‎) و OpenMP. سنفترض أنّنا نحتاج عدّة خيوط لمساعدتنا على توليد مجموعة كبيرة من الأشياء، وقد استخدمنا ‎int‎ هنا للتبسيط، بيْد أنّه يمكن استبدالها بأنواع بيانات أخرى. سيكون هذا مفيدًا عندما تحتاج إلى دمج نتائج من المهام الفرعية (slaves) لتجنّب أخطاء الأجزاء (segement faults)، أو انتهاكات الوصول إلى الذاكرة، وفي نفس الوقت لا ترغب في استخدام المكتبات، أو مكتبات حاويات مخصّصة للمزامنة. // المتجه الرئيسي // نريد متجهًا يضم النتائج المجموعة من المهام الفرعية std::vector < int > Master; // تلميح للمصرّف لكي ينفّذ الكتلة { } بالتوازي في كل الخيوط المتاحة #pragma omp parallel { // في هذه المنطقة، يمكنك كتابة أي شيفرة تريد لكل خيط فرعي // في هذه الحالة، هي متجهة لتخزين كل نتائجها // ليس علينا أن نقلق من عدد الخيوط المُنشأة أو مما إذا كنا نريد تكرار هذا التصريح هنا std::vector < int > Slave; // أخبر المصرف هنا أن يستخدم هنا كل الخيوط المخصصة لهذا الحيز المتوازي لتنفيذ هذه الحلقة على مراحل // 1000000 / Thread Qty الفعلية هي الحِمل الفعلي هي // المصرّفَ ألا ينتظر انتهاء جميع المهام الفرعية nowait وتخبر الكلمة المفتاحية #pragma omp for nowait for (size_t i = 0; i < 1000000; ++i { /* افعل شيئا ما */ .... Slave.push_back(...); } // ستنفّذ المهام الفرعية التي تنهي هذا الجزء من العمل الخيوطَ واحدًا تلو الآخر // تضمن الأقسام الحرجة أنّ خيطا واحدا على الأكثر سينفِّذ الكتلة { } في كل مرة #pragma omp critical { // دمج المهمة الفرعية مع الرئيسية. // استخدم مكرِّرات النقل بدلًا من ذلك، وتجنب النسخ إلا إن كنت تريد استخدامه بعد هذا القسم. Master.insert(Master.end(), std::make_move_iterator(Slave.begin()), std::make_move_iterator(Slave.end())); } } هذا الدرس جزء من سلسلة دروس عن C++‎. ترجمة -بتصرّف- للفصول: Chapter 80: Threading Chapter 85: Mutexes Chapter 86: Recursive Mutex Chapter 87: Semaphore Chapter 88: Futures and Promises Chapter 131: Concurrency With OpenMP من كتاب C++ Notes for Professionals
  17. صار بالإمكان قولبة الأصناف والدوالّ والمتغيّرات في لغة ++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
  18. معالج 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
  19. تُسنَد فئات القيمة إلى تعبيرات 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
  20. ما هي تعابير لامدا؟ توفّر دوال لامدا طريقة موجزة لإنشاء كائنات دوالّ بسيطة، وتعبير لامدا هو قيمة يمينية خالصة (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
  21. حجم الأنواع العددية الصحيحة الأنواع التالية هي أنواع عددية صحيحة: ‎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
  22. التعبيرات النمطية (تُسمّى أحيانًا 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
  23. ‎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
  24. نماذج الذاكرة إن حاوَلَت عدّة خيوط الوصول إلى نفس الموضع من الذاكرة، فستدخل في تسابق على البيانات (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
  25. القيم الثنائية مصنفة النوع المُعرّفة من المستخدم (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
×
×
  • أضف...