محمد الميداوي

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

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

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

  • Days Won

    6

السُّمعة بالموقع

44 Excellent

10 متابعين

  1. النطاقات (Scopes) المتغيرات العامة (Global variables) إذا أردت التصريح عن نسخة واحدة من متغير ما، وكانت هذه النسخة متاحة للوصول في عدة ملفات مصدرية (source files)، فمن الممكن أن نجعلها في النطاق العام (global scope) باستخدام الكلمة المفتاحية extern، فهي تخبر المصرِّف بوجود تعريف لهذا المتغير في مكان ما داخل الشيفرة الحالية، لذا فمن الممكن استخدامه في أي مكان، كما ستُجرى جميع عمليات القراءة / الكتابة في نفس المكان من الذاكرة. يتألّف المثال التالي من عدّة ملفات مصدرية: الملف my_globals.h: #ifndef __MY_GLOBALS_H__ #define __MY_GLOBALS_H__ extern int circle_radius; // سيُعرَّف في مكان ما circle_radius تخطر المصرّف بأنّ #endif الملف foo1.cpp: #include "my_globals.h" int circle_radius = 123; // extern تعريف المتغير الخارجي الملف main.cpp: #include "my_globals.h" #include <iostream> int main() { std::cout << "The radius is: " << circle_radius << "\n"; ' return 0; } سينتج الخرج التالي: The radius is: 123 المتغيرات المُضمّنة (Inline variables) يُسمح بتعريف مُتغير مُضمّن (Inline variable) في عدّة وحدات ترجمة دون انتهاك قاعدة التعريف الواحد. وفي حال تعريف متغيّر مُضمّن أكثر من مرة، فسيَدمِج الرابط (linker) كلّ تَعاريفه في كائن واحد في البرنامج النهائي. يمكن تعريف حقل ساكن (static data member) في صنف إن صُرِّح عنه بأنّه مُضمّن (‎inline‎). على سبيل المثال، يمكن تعريف الصنف في المثال التالي في الترويسة. ملاحظة: قبل C++‎ 17، كان من الضروري توفير ملفّ ‎.cpp‎ لاحتواء تعريف Foo::num_instances كي نضمن ألا يُعرّف أكثر من مرّة واحدة، لكن في C++‎ 17، أصبحت مُختلف تعريفات المتغيّر المضمن ‎Foo::num_instances‎ تشير إلى نفس الكائن ‎int‎. // not thread-safe قد لا يكون متوافقا مع المسالك class Foo { public: Foo() { ++num_instances; }~Foo() { --num_instances; } inline static int num_instances = 0; }; كحالّة خاصة، الحقول الساكنة للتعابير الثابتة (‎constexpr‎) مضُمّنة بشكل غير صريح (implicitly). class MyString { public: MyString() { /* ... */ } // ... static constexpr int max_size = INT_MAX / 2; }; صار التعريف constexpr int MyString::max_sizeضروريًا في كل وحدات الترجمة في إصدار C++ 1. نطاق كتلة بسيط يبدأ نطاق متغيّر ما في كتلة ‎{ ... }‎ بعد التصريح وينتهي عند نهاية الكتلة، فإذا كانت هناك كتلة متشعّبة (nested block)، فتستطيع الكتلة الداخلية إخفاء نطاق المتغيّر الذي صُرِّح عنه في الكتلة الخارجية. { int x = 100; // ^ // يبدأ هنا `x` نطاق // } // <- ينتهي هنا `x` نطاق في حال التصريح عن متغير جديد داخل الكتلة المُتشعِّبة، وكان ذلك المتغير يحمل اسم متغير موجود في الكتلة الخارجية، فإنه يخفي المتغير الأول الذي في الكتلة الخارجية. { int x = 100; { int x = 200; std::cout << x; // → 200 } std::cout << x; // → 100 } قاعدة التعريف الواحد (ODR) انتهاك قاعدة ODR عبر تحليل التحميل الزائد يمكن انتهاك ODR إذا لم يشِر البحث عن الأسماء إلى نفس الكيان حتى في حال استخدام مقاطع (tokens) متماثلة للتعبير عن دالة مضمّنة معيّنة. لاحظ الدالة ‎func‎ في المثال التالي: header.h void overloaded(int); inline void func() { overloaded('*'); } foo.cpp ، تشير overloaded إلى (void overloaded(char: #include "header.h" void foo() { func(); } bar.cpp، تشير overloaded إلى (void overloaded(char: void overloaded(char); // أخرى “include” قد تأتي من عبارات تضمين #include "header.h" void bar() { func(); } لدينا انتهاك هنا لقاعدة ODR، إذ يشير المقطع ‎overloaded‎ إلى عدّة كيانات مختلفة في نفس وحدة الترجمة. مضاعفة الدوال المعرفة (Multiply defined function) إنّ أهم نتيجة لقاعدة التعريف الواحد هي أنّ الدوال غير المضمّنة ذات الارتباط الخارجي (non-inline functions with external linkage) يجب ألا تُعرَّف إلا مرّة واحدة في البرنامج، لكن يمكن التصريح عنها عدّة مرات. لذا لا ينبغي تعريف مثل هذه الدوال في الترويسات، إذ يمكن إدراج الترويسة عدّة مرات من قبل عدة وحدات ترجمة مختلفة. foo.h‎: #ifndef FOO_H #define FOO_H #include <iostream> void foo() { std::cout << "foo"; } void bar(); #endif foo.cpp: #include "foo.h" void bar() { std::cout << "bar"; } main.cpp‎: #include "foo.h" int main() { foo(); bar(); } في هذا البرنامج، عُرِّفت الدالّة الأبسط ‎foo‎ في الترويسة ‎foo.h‎، والتي ضُمِّنت مرّتين: مرّة من قبل ‎foo.cpp‎ ومرّة أخرى من قبل ‎main.cpp‎، ولذلك تحتوي كل وحدة ترجمة على تعريف ‎foo‎ الخاص بها. لاحظ أنّ دروع التضمين (include guards) في ‎foo.h‎ لا تمنع حدوث ذلك، إذ أنّ الملفين ‎foo.cpp‎ و ‎main.cpp‎ يشتملان على ‎foo.h‎ بشكل منفصل، وعلى الأرجح سينتج عن محاولة إنشاء هذا البرنامج خطأٌ في وقت الربط (link-time error) بسبب تعريف ‎foo‎ أكثر من مرّة. ويجب التصريح عن الدوال في الترويسات وتعريفها في ملفات ‎.cpp‎ المقابلة لتجنّب مثل هذه الأخطاء، مع بعض الاستثناءات. الدوال المُضمّنة (Inline functions) يمكن تعريف دالّة مُضمّنة ‎inline‎ في عدّة وحدات ترجمة بشرط أن تكون جميع تَعاريفها متطابقة، ويجب أن تُعرّف أيضًا في كل وحدة ترجمة تُستخدم فيها، لذا يجب تعريف الدوال المضمّنة في الترويسات، ولا توجد حاجة إلى ذكرها في ملف التنفيذ (implementation file). سيتصرّف البرنامج كما لو كان هناك تعريف واحد للدالّة. foo.h‎: #ifndef FOO_H #define FOO_H #include <iostream> inline void foo() { std::cout << "foo"; } void bar(); #endif foo.cpp: #include "foo.h" void bar() { // تعريفات أخرى أكثر تعقيدا } main.cpp‎: #include "foo.h" int main() { foo(); bar(); } في هذا المثال، عُرِّفت الدالّة ‎foo‎ بشكل مُضمّن (inline) في الترويسة، أمّا الدالّة الأكثر تعقيدًا ‎bar‎ فهي غير مُضمنة، وقد عُرِّفت في ملف التقديم. وتحتوي كل من وحدتي الترجمة foo.cpp‎ و ‎main.cpp على تعريفات لـ ‎foo‎، وهذا لن يسبّب مشكلة لأنّ ‎foo‎ مضمّنة. تكون الدوالّ المُعرَّفة داخل تعريف صنف ما -والتي قد تكون دوالًا تابعة أو صديقة- مضمنة بشكل غير صريح، لذا يمكن تعريف الدوال التابعة للفصل داخل تعريف الصنف، رغم أنّه يمكن إدراج التعاريف في وحدات ترجمة متعددة: // in foo.h class Foo { void bar() { std::cout << "bar"; } void baz(); }; // in foo.cpp void Foo::baz() { // التعريف } عُرِّفت الدالّة ‎Foo::baz‎ خارج السطر، وعليه تكون غير مُضمّنة، ويجب ألا تُعُرِّف في الترويسة. البحث باسم الوسيط (Argument Dependent Name Lookup) عند البحث عن الدوال، تُجمع "الأصناف المرتبطة" (associated classes) و "فضاءات الأسماء المرتبطة" (associated namespaces) التي تحقق أحد الشروط التالية، وفقًا لنوع الوسيط ‎T‎. عند البحث عن الدوال فإننا نجدها عبر جمع "الأصناف المرتبطة" (associated classes) أولًا وكذلك "فضاءات الأسماء المرتبطة" (associated namespaces)، التي تحقق شرطًا أو أكثر من الشروط أدناه وفقًا لنوع الوسيط ‎T‎، لكن سنعرض قبل هذا القواعد الخاصة بأسماء الأصناف والتعداد وقوالب أسماء الأصناف: إذا كان ‎T‎ صنفًا مُتشعِّبا (nested class)، أو عضوَ تعدادٍ (member enumeration)، فسيُضاف الصنف المحيطة به إلى مجموعة البحث. إذا كان ‎T‎ تعدادًا -قد يكون أيضًا عضوًا من صنف-، يُضاف أعمق فضاء اسم - innermost namespace - داخله إلى مجموعة البحث. إذا كان ‎T‎ صنفًا -قد يكون مُتشعِّبًا أيضًا-، تُضاف جميع أصنافه الأساسية، وكذلك الصنف نفسه، وأعمق فضاءات الاسم في جميع الأصناف المرتبطة. إذا كان ‎T‎ من النوع ‎ClassTemplate<TemplateArguments>‎ -وهو صنف أيضًا-، تُضاف الأصناف وفضاءات الاسم المرتبطة بوسائط نوع القالب، وفضاء الاسم الخاص بوسائط قالب القالب (template template argument)، والصنف المحيط بوسائط قالب القالب، إذا كان وسيط القالبِ (template argument) قالبَ عضوٍ (member template). هناك بعض القواعد التي تخصّ الأنواع المضمّنة أيضًا: إذا كان ‎T‎ مؤشّرًا يشير إلى نوع ‎U‎، أو مصفوفةً مؤلّفة من عناصر من النوع ‎U‎، تُضاف أصناف وفضاءات الأسماء المرتبطة بـ ‎U‎ إلى مجموعة البحث. في المثال: void (*fptr)(A);f(fptr);‎، تُضاف فضاءات الأسماء والأصناف المرتبطة بـ ‎void(A)‎ (انظر القاعدة التالية). إذا كان ‎T‎ نوع دالّة، تُضاف الأصناف وفضاءات الأسماء المرتبطة بأنواع المُعاملات والقيمة المعادة. في المثال: void(A)‎)، تُضاف فضاءات الأسماء والأصناف المرتبطة بـ A. إذا كان ‎T‎ مؤشّرًا يشير إلى عضو، تُضاف الأصناف وفضاءات الأسماء المرتبطة بنوع ذلك العضو (قد تنطبق على كل من مؤشّرات التوابع ومؤشّرات الحقول). في مثال ‎‎B A::*p; void (A::*pf)(B); f(p);f(pf);‎؛ تُضاف فضاءات الأسماء والأصناف المرتبطة بـ A‎ و B‎ و void(B)‎، والتي تطبّق القواعد أعلاه التي تخصّ أنواع الدوال. يُعثَر على جميع الدوال والقوالب داخل جميع فضاءات الأسماء المرتبطة من خلال البحث باسم الوسيط، أو البحث المعتمد على الوسيط (argument dependent lookup). ويُعثَر على الدوال الصديقة (friend functions) لنطاق الأسماء المُصرَّح عنها في الأصناف المرتبطة، والتي عادة ما تكون غير مرئية. بالمقابل، تُتجَاهل الموجّهات (directives). وتكون جميع الاستدعاءات في المثال التالي صالحة حتى دون الحاجة إلى تأهيل ‎f‎ بفضاء الاسم عند الاستدعاء. namespace A { struct Z {}; namespace I { void g(Z); } using namespace I; struct X { struct Y {}; friend void f(Y) {} }; void f(X p) {} void f(std::shared_ptr < X > p) {} } // أمثلة على الاستدعاءات f(A::X()); f(A::X::Y()); f(std::make_shared < A::X > ()); g(A::Z()); // "using namespace I;"غير صالح، يُتجاهل حزم المعاملات (Parameter packs) قالبٌ بحزمة مُعاملات (template with a parameter pack) template < class...Types > struct Tuple {}; حزمة المُعاملات (parameter pack) هي مُعاملُ قالبٍ (template parameter) يقبل وسيط قالب أو أكثر، أو لا يقبل على الإطلاق، ويكون القالب متغايرًا (variadic) إذا كان يحتوي على حزمة مُعاملات واحدة على الأقل. توسيع حزمة مُعاملات يُوسَّع نمط ‎parameter_pack ...‎ إلى قائمة من البدائل لحزمة ‎parameter_pack‎ مفصولة بالفاصلة الأجنبية ,، وذلك مع كل معامل من معاملاتها: template<class T> // أساس التكرار void variadic_printer(T last_argument) { std::cout << last_argument; } template<class T, class ...Args> void variadic_printer(T first_argument, Args... other_arguments) { std::cout << first_argument << "\n"; variadic_printer(other_arguments...); // توسيع حزمة المعاملات } عند استدعاء الشيفرة أعلاه مع ‎variadic_printer(1, 2, 3, "hello");‎، ستطبع: 1 2 3 hello فواصل الأرقام (Digit separators) من الصعب قراءة القيم العددية المؤلّفة من من أرقام كثيرة، فمثلًا: حاول نُطق 7237498123 قارن بين 237498123 وبين 237499123 من حيث التساوي. حدّد العدد الأكبر من بين 237499123 و 20249472. وقد عرَّف الإصدار ‎C‎++ 14 علامةَ الاقتباس ‎'‎ كفاصلة للأرقام، وتُستخدَم داخل الأعداد والقيم مصنَّفة النوع المُعرَّفة من قبل المستخدم (user-defined literals) لتسهيل قراءة الأعداد الكبيرة. انظر: الإصدار ≥ C++‎ 14 long long decn = 1'000'000'000ll; long long hexn = 0xFFFF'FFFFll; long long octn = 00'23'00ll; long long binn = 0b1010'0011ll; تُتجَاهل علامة الاقتباس المُفردة عند تحديد القيمة. مثلًا: القيم مصنَّفة النوع التالية: ‎1048576‎ و ‎1'048'576‎ و ‎0X100000‎ و ‎0x10'0000‎ و ‎0'004'000'000‎ كلها لها نفس القيمة. القيم مصنفة النوع التالية ‎1.602'176'565e-19‎ و ‎1.602176565e-19‎ لهما نفس القيمة. ولا توجد أهمية تُذكر لموضع علامات الاقتباس المُفردة، فمثلًا، كل التعابير التالية متكافئة: الإصدار ≥ C++‎ 14 long long a1 = 123456789ll; long long a2 = 123'456'789ll; long long a3 = 12'34'56'78'9ll; long long a4 = 12345'6789ll; كذلك يُسمح باستخدام الفاصلة في القيم مصنَّفة النوع والمُعرّفة من المستخدم: الإصدار ≥ C++‎ 14 std::chrono::seconds tiempo = 1'674'456s + 5'300h; موازنة مع إصدارات C++‎ المختلفة اعلم أنه يمكن تنفيذ تكرار حلقي على حاوية التسلسل c في لغة ++C باستخدام الفهارس على النحو التالي: for (size_t i = 0; i < c.size(); ++i) c[i] = 0; هذه الصيغة -رغم بساطتها- عرضة لكثير من الأخطاء الشائعة، مثل استخدام عامل مقارنة خاطئ، أو متغيّر فهرسة غير صحيح: for(size_t i = 0; i <= c.size(); ++j) c[i] = 0; ^~~~~~~~~~~~~~^ كذلك يمكن التكرار على الحاويات باستخدام المُكرّرات (iterators)، مع عيوب مماثلة: for(iterator it = c.begin(); it != c.end(); ++it) (*it) = 0; وقد قدّمت C++‎ 11 حلقة for النطاقية، إضافة إلى الكلمة المفتاحية ‎auto‎، واللّتان سمحتا بكتابة شيفرات مثل: for(auto& x : c) x = 0; وهنا، يكون المُعاملان الوحيدان هما الحاوية ‎c‎، ومتغيّر ‎x‎ لتخزين القيمة الحالية. هذا يمنع الأخطاء المشار إليها سابقا. ووفقًا لمعيار C++‎ 11، فإنّ التنفيذ الأساسي (underlying implementation) يكافئ: for(auto begin = c.begin(), end = c.end(); begin != end; ++begin) { // ... } في مثل هذا التنفيذ فإن التعبير ‎auto begin = c.begin(), end = c.end();‎ يجبر ‎begin‎ و ‎end‎ على أن يكونا من نفس النوع، بينما لا تُزاد قيمة ‎end‎ ولا تحصَّل (dereferenced)، ومن ثم فإنّ حلقة for النطاقية لا تعمل إلا على الحاويات المُعرَّفة بواسطة زوج مُكرّر/ مُكرّر (iterator/iterator). وقد خفّف معيارُ C++‎ 17 هذا القيد عن طريق تعديل التنفيذ: auto begin = c.begin(); auto end = c.end(); for(; begin != end; ++begin) { // ... } هنا يُسمح بأن تكون ‎begin‎ و ‎end‎ من نوعين مختلفين طالما أنه يمكن مقارنتهما للتحقّق من أنّهما غير متساويين. هذا يسمح بالتكرار على العديد من الحاويات مثل الحاويات المعرّفة عبر زوج مُكرّر/حارس (iterator/sentinel). هذا الدرس جزء من سلسلة دروس عن C++‎. ترجمة -بتصرّف- للفصول Chapter 64: Inline variables Chapter 117: Scopes Chapter 120: One Definition Rule (ODR)‎ Chapter 122: Argument Dependent Name Lookup Chapter 129: Parameter packs Chapter 135: Digit separators Chapter 137: Side by Side Comparisons of classic C++ examples solved via C++ vs C++11 vs C++14 vs C++1 من كتاب C++ Notes for Professionals
  2. تُعرف اللغتان C و C++‎ بانّ أداءهما عال جدًّا - ويُعزى ذلك في الغالب إلى إمكانية التخصيص المكثّف للشيفرة، إذ يُسمح للمستخدم بتحسين الأداء عبر اختيار بنية الشيفرة وكيفية تنفيذها. وإن أردت تحسين الشيفرة فمن المهم أن تفهمها وتعرف كيفية استخدامها. وتشمل بعض أخطاء التحسين الشائعة ما يلي: التحسين السابق لأوانه: قد يسوء أداء الشيفرات المعقّدة بعد التحسين ممّا يضيع الوقت ويقلل الكفاءة. يجب أن تكون الأولوية هي كتابة شيفرة صحيحة وقابلة للصيانة، وليس شيفرة مُحسَّنة. التحسين في الحالات غير المناسبة: إضافة تحميل زائد لأجل حالة احتمال حدوثها 1% لا يستحقّ أن تبطئ لأجله 99% من الحالات الأخرى. التحسين المصغّر (Micro-optimization): تقوم المُصرِّفات بهذا الأمر بفعالية كبيرة، كما أن التحسين المصغَّر يمكن أن يضرّ بقدرة المُصرِّفات على زيادة تحسين الشيفرة. وبعض أهداف التحسين النموذجية ما يلي: تقليل كمية العمل المطلوبة. استخدام خوارزميات / بنيات أكثر فاعلية. استغلال العتاد بشكل أفضل. لكن قد تكون لعمليات التحسين تأثيرات جانبية سلبية، مثل: الاستخدام المكثّف للذاكرة. تعقيد الشيفرة وصعوبة قراءتها أو صيانتها. تعقيد الواجهات البرمجية (API) وتصميمات الشيفرة. على أي حال، غالبًا ما يُعدِّل المُصرِّفُ البرنامجَ عند التصريف لتحسين أدائه، وهذا مسموح به وفقًا لقاعدة "كما لو" (as-if rule). والتي تسمح بإجراء التغييرات والتحويلات التي لا تغيّر السلوك الملاحظ للبرنامج. تحسين الصنف الأساسي الفارغ Empty Base Class Optimization لا يمكن أن يشغل كائن ما أقل من بايت واحد، وإلّا لكانت أعضاء المصفوفات التي تضم عناصر من نفس النوع تحتل العنوان نفسه. لهذا، تكون ‎sizeof(T)>=1‎ صحيحة دائما. وكذلك لا يمكن أن يكون الصنف المشتق أصغر من صنفه الأساسي (base class). لكن إن كان الصنف الأساسي فارغًا، فلن يُضاف حجمه بالضرورة إلى الصنف المشتق: class Base {}; class Derived: public Base { public: int i; }; وليس من الضروري هنا تخصيص بايت للصنف ‎Base‎ داخل ‎Derived‎ حتى يحتلّ كل كائن -بغض النظر عن نوعه- عنوانًا مميّزًا. وفي حال إجراء تحسين للصنف الأساسي الفارغ -ولم تكن هناك حاجة إلى الحشو (padding)-، فستكون العبارة ‎sizeof(Derived) ==sizeof(int)‎ صحيحة، أي أنّه لن يُجرى أيّ تخصيص إضافي للصنف الفارغ. هذا ممكن حتّى مع الأصناف الأساسية (base classes) المتعددة، إذ لا يمكن لعدّة أًصناف أساسية في ++C أن يكون لها نفس النوع، وعليه فلن تنشأ مشكلة من ذلك. لاحظ أنّه لا يمكن إجراء ذلك إلا إذا كان نوع العضو الأول من ‎Derived‎ مختلفًا عن كلّ الأصناف الأساسية، وهذا يشمل كل الأصناف الأساسية المشتركة بشكل مباشر أو غير مباشر. وإذا كان له نفس نوع أحد أصنافه الأساسية -أو كانت هناك صنف أساسية مشترك)- فيلزم على الأقل تخصيص بايت واحد لضمان ألّا يحتلّ كائنان مختلفان من نفس النوع العنوان نفسه. التحسين عن طريق تقليل الشيفرة المُنفّذة الأسلوب الأوضح للتحسين هو تقليل الشيفرة المنفّذة، فعادة ما يسرِّع التنفيذَ دون تغيير التعقيد الزمني للشيفرة. لكن رغم أنّ هذه الطريقة تسرّع البرنامج، فلن تظهر فائدتها إلّا في الشيفرات التي تُستدعى كثيرًا. هذه بعض الأمثلة على تقليل الشيفرات المنفّذة: إزالة الشيفرات عديمة الفائدة void func(const A *a); // دالة ما // تخصيص غير ضروري للذاكرة + إلغاء تخصيص النسخة auto a1 = std::make_unique<A>(); func(a1.get()); // استخدام المكدّس يمنع auto a2 = A{}; func(&a2); الإصدار ≥ C++‎ 14 منذ الإصدار C++‎ 14، يُسمح للمُصرِّفات بتحسين هذه الشيفرة لإزالة تخصيص الذاكرة، ومطابقة التحرير (matching deallocation). تنفيذ الشيفرة مرة واحدة فقط std::map<std::string, std::unique_ptr<A>> lookup; // إدارج/بحث بطيئ // داخل هذه الدالة، سنمر على القاموس مرّتين لأجل البحث عن العنصر، ومرّة ثالثة إن لم يكن موجودا const A *lazyLookupSlow(const std::string &key) { if (lookup.find(key) != lookup.cend()) lookup.emplace_back(key, std::make_unique<A>()); return lookup[key].get(); } // داخل هذه الدالة، سيحصل نفس التأثير الذي يحدث في النسخة الأبطأ لكن مع مضاعفة السرعة // لأنّنا سنمر على القاموس مرة واحدة فقط const A *lazyLookupSlow(const std::string &key) { auto &value = lookup[key]; if (!value) value = std::make_unique<A>(); return value.get(); } يمكن استخدام أسلوب مشابه للتحسين عبر تنفيذ (implement) إصدار مستقر من ‎unique‎: std::vector<std::string> stableUnique(const std::vector<std::string> &v) { std::vector<std::string> result; std::set<std::string> checkUnique; for (const auto &s : v) { // يُعاد الإدراج إن كان ناجحا، لذلك يمكن أن نعرف ما إذا كان العنصر موجودًا أم لا // هذا يمنع الإدراج، والذي سيمر على القاموس مرّة لكل عنصر فريد // على عناصر مكرّرة v يُكسِبنا هذا حوالي نصف الوقت إن لم تحتو if (checkUnique.insert(s).second) result.push_back(s); } return result; } منع عمليات إعادة التخصيص والنسخ/النقل عديمة الفائدة منعنا في المثال السابق عمليّات البحث في std::set، بيْد أنّ هذا غير كافٍ، فما يزال المتجه (‎std::vector‎) يحتوي على خوارزمية مكلّفة ونامية ستحتاج إلى نقل ذاكرتها، يمكن منع هذا عبر حجز الحجم المناسب. في المثال التالي، نضمن عدم حدوث أي نسخ أو نقل في المتجه لأن سعته تساوي الحد الأقصى للأعداد التي سندرجها، وإن افترضنا عدم حدوث أي تخصيص من الحجم صفر، وأن تخصيص كتلة كبيرة من الذاكرة يأخذ نفس الوقت الذي يأخذه تخصيص حجم صغير فهذا لن يبطئ البرنامج. ملاحظة: يمكن أن تنتبه المصرفات لهذا الأمر وتحذف شيفرة التحقق من الشيفرة المولَّدة. std::vector<std::string> stableUnique(const std::vector<std::string> &v) { std::vector < std::string > result; result.reserve(v.size()); std::set < std::string > checkUnique; for (const auto &s : v) { // انظر المثال أعلاه if (checkUnique.insert(s).second) result.push_back(s); } return result; } استخدام حاويات أكفأ استخدام بنيات البيانات الصحيحة في الوقت المناسب يمكن أن يحسّن التعقيد الزمني للشيفرة، انظر المثال التالي حيث يساوي تعقيد stableUnique هنا (N log(N، حيث N أكبر من عدد العناصر في v: // log(N) > insert complexity of std::set std::vector<std::string> stableUnique(const std::vector<std::string> &v) { std::vector<std::string> result; std::set<std::string> checkUnique; for (const auto &s : v) { // انظر فقرة التحسين عبر تقليل الشيفرة المنفّذة if (checkUnique.insert(s).second) result.push_back(s); } return result; } يمكننا تخفيض تعقيد التقديم إلى N عبر استعمال حاوية تستخدم تنفيذًا مختلفًا لتخزين عناصرها، كقاموس مثلًا (hash) بدلًا من شجرة (tree). وهناك أثر جانبي إضافي أيضًا، وهو تقليل استدعاء مُعامل المقارنة على السلاسل النصية، لأنّنا لن نحتاج إلى استدعائها إلّا في حال كانت ستُدرج السلسلة النصية في نفس المجموعة. انظر المثال التالي حيث يكون تعقيد stableUnique مساويًا لـ (N log (N، حيث N أكبر من عدد العناصر في v: // أكبر من 1 std::unordered_set تعقيد الإدراج في std::vector<std::string> stableUnique(const std::vector<std::string> &v) { std::vector<std::string> result; std::unordered_set<std::string> checkUnique; for (const auto &s : v) { // انظر فقرة التحسين عبر تقليل الشيفرة المنفّذة if (checkUnique.insert(s).second) result.push_back(s); } return result; } تحسين الكائنات الصغيرة Small Object Optimization تحسين الكائنات الصغيرة هي تقنية تُستخدم في بنيات البيانات منخفضة المستوى، مثل ‎std::string‎ (يشار إليها أحيانًا باسم تحسين السلاسل النصية القصيرة/الصغيرة)، ويُقصد بها استخدام مساحة مكدّس كمخزن مؤقت (buffer) بدلًا من استخدام ذاكرة مخصّصة عندما يكون المحتوى صغيرًا بدرجة كافية لتسَعَه المساحة المحجوزة. وسنحاول منع تخصيص مساحة في الكومة (heap)، لكنّ ثمن هذا هو إضافة حمل إضافي (overhead) على الذاكرة، وإجراء بعض الحسابات الإضافية. تعتمد فوائد هذه التقنية على نوع الاستخدام، ويمكن أن تضرّ بالأداء في حال استخدامها بشكل غير صحيح. هذا مثال على ذلك، الطريقة التالية ساذجة لكتابة سلسلة نصية باستخدام هذا التحسين: #include <cstring> class string final { constexpr static auto SMALL_BUFFER_SIZE = 16; bool _isAllocated{false}; ///< تذكّر إذا كنّا قد خصّصنا الذاكرة char *_buffer{nullptr}; ///< مؤشر يشير إلى المخزن المؤقت الذي نستخدمه char _smallBuffer[SMALL_BUFFER_SIZE]= {'\0'}; ///< SMALL OBJECT مساحة المكدّس المُستخدمة لأجل الكائن الصغير OPTIMIZATION public: ~string() { if (_isAllocated) delete [] _buffer; } explicit string(const char *cStyleString) { auto stringSize = std::strlen(cStyleString); _isAllocated = (stringSize > SMALL_BUFFER_SIZE); if (_isAllocated) _buffer = new char[stringSize]; else _buffer = &_smallBuffer[0]; std::strcpy(_buffer, &cStyleString[0]); } string(string &&rhs) : _isAllocated(rhs._isAllocated) , _buffer(rhs._buffer) , _smallBuffer(rhs._smallBuffer) //< غير ضروري عند التخصيص { if (_isAllocated) { // منع الحذف المزدوج للذاكرة rhs._buffer = nullptr; } else { // نسخ البيانات std::strcpy(_smallBuffer, rhs._smallBuffer); _buffer = &_smallBuffer[0]; } } // حَذفنا التوابع الأخرى، مثل منشئ النسخ ومعامل الإسناد، لأجل تحسين القراءة والوضوح }; كما ترى في الشيفرة أعلاه، فقد أُضيفت بعض التعقيدات الإضافية لمنع تنفيذ بعض عمليات ‎new‎ و ‎delete‎. وأصبح لدى الصنف مساحة أكبر للذاكرة، وقد لا يستخدَمها إلا في بضع حالات فقط. غالبًا ما سيُحاول ترميزَ القيمة البوليانية ‎_isAllocated‎ داخل المؤشّر ‎_buffer‎ عبر معالجة البتّات من أجل تقليل حجم النُسخة (intel 64 bit: يمكن أن يقلّل الحجم بمقدار 8 بايت)، لكن هذا التحسين غير ممكن إلا إن كانت قواعد المحاذاة الخاصّة بالمنصة معروفة. حالات الاستخدام نظرًا لأنّ هذا التحسين يضيف الكثير من التعقيد، فليس من المستحسن استخدامه في كل الأصناف. وستُصادفه غالبًا في بنيات البيانات منخفضة المستوى شائعة الاستخدام. قد تجد بعض الاستخدامات في ‎std::basic_string<>‎ و ‎std::function<>‎ في تطبيقات (implementations) المكتبة القياسية الشائعة في C++‎ 11. وبما أن هذا التحسين لا يمنع تخصيصات الذاكرة إلا عندما تكون البيانات المُخزّنة أصغر من القيمة المضافة، فستظهر فائدته بالخصوص في الأصناف التي تُستخدم غالبًا مع البيانات الصغيرة. لكن هناك عيب فيه وهو الحاجة إلى جهد إضافي عند تحريك المخزن المؤقت، وهذا يجعل عمليّات النقل أكثر كلفة موازنة بالحالات التي لا يُستخدم المخزن المؤقّت فيها، خاصة عندما يحتوي المخزن المؤقّت على نوع بيانات غير قديم (non-POD type). توسيع التضمين والتضمين توسيع التضمين (Inline expansion)، أو التضمين inlining وحسب، هي تقنية تحسين يستخدمها المُصرِّف تقوم على تعويض استدعاء دالة بمتن تلك الدالّة. هذا يقلّل من الحمل الزائد (overhead) لاستدعاء الدوالّ، ولكن على حساب الذاكرة، إذ أنّ الدالّة قد تُكرّر عدّة مرات. // :المصدر int process(int value) { return 2 * value; } int foo(int a) { return process(a); } // :البرنامج بعد التضمين int foo(int a) { return 2 * a; // foo() في process() نسخ متن } تُستخدم تقنية التضمين غالبًا مع الدوال الصغيرة، حيث يكون الحمل الزائد لاستدعاء الدالّة صغيرًا مقارنةً بحجم جسم الدالّة. تحسين الصنف الأساسي الفارغ Empty base optimization يجب أن لا يقلّ حجم أي كائن أو عضو من كائن فرعي عن القيمة 1، حتى لو كان ذلك النوع فارغًا (أي حتى لو لم يحتوٍ الصنف ‎class‎ أو البنية ‎struct‎ على أعضاء بيانات غير ساكنة)، وذلك لأجل ضمان أن تكون عناوين الكائنات المختلفة التي تنتمي لنفس النوع مختلفة دائمًا. بالمقابل، فإنّ القيود على الكائنات الفرعية من الصنف الأساسي (base class subobjects) أقل بكثير، ويمكن تحسينها بشكل كامل انطلاقًا من مخطط الكائن: #include <cassert> struct Base {}; // صنف فارغ struct Derived1: Base { int i; }; int main() { // حجم أيّ كائن من صنف فارغ يساوي 1 على الأقل assert(sizeof(Base) == 1); // تطبيق تحسين الصنف الفارغ assert(sizeof(Derived1) == sizeof(int)); } يُستخدم تحسين الصنف الفارغ عادةً في أصناف المكتبة القياسية الواعية بتخصيص الذاكرة (‎std::vector‎ و std::function و std::shared_ptr، … إلخ) لتجنّب شغل أي مساحة تخزين إضافية من قبل عضو مُخصِّص (allocator member) إذا كان ذلك المُخصّص عديم الحالة (stateless). ويمكن تحقيق ذلك عن طريق تخزين أحد أعضاء البيانات المطلوبة (مثل مؤشّرات ‎begin‎ و ‎end‎، أو ‎capacity‎ الخاصّة بالمتجهات). التشخيص (Profiling) التشخيص باستخدام gcc و gprof يتيح لك برنامج التشخيص gprof الخاصّ بـ GNU gprof تشخيص شيفرتك. ولأجل لاستخدامه، عليك إنجاز الخطوات التالية: ابنِ التطبيق مع الإعدادات المناسبة لتوليد معلومات التشخيص. قم بتوليد معلومات التشخيص عن طريق تشغيل التطبيق المبني. اعرض معلومات التشخيص المُولّدة باستخدام أداة gprof. أضف راية ‎-pg‎ لأجل بناء التطبيق مع إعدادات توليد معلومات التشخيص المناسبة، انظر: $ gcc -pg *.cpp -o app أو: $ gcc -O2 -pg *.cpp -o app وهكذا دواليك. بمجرد إنشاء التطبيق (نفترض أنّ اسمه ‎app‎)، نفِّذه كالمعتاد: $ ./app من المفروض أن ينتج هذا الأمرُ ملفًّا يُسمّى ‎gmon.out‎. إن أردت رؤية نتائج التشخيص، نفّذ الأمر التالي: $ gprof app gmon.out (لاحظ أننا وقرنا كلًّا من التطبيق وكذلك الخرج الناتج). ويمكنك توجيه الخرج أو إعادة توجيهه: $ gprof app gmon.out | less يجب أن تكون نتيجة الأمر الأخير مطبوعة على شكل جدول، ستمثّل صفوفهُ الدوالَّ، أمّا أعمدته فتشير إلى عدد الاستدعاءات، وإجمالي الوقت المستغرق، والوقت الذاتي المستغرق - self time spent - (أي الوقت المستغرق داخل الدالة ما عدا استدعاءات الدوال الداخلية). إنشاء مخططات الاستدعاءات callgraph بواسطة gperf2dot بالنسبة للتطبيقات الكبيرة، فقد يصعب فهم تنسيق التشخيص المُستَوي (flat execution profiles)، لهذا تنتج العديد من أدوات التشخيص رسومًا تخطيطية لتسهيل قراءتها. تحوّل أداة gperf2dot خرْج النص الذي تنتجه العديد من المُشخِّصات (مثل Linux perf و callgrind و oprofile) إلى رسم تخطيطي، يمكنك استخدام هذه الأداة عن طريق تشغيل المشخّص (profiler) الذي تعمل به (مثل ‎gprof‎): # التصريف مع رايات التشخيص g++ *.cpp -pg # توليد بيانات التشخيص ./main # ترجمة بيانات التشخيص إلى نص، وإنشاء صورة gprof ./main | gprof2dot -s | dot -Tpng -o output.png تشخيص استخدام وحدة المعالجة المركزية بواسطة gcc و Google Perf توفّر أدوات Google Perf برنامج تشخيص لوحدة المعالجة المركزية (CPU)، مع واجهة استخدام سهلة نسبيًا. لاستخدامها عليك: تثبيت أدوات Google Perf . تصريف الشيفرة كالمعتاد. إضافة مكتبة التشخيص ‎libprofiler‎ إلى مسار تحميل المكتبات في وقت التشغيل. استخدم ‎pprof‎ لتوليد تشخيص نصّي، أو رسم تخطيطي. مثلّا: g++ -O3 -std=c++11 main.cpp -o main # تنفيذ المُشخِّص LD_PRELOAD=/usr/local/lib/libprofiler.so CPUPROFILE=main.prof CPUPROFILE_FREQUENCY=100000 ./main حيث: تشير ‎CPUPROFILE‎ إلى ملف بيانات التشخيص، و تشير ‎CPUPROFILE_FREQUENCY‎ إلى تردّد عيّنات المشخّص (profiler sampling frequency). إن أردت إجراء المعالجة اللاحقة (post-process) لبيانات التشخيص، فاستخدم ‎pprof‎، ويمكنك إنشاء تشخيص استدعاءات (flat call profile) على هيئة نصّ: $ pprof --text ./main main.prof PROFILE: interrupts/evictions/bytes = 67/15/2016 pprof --text --lines ./main main.prof Using local file ./main. Using local file main.prof. Total: 67 samples 22 32.8% 32.8% 67 100.0% longRunningFoo ??:0 20 29.9% 62.7% 20 29.9% __memmove_ssse3_back /build/eglibc-3GlaMS/eglibc-2.19/string/../sysdeps/x86_64/multiarch/memcpy-ssse3-back.S:1627 4 6.0% 68.7% 4 6.0% __memmove_ssse3_back /build/eglibc-3GlaMS/eglibc-2.19/string/../sysdeps/x86_64/multiarch/memcpy-ssse3-back.S:1619 3 4.5% 73.1% 3 4.5% __random_r /build/eglibc-3GlaMS/eglibc-2.19/stdlib/random_r.c:388 3 4.5% 77.6% 3 4.5% __random_r /build/eglibc-3GlaMS/eglibc-2.19/stdlib/random_r.c:401 2 3.0% 80.6% 2 3.0% __munmap /build/eglibc-3GlaMS/eglibc-2.19/misc/../sysdeps/unix/syscall-template.S:81 2 3.0% 83.6% 12 17.9% __random /build/eglibc-3GlaMS/eglibc-2.19/stdlib/random.c:298 2 3.0% 86.6% 2 3.0% __random_r /build/eglibc-3GlaMS/eglibc-2.19/stdlib/random_r.c:385 2 3.0% 89.6% 2 3.0% rand /build/eglibc-3GlaMS/eglibc-2.19/stdlib/rand.c:26 1 1.5% 91.0% 1 1.5% __memmove_ssse3_back /build/eglibc-3GlaMS/eglibc-2.19/string/../sysdeps/x86_64/multiarch/memcpy-ssse3-back.S:1617 1 1.5% 92.5% 1 1.5% __memmove_ssse3_back /build/eglibc-3GlaMS/eglibc-2.19/string/../sysdeps/x86_64/multiarch/memcpy-ssse3-back.S:1623 1 1.5% 94.0% 1 1.5% __random /build/eglibc-3GlaMS/eglibc-2.19/stdlib/random.c:293 1 1.5% 95.5% 1 1.5% __random /build/eglibc-3GlaMS/eglibc-2.19/stdlib/random.c:296 1 1.5% 97.0% 1 1.5% __random_r /build/eglibc-3GlaMS/eglibc-2.19/stdlib/random_r.c:371 1 1.5% 98.5% 1 1.5% __random_r /build/eglibc-3GlaMS/eglibc-2.19/stdlib/random_r.c:381 1 1.5% 100.0% 1 1.5% rand /build/eglibc-3GlaMS/eglibc-2.19/stdlib/rand.c:28 0 0.0% 100.0% 67 100.0% __libc_start_main /build/eglibc-3GlaMS/eglibc-2.19/csu/libcstart. c:287 0 0.0% 100.0% 67 100.0% _start ??:0 0 0.0% 100.0% 67 100.0% main ??:0 0 0.0% 100.0% 14 20.9% rand /build/eglibc-3GlaMS/eglibc-2.19/stdlib/rand.c:27 0 0.0% 100.0% 27 40.3% std::vector::_M_emplace_back_aux ??:0 أو يمكنك إنشاء رسم تخطيطي في ملف pdf باستخدام الأمر: pprof --pdf ./main main.prof > out.pdf هذا الدرس جزء من سلسلة دروس عن C++‎. ترجمة -بتصرّف- للفصل Chapter 143: Optimization in C++‎ والفصل Chapter 144: Optimization والفصل Chapter 145: Profiling من كتاب C++ Notes for Professionals
  3. يقضي مطوّرو C++‎ الكثير من وقتهم في تنقيح الأخطاء (debugging)، ويهدف هذا المقال إلى مساعدتك في هذه المهمة وإعطائك بعض التقنيات المفيدة، لكن لا تتوقع قائمة شاملة للمشاكل وحلولها التي تدعمها هذه الأدوات. اختبار الوحدات في C++‎ يسعى اختبار الوحدات (Unit testing) إلى التحقق من صحّة وسلامة وحدات الشيفرة (units of code)، ويشير مصطلح "وحدات الشيفرة" في C++‎ غالبًا إلى الأصناف أو الدوال أو مجموعات مؤلّفة منهما. ويُجرى اختبار الوحدات غالبًا باستخدام "أُطر اختبار" متخصصة أو "مكتبات اختبار"، والتي تستخدِم غالبًا صياغات أو أنماط غير اعتيادية، وسنراجع في هذا الموضوع بعض استراتيجيات ومكتبات وأطر اختبار الوحدات. اختبار جوجل اختبار جوجل (Google Test) هو إطار لاختبار C++‎ من Google، ويتطلّب استخدامُه بناء مكتبة ‎gtest‎ وربطها بإطار الاختبار الخاص بك. مثال بسيط، سنقسم المثال بداعي الشرح … // main.cpp #include <gtest/gtest.h> #include <iostream> أنشئت حالات اختبار جوجل بواسطة وحدات ماكرو خاصة بالمعالج الأولي لـ ++C، وسنوفر المعامِليْن test name و test suite، نتابع … TEST(module_name, test_name) { std::cout << "Hello world!" << std::endl; كذلك توفر Google Test وحدات ماكرو لأجل التوكيدات (assertions): ASSERT_EQ(1+1, 2); } يمكن تشغيل اختبار جوجل يدويًا من دالة ()main أو ربطها بمكتبة gtest_main لدالة ()main معدَّة سلفًا لقبول حالات اختبار جوجل، … int main(int argc, char** argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); } // أمر البناء: g++ main.cpp -lgtest catch Catch هي مكتبة ترويسة (header only library) تسمح لك باستخدام كل من نمطي الاختبار TDD و BDD. الشيفرة التالية مأخوذة من صفحة توثيق Catch: SCENARIO("vectors can be sized and resized", "[vector]") { GIVEN("A vector with some items") { std::vector v(5); REQUIRE(v.size() == 5); REQUIRE(v.capacity() >= 5); WHEN("the size is increased") { v.resize(10); THEN("the size and capacity change") { REQUIRE(v.size() == 10); REQUIRE(v.capacity() >= 10); } } WHEN("the size is reduced") { v.resize(0); THEN("the size changes but not capacity") { REQUIRE(v.size() == 0); REQUIRE(v.capacity() >= 5); } } WHEN("more capacity is reserved") { v.reserve(10); THEN("the capacity changes but not the size") { REQUIRE(v.size() == 5); REQUIRE(v.capacity() >= 10); } } WHEN("less capacity is reserved") { v.reserve(0); THEN("neither size nor capacity are changed") { REQUIRE(v.size() == 5); REQUIRE(v.capacity() >= 5); } } } } سيُبلَغ عن هذه الاختبارات على النحو التالي عند التشغيل: Scenario: vectors can be sized and resized Given: A vector with some items When: more capacity is reserved Then: the capacity changes but not the size التحليل الساكن Static analysis التحليل الساكن طريقة لتفحّص الشيفرة للبحث عن الأنماط التي ترتبط عادة بالأخطاء المشهورة وتدل عليها، هذه التقنية تستغرق وقتًا أقل من مراجعة الشيفرة، لكن تقتصر عمليات التحقق التي تقوم بها على الحالات المُبرمجة في الأداة. وتتضمّن عمليات مثل التحقّق استخدام الفاصلة المنقوطة بشكل غير صحيح بعد عبارة ‎(if (var);)‎ مثلًا، وخوارزميات الشُّعَب (graph algorithms) المتقدمة التي تحدد ما إذا كان المتغيّر مُهيَّأً أم لا. تحذيرات المُصرِّف Compiler warnings من السهل تمكين التحليل الساكن، فهناك إصدارات مُبسّطة مُضمّنة سلفًا في مصرّفك: clang++ -Wall -Weverything -Werror ...‎ g++ -Wall -Weverything -Werror ...‎ cl.exe /W4 /WX ...‎ إذا مكّنتَ هذه الخيارات، ستلاحظ أنّ كل مُصرِّف سيعثر على أخطاء لا تنتبه إليها المصرّفات الأخرى، وستحصل على أخطاء على تقنيات قد تكون صالحة في سياقات محدّدة. فمثلًا، قد تكون ‎while (staticAtomicBool);‎ مقبولة حتى لو لم تكن ‎while (localBool);‎ كذلك. لذلك، على عكس مراجعة الشيفرة، فأنت تستخدم أداة تفهم شيفرتك، وتنبّهك إلى الكثير من الأخطاء، وأحيانًا قد لا تتفق معك. قد تضطر إلى وقف التحذير محليًا في هذه الحالة نظرًا لأنّ الخيارات المذكورة أعلاه قد تُمكِّن جميع التحذيرات، بما في ذلك التحذيرات التي لا تريدها (مثلًا، لماذا يجب أن تكون شيفرتك متوافقة مع C++‎ 98؟). في هذه الحالة، يمكنك تعطيل تحذير بعينه: clang++ -Wall -Weverything -Werror -Wno-errortoaccept ...‎ ‎‎‎‎g++ -Wall -Weverything -Werror -Wno-errortoaccept ...‎‎ ‎‎‎‎cl.exe /W4 /WX /wd<no of warning>...‎‎ صحيح أنّ تحذيرات المصرّف ستساعدك أثناء التطوير، إلّا أنّها تُبطئ عملية التصريف، لهذا قد لا ترغب في تمكينها افتراضيًا في كل مرة، فإما أن تشغّل تحذيرات المصرّف افتراضيًا، أو يمكنك تمكين التكامل المستمر (continuous integration) باستخدام عمليات تحقق أكثر كلفة (أو كليهما). أدوات خارجية إذا قرّرت العمل بالتكامل المستمر (continuous integration)، فإنّ استخدام الأدوات الأخرى ليست ذات فائدة. وتحتوي أداة مثل clang-tidy على قائمة من عمليّات التحقق التي تغطي مجموعة واسعة من المشاكل، منها: الأخطاء الفعلية منع التشريح (Prevention of slicing) توكيدات (Asserts) مع آثار جانبية التحقّق من إمكانية القراءة إزاحات بادئة (indentation) غير صحيحة التحقق من تسمية المُعرِّف (Check identifier naming) التحقّق من التحديث (Modernization checks) استخدام make_unique()‎ استخدام nullptr *التحقّق من الأداء إيجاد النسخ غير الضرورية البحث عن خوارزميات الاستدعاء غير الفعّالة قد لا تكون القائمة كبيرة، إذ أنّ Clang تحتوي على الكثير من التحذيرات، إلّا أنها ستجعل شيفرتك أكثر أمانًا. أدوات أخرى توجد أدوات أخرى مماثلة بنفس الغرض، مثل: محلل visual studio الساكن كأداة خارجية clazy، إضافة خاصّة بمصرّف Clang للتحقق من شيفرات Qt الخلاصة توجد الكثير من أدوات التحليل الساكن لـ C++‎، سواء المضمّنة في المُصرِّف، أو الأدوات الخارجية، ولا تأخذ تجربتها الكثير من الوقت عند الاكتفاء بالعمل بالإعدادات السهلة، وستساعدك على رصد أخطاء قد لا تنتبه لها عند مراجعة الشيفرة. تحليل Segfault مع GDB سنستخدم نفس الشيفرة التي استخدمناها في المثال أعلاه. #include <iostream> void fail() { int *p1; int *p2(NULL); int *p3 = p1; if (p3) { std::cout << *p2 << std::endl; } } int main() { fail(); } أولًا، سنحاول تصريفها: g++ -g -o main main.cpp ثم نشغّلها بواسطة gdb: gdb ./main الآن، سنكون في صدفة gdb. اكتب الأمر run. (gdb) run The program being debugged has been started already. Start it from the beginning? (y or n) y Starting program: /home/opencog/code-snippets/stackoverflow/a.out Program received signal SIGSEGV, Segmentation fault. 0x0000000000400850 in fail () at debugging_with_gdb.cc:11 11 std::cout << *p2 << std::endl; لاحظ أنّ خطأ التجزئة (segmentation fault) قد حدث في السطر 11، لذا فالمتغيّر الوحيد المُستخدم في هذا السطر هو المؤشّر p2. يمكن التحقّق من محتواه بطباعة ما يلي: (gdb) print p2 $1 = (int *) 0x0 نلاحظ أنّ p2 هُيِّئ عند القيمة 0×0، والتي تكافئ NULL. في هذا السطر، حاولنا تحصيل (dereference) مؤشّر فارغ، لذا علينا إصلاح هذا الخلل. تنظيف الشيفرات يبدأ التنقيح بفهم الشيفرة التي تحاول تنقيحها. انظر المثال التالي عن شيفرة سيّئة: int main() { int value; std::vector < int > vectorToSort; vectorToSort.push_back(42); vectorToSort.push_back(13); for (int i = 52; i; i = i - 1) { vectorToSort.push_back(i *2); } /// مُحسّنة لاستخدامها في ترتيب المتجهات الصغيرة if (vectorToSort.size() == 1); else { if (vectorToSort.size() <= 2) std::sort(vectorToSort.begin(), std::end(vectorToSort)); } for (value: vectorToSort) std::cout << value << ' '; return 0; } هذه الشيفرة أفضل: std::vector < int > createSemiRandomData() { std::vector < int > data; data.push_back(42); data.push_back(13); for (int i = 52; i; --i) vectorToSort.push_back(i *2); return data; } /// مُحسّنة لاستخدامها في ترتيب المتجهات الصغيرة void sortVector(std::vector &v) { if (vectorToSort.size() == 1) return; if (vectorToSort.size() > 2) return; std::sort(vectorToSort.begin(), vectorToSort.end()); } void printVector(const std::vector<int> &v) { for (auto i: v) std::cout << i << ' '; } int main() { auto vectorToSort = createSemiRandomData(); sortVector(std::ref(vectorToSort)); printVector(vectorToSort); return 0; } بغض النظر عن نمط التشفير الذي تفضله، سيساعدك الثّبات على نفس النمط على فهم شيفرتك، ويمكن رصد فرصتين لتحسين قراءة الشيفرة بالنظر إلى الشيفرة أعلاه، وكذلك تسهيل تنقيحها: استخدام دوال منفصلة للإجراءات المنفصلة يتيح لك استخدام دوال منفصلة تخطّي بعض الدوال في المنقّح إذا لم تكن مهتمًّا بتفاصيلها. قد لا تكون مهتمًا بتفاصيل إنشاء البيانات أو طباعتها في هذه الحالة الخاصّة، وقد ينصبّ اهتمامك على عملية الترتيب وحَسب. هناك ميزة أخرى، وهي أنّ الشيفرة التي عليك قراءتها وحفظها ستكون أقصر عند تفحّص الشيفرة، إذ ستحتاج الآن إلى قراءة 3 أسطر فقط من الشيفرة في دالة ‎main()‎ لفهمها، بدلًا من قراءة الدالّة بأكملها. والميزة الثالثة هي أنّه نظرًا لكون الشيفرةً المُتحقَّق منها أقصر، فإن عينك ستتدرب الآن على رصد الأخطاء في غضون ثوانٍ. استخدام تنسيقات وإنشاءات متّسقة استخدامُ التنسيقات والإنشاءات المتّسقة ينظّم الشيفرةَ ويسهّل تحليلها، وهناك نقاشات كثيرة حول طريقة التنسيق "الأمثل". لكن بغضّ النظر فالمهمّ أن تلتزم بنمط برمجة واحد في شيفرتك، فذلك سيحسّن قراءتها ويسهّل التركيز على محتوى الشيفرة. ويوصى باستخدام أداة مخصّصة لتنسيق الشيفرة نظرًا لأنّ هذه المهمّة قد تستغرق وقتًا طويلاً، وتدعم معظم بيئات التطوير المتكاملة (IDEs) هذه الميزة بشكل أو بآخر. قد تلاحظ أنّ نمط البرمجة لا يقتصر على استخدام المسافات الفارغة والسطور الجديدة، فلم نعد نمزج بين النمط الحر (free-style) والدوال التابعة لبدء / إنهاء الحاويات (‎v.begin()‎ و ‎std::end(v)‎). تسليط الضوء على الأجزاء الهامّة من شيفرتك بغض النظر عن النمط الذي تستخدمه، فإنّ الشيفرة أعلاه تحتوي على عدد من الملاحظات التي قد تعطيك تلميحًا حول الأمور المهمّة في الشيفرة: تعليق يتضمّن الكلمة "مُحسَّن" ، في إشارة إلى استخدام بعض التقنيات الجذابة. بعض تعليمات return المبكّرة في ‎sortVector()‎ تشير إلى أنّنا نريد فعل شيء خاصّ. ‎std::ref()‎ - تشير إلى أنّ شيئًا ما يحدث في ‎sortVector()‎. الخلاصة الشيفرات النظيفة أسهل في الفهم وتقلل الوقت اللازم لتنقيحهَا، فقد يتمكّن مُراجِع الشيفرة من رصد الأخطاء منذ الوهلة الأولى في المثال الثاني، بينما قد تكون الأخطاء أخفَى في المثال الأوّل. (ملاحظة الخطأ موجود في: عملية الموازنة مع العدد ‎2‎.) هذا الدرس جزء من سلسلة دروس عن C++‎. ترجمة -بتصرّف- للفصل Chapter 142: C++ Debugging and Debugprevention Tools & Techniques والفصل Chapter 141: Unit Testing in C++‎ من كتاب C++ Notes for Professionals
  4. ينبغي تصريف البرامج المكتوبة بلغة C++‎ قبل أن تتمكن تلك البرامج من العمل، وستجد مجموعة كبيرة ومتنوّعة من برامج التصريف أو المصرِّفات (compilers) المتاحة والمناسبة لنظام التشغيل الذي تعمل به. التصريف بواسطة GCC التصريف دون تحسين مفيد في المراحل الأولى من التطوير والتنقيح، على الرغم من أنّ خيار ‎-Og‎ موصىً به في الإصدارات الحديثة من GCC، وبناء عليه فيمكن تصريف وربط ملف تنفيذي وغير محسَّن على افتراض أن هناك ملفًا مصدريًا واحدًا يسمى main.cpp، وذلك على النحو التالي: g++ - o app - Wall main.cpp - O0 ولإنتاج ملف مُحسّن قابل للتنفيذ لاستخدامه في الإنتاج، استخدم أحد خيارات ‎-O‎ (راجع: ‎،-O1‎، ‎-O2‎، ‎-O3‎، ‎-Os‎، ‎-‎‎Ofast‎): g++ - o app - Wall - O2 main.cpp في حال حذف الخيار ‎-O، سيُستخدَم ‎-O0 الذي يعني إلغاء التحسينات، كخيار افتراضي، واعلم أن تحديد ‎-O بدون رقم يجعله مساويًا لـ ‎-O1. وكخيار بديل، يمكن أن نستخدم رايات التحسين (optimization flags) من مجموعات ‎O‎ -أو تحسينات تجريبية أخرى- مباشرةً. انظر المثال التالي حيث يبني البرنامج مع التحسين ‎-O2‎، بالإضافة إلى راية واحدة من مستوى التحسين ‎-O3‎: g++ -o app -Wall -O2 -ftree-partial-pre main.cpp لإنتاج ملفّ تنفيذي مُحسّن لأجل منصة معيّنة -لاستخدامه على جهاز له نفس المعمارية-، استخدم: g++ -o app -Wall -O2 -march=native main.cpp سوف ينتُج عن الشيفرَتين أعلاه ملفٌّ ثنائي (binary file) يمكن تشغيله باستخدام ‎.\app.exe‎ على Windows، أو باستخدام ‎./app‎ على Linux و Mac OS، كما يمكن أيضًا تخطي الراية ‎-o‎، وفي هذه الحالة سينشئ GCC الملفّ التنفيذي الافتراضي ‎a.exe‎ على Windows و ‎a.out‎ على الأنظمة الشبيهة بيونكس (Unix-like). لأجل تصريف ملف دون ربطه، استخدم الخيار ‎-c‎: g++ -o file.o -Wall -c file.cpp سينتج عن هذا ملفٌّ يحمل الاسم ‎file.o‎، والذي يمكن ربطه لاحقًا بملفّات أخرى لإنتاج ملف ثنائي: g++ -o app file.o otherfile.o انظر gcc.gnu.org للمزيد من التفاصيل حول خيارات التحسين، خاصة ‎-Og‎، وهو التحسين مع التركيز على تجربة التنقيح (debugging)، الذي يوصى باستخدامه في دورات تحرير-تصريف-تنقيح القياسية - standard edit-compile-debug cycle)، وكذلك ‎-Ofast‎ الذي يشمل جميع التحسينات، بما في ذلك تلك التي تتجاهل الامتثال الصارم للمعايير. تُمكِّنك راية ‎-Wall‎ من إطلاق تحذيرات من بعض الأخطاء الشائعة، وينبغي أن تُستخدم دائما، ويوصى باستخدام ‎-Wextra‎ لتحسين جودة الشيفرة، وكذلك رايات التحذير الأخرى التي لا تُمكَّن تلقائيًا من قِبل ‎-Wall‎ و ‎-‎‎Wextra‎. إذا كانت الشيفرة تتوقع معيارًا محدّدًا للغة C++‎، فيمكنك تحديد المعيار الذي تريد استخدامه عن طريق تضمين راية ‎-std=‎. وتتوافق القيم المدعومة مع سنة الإصدار النهائي لمعيار ISO C++‎، واعتبارا من GCC 6.1.0 فإنّ القيم الصالحة لـ ‎std=‎ هي: ‎c++98‎ / ‎c++03‎، ‎c++11‎، ‎c++14‎، و‎c++17‎ / ‎c++1z‎. لاحظ أن القيم المفصولة بشرطةٍ مائلة / متكافئة. g++ -std=c++11 <file> يتضمّن GCC بعض الإضافات (Extensions) (extensions) الخاصة بالمُصرِّف، والتي تُعطَّل عندما تتعارض مع معيار قياسي محدّد من قبل راية ‎-std=‎، فإذا أردت التصريف مع كل الإضافات (Extensions) المُمَكَّنة، فاستخدام القيمة ‎gnu++XX‎، حيث تمثّل ‎XX‎ أيًّا من السنوات المذكورة أعلاه. ويُستخدَم المعيار الافتراضي في حالة عدم تعريف أيّ منها، وبالنسبة لإصدارات GCC التي تسبق 6.1.0 فإنّ الإعداد الافتراضي هو ‎-‎std‎ = ‎gnu‎++03، أما في GCC 6.1.0 والإصدارات الأحدث فإن الإعداد الافتراضي هو ‎-std=gnu++14‎. لاحظ أنّه نظرا لوجود بعض الأخطاء (bugs) في GCC، فيجب أن تكون راية pthread- حاضرة في التصريف والربط إن أردت أن يدعم GCC معيار الخيوط (threading) الذي أُدخل في C++‎11، مثل ‎std::thread‎ و ‎std::wait_for‎. قد لا ينتج عن حذفها عند استخدام الخيوط أيّ تحذيرات، ولكن قد تحصل على نتائج غير صحيحة في بعض المنصّات. الربط بالمكتبات Linking with libraries استخدم خيار ‎-l‎ لتمرير اسم المكتبة: g++ main.cpp -lpcre2-8 # 8bit code units (UTF-8) في وحدات الشيفرات ثمانية البتّات PCRE2 هي مكتبة pcre2-8 إذا لم تكن المكتبة موجودة في مسار المكتبة القياسية، فأضف المسار باستخدام ‎-L‎: g++ main.cpp -L/my/custom/path/ -lmylib يمكن ربط عدّة مكتبات معًا: g++ main.cpp -lmylib1 -lmylib2 -lmylib3 إذا كانت إحدى المكتبات تعتمد على مكتبة أخرى، فضع المكتبة المعتِمدة قبل المكتبة المستقلة: g++ main.cpp -lchild-lib -lbase-lib أو اترك الرابط (linker) يتكفّل بتحديد الترتيب عبر الخيارات ‎--start-group‎ و ‎--end-group‎ (ملاحظة هذا له تكلفة كبيرة على الأداء): g++ main.cpp -Wl,--start-group -lbase-lib -lchild-lib -Wl,--end-group التصريف باستخدام Visual Studio (واجهة رسومية) نزِّل Visual Studio Community 2015 وثبِّته. افتح Visual Studio Community . انقر على File، ثم NewK ثم Project. انقر على Templates ثم ++Visual C ثم Win32 Console Application، ثم ضع اسمًا للمشروع وليكن MyFirstProgram. انقر على Ok. انقر على "Next " في النافذة التالية. اختر الخانة ‎Empty project‎ تحت الخيارات الإضافية (Additional options) ثم انقر على "Finish": انقر بالزر الأيمن فوق مجلد ملف المصدر ثم ‎Add، ثم New Item: حدّد ملف C++‎ وقم بتسمية main.cpp، ثم انقر فوق Add: انسخ والصق الشيفرة التالية في الملف الجديد main.cpp: #include <iostream> int main() { std::cout << "Hello World!\n"; return 0; } ينبغي أن تبدو بيئة العمل لديك كما يلي: انقر على Debug ثم Start Without Debugging، (أو اضغط على ctrl + F5): يجب أن تحصل على الخرج التالي في سطر الأوامر: مُصرِّفات الشبكة Online Compilers توفّر العديد من المواقع مصرّفات C++‎ يمكن الوصول إليها عبر شبكة الإنترنت، وتختلف مزايا وإمكانيات مصرّفات الشبكة من موقع إلى آخر، ولكنها عادة ما تسمح بالقيام بما يلي: لصق الشيفرة في نموذج في المتصفّح. تحديد بعض خيارات المصرّف، وتصريف الشيفرة. الحصول على خرج المصرّف و/أو البرنامج. عادةً ما تكون المُصرِّفات الشبكية مقيّدة إذ أنّها تتيح لأيّ شخص تشغيل المصرّف وتنفيذ شيفرات عشوائية على الخادم، وهذا قد يكون مصدر خطر في حال تنفيذ شيفرات ضارة. قد تكون مصرّفات الشبكة مفيدة للأغراض التالية: تشغيل مقتطف صغير من شيفرة جهاز يفتقر إلى مصرّف C++‎ (مثل الهواتف الذكية والأجهزة اللوحية وما إلى ذلك). التحقّق من أنّ الشيفرة تُصرَّف بنجاح في مختلف المُصرِّفات، وتعمل بنفس الطريقة بغض النظر عن المُصرِّف الذي صُرِّفت فيه. تعلُّم أو تعليم أساسيات C++‎. تعلّم ميزات C++‎ الحديثة (C++‎ 14 و C++‎ 17 في المستقبل القريب) إذا لم يكن لديك مصرّف C++‎ حديث على جهازك. رصد الأخطاء التي يمكن أن تكون موجودة في المصرّف الذي تعمل به بمقارنته بمجموعة كبيرة من المصرّفات الأخرى. التحقق ممّا إذا كانت الإصدارات اللاحقة من المصرّف الذي تعمل قد صُحِّحت في حال لم تتوفر تلك الإصدارات على جهازك. حل المشاكل والتمارين عبر الشبكة. بالمقابل، لا ينبغي استخدام مُصرِّفات الشبكة لأجل: تطوير برامج كاملة (ولو كانت صغيرة) باستخدام C++‎، وفي العادةً فإن مصرّفات الشبكة لا تسمح بالارتباط مع مكتبات خارجية، أو تنزيل أدوات البناء. إجراء حسابات مكثفة. موارد الخادم الحسابية محدودة، لذلك سيوقَف أيّ برنامج يقدّمه المستخدم بعد بضع ثوانٍ من بدء تنفيذه، فوقت التنفيذ المسموح به يكفي عادة للاختبار والتعلم فقط. قرصنة ومهاجمة الخادم الذي يعمل عليه المُصرِّف، أو أيّ جهة خارجية مُستضافة على الشبكة. أمثلة على المصرفات الشبكية: codepad.org: مُصرِّف مع إمكانية مشاركة الشيفرة، وتحرير الشيفرة بعد التصريف، لكن التحذيرات و الأخطاء لا تعمل بشكل جيد. coliru.stacked-crooked.com: مصرّف يمكّنك من أن تحدّد سطر الأوامر، ويتيح لك اختيار أحد المُصرِّفين GCC أو Clang. cpp.sh: مُصرِّف يدعم C++‎ 14، لا يسمح لك بتحرير سطر أوامر المصرّف، ولكن يوفّر بعض الخيارات عبر عناصر التحكم في واجهة المستخدم الرسومية. gcc.godbolt.org: يوفر قائمة واسعة من إصدارات المُصرِّف، والمعماريات، وهو مفيد للغاية لمن يحتاج إلى التحقّق من عمل الشيفرة في عدّة مُصرِّفات مختلفة. والمصرفات التالية هي بعض المصرّفات المتاحة: GCC و Clang و MSVC ومصرّف Intel و ELLCC و Zapcc، مع توفّر واحد أو أكثر من هذه المصرّفات للمعماريّات ARM و ARMv8 (مثلARM64) و Atmel AVR و MIPS و MIPS64 و MSP430 و PowerPC و x86 و x64. أيضًا، يمكن تعديل وسائط سطر الأوامر الخاص بالمُصرِّف. ideone.com: يُستخدم على نطاق واسع لتوضيح سلوك الشيفرة، ويوفر كلًّا من GCC و Clang، لكنّه لا يسمح بتحرير سطر أوامر المصرّف. melpon.org/wandbox: يدعم العديد من إصدارات Clang و GNU / GCC. onlinegdb.com: بيئة تطوير متكاملة لكن محدودة، تتضمّن محررًا ومُصرِّفًا (gcc) ومنقّحًا (gdb). rextester.com: يوفر المُصرِّفات Clang و GCC و Visual Studio لكل من C و C++‎ (إضافة إلى مُصرِّفات خاصّة بلغات أخرى)، ومكتبة Boost. tutorialspoint.com/compile_cpp11_online.php: صدفة UNIX متكاملة، مع مصرّف GCC، ومستكشف مشاريع سهل الاستخدام. webcompiler.cloudapp.net: مُصرِّف لبرنامج Visual Studio 2015، مُقدّم من قبل Microsoft كجزء من RiSE4fun. التصريف باستخدام Visual C++‎ (سطر الأوامر) بالنسبة للمبرمجين الذين اعتادوا على العمل بالمصرّفَين GCC أو Clang، والذين يودّون الانتقال إلى Visual Studio، أو المبرمجين الذين يفضّلون العمل بسطر الأوامر بشكل عام، يمكن استخدام مصرّف Visual C++‎ من سطر الأوامر إلى جانب بيئة تطوير متكاملة (IDE). وإذا كنت ترغب في تصريف شيفرتك من سطر الأوامر في Visual Studio، فسيكون عليك إعداد بيئة سطر الأوامر، عن طريق فتح: Visual Studio Command Prompt/Developer Command Prompt/x86 Native Tools Command Prompt/x64 Native Tools Command Prompt أو شيئٍا من هذا القبيل (يختلف الأمر حسب إصدار Visual Studio الذي تعمل به)، أو في موجّه الأوامر، من خلال الانتقال إلى المجلّد الفرعي ‎VC‎ في مجلّد التثبيت الخاصّ بالمصرّف (يكون عادةً ‎\Program Files (x86)\Microsoft Visual Studio x\VC‎، حيث يمثّل ‎x‎ رقم الإصدار (مثل ‎10.0‎ لعام 2010، أو ‎14.0‎ لعام 2015)، ثمّ تنفيذ الملفّ ‎VCVARSALL‎ مع مُعامل سطر الأوامر المُحدّد هنا. لاحظ أنّه على عكس GCC، فإنّ Visual Studio لا يوفّر واجهة للرابط (‎link.exe‎) عبر المصرّف (‎cl.exe‎)، ولكنّها توفّر الرابط (linker) كبرنامج منفصل، ويستدعيه االمُصرِّف عند الإنهاء. ويمكن استخدام ‎cl.exe‎ و ‎link.exe‎ بشكل منفصل مع عدّة ملفّات وخيارات، أو إخبار ‎cl‎ بتمرير الملفّات والخيارات إلى ‎link‎ إذا كانت المهمّتان ستُنجزان معًا، وستُترجم خيارات الربط المُحدّدة لـ ‎cl‎ إلى خيارات خاصة بـ ‎link‎، وتُمرّر الملفات غير المُعالجة بواسطة ‎cl‎ مباشرة إلى الرابط ‎link‎. انظر هذه الصفحة للمزيد عن وسائط ‎link‎. لاحظ أنّ الوسائط التي تخصّ ‎cl‎ حسّاسة لحالة الأحرف، وذلك على خلاف وسائط ‎link‎، وأن بعض الأمثلة التالية تستخدم المتغيّر "current directory" الخاصّ بصدفة (shell) ويندوز، ‎%cd%‎، عند تحديد أسماء المسار المطلق (absolute path names)، ويُوسَّع هذا المتغيّر إلى مجلّد العمل الحالي (current working directory). وفي سطر الأوامر، سيكون هو المجلّد الذي كنت تستخدمه عند تشغيل ‎cl‎، وهو محدَّد في موجّه الأوامر - command prompt - افتراضيًا (مثلًا، في موجّه الأوامر ‎C:\src>‎، فإنّ ‎%cd%‎ سيكون ‎C:\src\‎). يمكنك تصريف وربط ملف تنفيذي غير مُحسَّن (unoptimised executable) بافتراض أنّ هناك ملفّا مصدريًا واحدًا يُسمّى ‎main.cpp‎ في المجلد الحالي، وهذا مفيد في مرحلة التطوير الأولي والتنقيح، عبر استخدام أيٍّ ممّا يلي: cl main.cpp // "main.obj" إنشاء ملف // "main.obj" إجراء الربط مع // "main.exe" إنشاء الملف القابل للتنفيذ cl /Od main.cpp في المثال السابق، سيتصرف cl /Od main.cpp كسابقه، لكن يكون "Od/" هو خيار "Optimisation: disabled"، ويكون الخيار الافتراضي عند عدم تحديد أي من خيارات "O/". بافتراض أنّ هناك ملفًّا مصدريًا إضافيًا " niam.cpp " في نفس المجلّد، فستنشئ الشيفرة التالية الملفين "main.obj" و "niam.obj"، ثم يجري الربط معهما، ومن ثم ينشئ الملف التنفيذي "main.exe": cl main.cpp niam.cpp يمكنك أيضًا استخدام محارف البدل (wildcards)، ستنشئ الشيفرة التالية ملف كائن "main.obj" إضافة إلى ملف كائن لكل ملف cpp. في المجلد التالي: "%cd%\src" ثم تُجري الربط مع "main.obj" وكل ملف كائن مُنشأ، وستكون كل ملفات الكائن في المجلد الحالي، ثم تولِّد ملف main.exe. cl main.cpp src\* .cpp لإعادة تسمية الملف القابل للتنفيذ أو نقله، استخدم أحد الخيارات التالية: cl /o name main.cpp // "name.exe" تولد ملف قابل للتنفيذ cl /o folder\ main.cpp // "%cd%\folder" في المجلد "main.exe" إنشاء ملف cl /o folder\name main.cpp // "%cd%\folder" في المجلد "main.exe" إنشاء ملف cl /Fename main.cpp // "/o name" مثل cl /Fename main.cpp // "/o folder\" مثل cl /Fefolder\name main.cpp // "/o folder\name" مثل يمرِّر كلٌّ من ‎/o‎ و ‎/Fe‎ معاملاتِهما (دعنا نسميها ‎o-param‎) إلى ‎link‎ على شكل ‎/OUT:o-param‎، مع إلحاق الامتداد المناسب (بشكل عام ‎.exe‎ أو ‎.dll‎) لتسمية ‎o-param‎ بما يُناسب، في حين أنّ لـ ‎/o‎ و ‎/Fe‎ نفس الوظيفية - على حدّ علمي - إلّا أنّ الأخير هو المفضل في Visual Studio. لقد أصبحت ‎/o‎ مهمَلة، ويبدو أنّها تًقدّم بشكل أساسي للمبرمجين الذين اعتادوا العمل بالمصرّفَين GCC و Clang. لاحظ أنّه على الرغم من أنّ المسافة الفارغة بين ‎/o‎ واسم المجلد المحدّد اختيارية، إلا أنه لا يمكن وضع مسافة بيضاء بين ‎/Fe‎ واسم المجلد المحدّد. بالمثل، يمكن إنشاء ملف تنفيذي مُحسّن (لاستخدامه في مرحلة الإنتاج) على النحو التالي: سنحسِّن حجم الملف التنفيذي في السطر الأول لكن قد يكون هذا على حساب سرعة التنفيذ، أما في السطر الثاني فيحسِّن سرعة البرنامج، لكن ربما يجعل هذا حجم الملف أكبر. cl /O1 main.cpp cl /O2 main.cpp هنا ننشئ ملفات خاصة لتحسين البرنامج، مما يسمح لسطر الأوامر أن يأخذ كل وحدات الترجمة في حسبانه خلال عملية التحسين، وسيمرر خيار توليد شيفرة الرابط-الوقت (Link-Time Code Generation) أو LTCG/، إلى الرابط LINK، ليخبره باستدعاء سطر الأوامر خلال مرحلة الربط لإجراء تحسينات إضافية. وينبغي ربط كائن الملف المُنشأ مع LTCG/ في حال عدم إجراء الربط في ذلك الوقت، كما يمكن استخدامها مع خيارات التحسين الأخرى في سطر الأوامر، … cl /GL main.cpp other.cpp أخيرًا، لإنتاج ملف تنفيذي مُحسَّن لأجل منصّة معيّنة (لاستخدامه في الإنتاج على جهاز ذي معمارية معيّنة)، فعليك اختيار موجّه الأوامر المناسب، أو مُعامل ‎VCVARSALL‎ المناسب للمنصّة المُستهدفة. ويجب أن يرصُد الرابط (‎link‎) المنصّة المطلوبة من ملفّات الكائن؛ وإلّا، فعليك استخدام الخيار ‎/MACHINE‎ لتعريف المنصّة المُستهدفة بشكل صريح. مثال: إذا أردت التصريف للتنفيذ على معمارية 64 بت، وتعذر على الرابط LINK أي يرى المنصة المستهدفة: cl main.cpp / link / machine: X64 سينتج أيًا مما سبق ملفًا تنفيذيًا يحمل الاسم المحدّد من قِبل ‎/o‎ أو ‎/Fe‎، وفي حالة عدم توفير أيّ منهما، سيحمل اسمًا مطابقًا للملف المصدري الأول أو ملفِّ الكائن المحدّد للمُصرِّف. cl a.cpp b.cpp c.cpp // "a.exe" إنشاء cl d.obj a.cpp q.cpp // "d.exe" إنشاء cl y.lib n.cpp o.obj // "n.exe" إنشاء cl / o yo zp.obj pz.cpp // "yo.exe" إنشاء لتصريف الملفّات دون ربط: cl / c main.cpp // "main.obj" إنشاء ملف تخبر الشيفرةُ السابقة سطرَ الأوامر ‎cl‎ بالخروج دون استدعاء ‎link‎، وتنتج ملفّ كائن يمكن ربطه لاحقًا بالملفّات الأخرى لإنتاج ملف ثنائي. في الشيفرة التالية، ينشئ السطر الأول ملف الكائن niam.obj، ويجري الربط مع niam.obj و main.obj، ثم ينشئ الملف التنفيذي main.exe. ويجري السطر الثاني ربطًا مع niam.obj و main.obj، ثم ينشئ الملف التنفيذي main.exe. cl main.obj niam.cpp link main.obj niam.obj هناك مُعاملات أخرى مهمّة لسطر الأوامر، ومن المفيد جدًا معرفتها، منها ما يلي: cl /EHsc main.cpp: يشير EHsc إلى أنه لن تُمسك إلا اعتراضات ++C القياسية، وأن دوال C الخارجية لن ترفع اعتراضات (exceptions). يوصى بهذا المعامل لمن يريد كتابة شيفرة محمولة ومتعددة المنصات. cl /clr main.cpp: يشير clr/ إلى أن الشيفرة يجب أن تُصرَّف لاستخدام اللغة المشتركة لوقت التشغيل، وهي آلة وهمية خاصة بإطار عمل NET.، وتسمح باستخدام لغة C++/CLI الخاصة بميكروسوفت إضافة إلى سطر أوامر ++C، وتنشئ ملفًا تنفيذيًا يتطلب NET. ليعمل. cl /Za main.cpp: يشير Za/ إلى وجوب تعطيل إضافات (Extensions) ميكروسوفت، وأن الشيفرة يجب أن تُصرَّف وفق مواصفات ISO للغة ++C حصرًا، وهذا ضروري لضمان محمولية البرنامج. cl /Zi main.cpp: يولد Zi/ ملف قاعدة بيانات PDB للبرنامج من أجل استخدامه عند تنقيح برنامج ما دون التأثير على مواصفات التحسين، ويُمرَّر خيار DEBUG/ إلى الرابط LINK. cl /LD main.cpp: يخبر LD/ سطرَ الأوامر أن يضبط LINK كي يولد ملف DLL بدلًا من ملف تنفيذي، وسينتج الرابط ملف DLL إضافة إلى ملفي EXP و LIB لاستخدامهما عند الربط. مرر ملف LIB المرتبط بملف DLL إلى سطر الأوامر أو الرابط عند تصريف تلك البرامج، وذلك لاستخدام الأخير في برامج أخرى. cl main.cpp /link /LINKER_OPTION: يمرر link/ كل ما بعده إلى الرابط مباشرة دون تحليل. استبدل LINKER_OPTION/ بخيارات الرابط التي تريد. بالنّسبة للذين لديهم خبرة في التعامل مع اليونكسات وأشباهها، و/أو GCC / Clang و ‎cl‎ و ‎link‎، وأدوات سطر أوامر Visual Studio الأخرى، يمكنهم أن يقبلوا المُعاملات المُحدّدة باستخدام الواصلة - (مثل ‎-c‎) بدلًا من الشرطة المائلة (مثل ‎/c‎). إضافة إلى ذلك، يتعرّف نظام ويندوز على الشرطة المائلة العكسية \ (backslash) أو الأمامية / (slash) ويعدّهما فواصل صالحة للمسارات، لذلك يمكن استخدام المسارات التي على نمط يونكس وما شابهه. يسهّل هذا تحويل أسطر أوامر المصرّف من ‎g++‎ أو ‎clang++‎ إلى ‎cl‎، أو العكس، بالحد الأدنى من التغييرات. g++ - o app src / main.cpp cl - o app src / main.cpp ستحتاج إلى البحث عن أوامر مكافئة في توثيق المصرّف الذي تستخدمه و/أو في مواقع توثيق أخرى، وذلك عند محاولة نقل سطور الأوامر التي تستخدم خيارات معقّدة لـ ‎g++‎ أو ‎clang++‎. وإن احتجت إلى استخدام ميزات لغة معيّنة في شيفرتك، فيلزم استخدام إصدار محدّد من MSVC. ومن الممكن في Visual C++‎ 2015 التحديث 3 اختيار إصدار المعيار الذي تريد اعتماده عند التصريف عبر الراية ‎/std‎، والقيمُ الممكنةُ هي ‎/std:c++14‎ و ‎/std:c++latest‎ (ستتبعُهما ‎/std:c++17‎ قريبًا). ملاحظة في الإصدارات القديمة من هذا المُصرِّف، كانت بعض رايات المزايا متوفرة، بيْد أنّها كانت في الغالب تُستخدم لمعاينة الميزات الجديدة. التصريف باستخدام Clang بما أن الواجهة الأمامية لمصرّف Clang مُصمّمة لتكون متوافقة مع GCC، فإنّ معظم البرامج التي يمكن تصريفها عبر GCC ستُصرَّف كذلك عند استبدال ‎clang++‎ بـ‎g++‎ في برامج البناء النصية (build scripts)، وفي حال عدم إعطاء ‎-std=version‎، فسيُستخدَم الإصدار gnu11. يمكن لمستخدمي ويندوز الذين اعتادوا على MSVC استبدال ‎clang-cl.exe‎ بـ ‎cl.exe‎. واعلم أن clang يحاول بشكل افتراضي أن يكون متوافقًا مع أحدث إصدار مُثبّت من MSVC. ويمكن استخدام clang-cl عبر تغيير ‎Platform toolset‎ في خاصّيات المشروع، وذلك عند التصريف باستخدام visual studio. وفي كلتا الحالتين، سيكون clang متوافقًا عبر واجهته الأمامية وحسب، لكنه سيحاول أيضًا بناء ملفات ثنائية متوافقة. كذلك يجب على مستخدمي clang-cl أن ينتبهوا إلى أنّ التوافق مع MSVC ليس كاملًا. لاستخدام clang أو clang-cl، يمكن استخدام التثبيت الافتراضي على توزيعات محدّدة من Linux، أو تلك المُجمَّعة في بيئات التطوير المتكاملة - IDEs - (مثل XCode على Mac). أما بالنسبة للإصدارات الأخرى من هذا المُصرِّف، أو في المنصّات التي لم يُثبّت عليها، فيمكن تنزيله من صفحة التنزيل الرسمية. إذا كنت تستخدم CMake لبناء شيفرتك، فيمكنك تبديل االمُصرِّف عادة عن طريق تعيين متغيّرَي البيئة ‎CC‎ و ‎CXX‎ على النحو التالي: mkdir build cd build CC=clang CXX=clang++ cmake .. cmake --build . عملية التصريف في C++‎ بعد تطوير برنامج بلغة C++‎، فإنّ الخطوة التالية هي تصريف ذلك البرنامج قبل تشغيله. والتصريف (compiling) هو العملية التي تحوّل البرنامج المكتوب بلغة قابلة للقراءة البشرية، مثل C و C++‎ وغيرهما، إلى شيفرة الآلة، وهي شيفرة يمكن أن تُفهمها وحدة المعالجة المركزية مباشرة. على سبيل المثال، إذا كان لديك ملفّ مصدَري مكتوب بلغة C++‎ باسم prog.cpp، وقمت بتنفيذ الأمر compile … g++ -Wall -ansi -o prog prog.cpp ستحدث 4 مراحل رئيسية عند إنشاء ملف تنفيذي من الملف المصدري. يأخذ معالج C++‎ الأولي ملفّ الشيفرة المصدرية ويتعامل مع الترويسات (include#)، ووحدات الماكرو (‎#define‎) وموجّهات المعالج الأخرى. تُصرّف شيفرة C++‎ المصدرية المُوسّعة التي تم إنتاجها من قبل معالج C++‎ الأوّلي إلى لغة التجميع (assembly) المناسبة للمنصّة. تُصرَّف الشيفرة المجمّعة التي تم إنتاجها بواسطة المصرّف إلى كائن شيفرة (object code) مناسب للمنصة. يُربط ملفّ كائن الشيفرة المُنتج من قبل المجمّع مع ملفّات كائنات الشيفرة الخاصة بمكتبات الدوال المُستخدمة، وذلك لإنتاج مكتبة أو ملف قابل للتنفيذ. المعالجة الأولية Preprocessing يُعالج المعالج الأولي تعليمات المعالج، مثل ‎#include و ‎#define، وهو غير واعٍ بصيغة C++‎، لذا يجب توخّي الحذر عند استخدامه. يعمل المعالج الأوّلي على ملفّ ++C مصدري واحد عن طريق استبدال محتوى الملفّات المناسبة (والتي تكون عادة مجرّد تصريحات) بتعليمات ‎#include، ويستبدل وحدات الماكرو (‎#define)، ويختار عدة أجزاء من النص بحسب تعليمات ‎#if و ‎#ifdef و ‎#fndef. يعمل المعالج الأوّلي على مجموعة من وحدات المعالجة الأوّلية (preprocessing tokens)، ويُعُرِّف استبدال الماكرو على أنّه استبدال مقاطع بمقاطع أخرى (يتيح المُعامل ## دمجَ شيفرَتين إذا كان ذلك مناسبًا). بعد كل هذا، يُنتج المعالج الأوّلي خرجًا واحدًا يمثّل مجرى من المقاطع الناتجة عن التحويلات الموضّحة أعلاه، ويضيف أيضًا بعض العلامات/الوسوم الخاصة التي تخبر المُصرِّف بالسطر الذي جاء منه كل مقطع، حتّى يتمكّن من صياغة رسائل الخطأ المناسبة. قد تنتج بعض الأخطاء في هذه المرحلة عند استخدام التعليمتين #if و ‎#error. ونستطيع إيقاف العملية في مرحلة المعالجة الأولية باستخدام راية المصرّف أدناه: g++ -E prog.cpp التصريف تُجرى خطوة التصريف على كل خرج من مخرجات المعالج الأوّلي (Preprocessor)، ويحلّل االمُصرِّف شيفرة C++‎ المصدرية الخالصة - دون أي تعليمات للمعالج الأولي الآن- ثمّ يحوّلها إلى شيفرة المجمّع، ثم يستدعي الواجهة الخلفية الأساسية (underlying backend)، وهي المجمّع في سلسلة الأدوات، التي تجمّع تلك الشيفرة وتحوّلها إلى شيفرة الآلة، وتنتج ملفًّا ثنائيًا وفق تنسيق معيّن (مثل ELF أو COFF أو a.out …). تحتوي ملفات الكائنات على الشيفرة المُصرَّفة (في شكل ثنائي) للرموز المُعرَّفة في المدخلات، ويشار إلى الرموز الموجودة في ملف الكائن بالاسم. وتستطيع ملفات الكائنات أن تشير إلى الرموز التي لم تُعرِّف بعد، كأن تستخدم تصريحًا بدون توفير تعريف له، فلا يمانع المُصرِّف ذلك، وسينتج ملفَّ الكائن بلا مشاكل طالما أنّ الشيفرة المصدرية مصاغة بشكل جيّد. تسمح لك المصرّفات عادةً بإيقاف عملية التصريف في هذه المرحلة، وهو أمر مفيد للغاية إذ يُمكِّنك من تصريف كل ملفّ مصدري بشكل منفصل، وعليه فلن تكون بحاجة إلى إعادة تصريف كل شيء إن لم تكن غيرت إلا ملفًّا واحدًا فقط. يمكن وضع ملفات الكائنات الناتجة في أرشيفات خاصة تسمّى المكتبات الساكنة (static libraries)، لتسهيل استخدامها لاحقًا، ويُبلَّغ في هذه المرحلة عن أخطاء المصرّف "المعتادة"، مثل أخطاء الصيغة، أو أخطاء فشل تحليل التحميل الزائد. لإيقاف العملية بعد خطوة التصريف، يمكن استخدام الخيار ‎-S: g++ -Wall -ansi -S prog.cpp التجميع Assembling ينشئ المجمّع شيفرة الكائن، وقد ترى على نظام UNIX ملفّات ذات لاحقة ‎.o (أو .OBJ على MSDOS) للدلالة إلى ملفات كائنات الشيفرَة (object code files). في هذه المرحلة، يحوّل المجمّع تلك الملفّات من شيفرة مُجمَّعة (assembly code) إلى تعليمات على مستوى الآلة، كما سيكون الملفُّ المُنشأ كائنَ شيفرة قابلة للنقل (relocatable object code)، ومن ثم ستنشئ مرحلةُ التصريف برنامجَ الكائن القابل للنقل، ويمكن استخدام هذا البرنامج في مواضع أخرى دون الحاجة إلى إعادة تصريفه مرّة أخرى. لإيقاف العملية بعد خطوة التجميع، استخدم الخيار‎-c: g++ -Wall -ansi -c prog.cpp الربط Linking الرابط (Linker) هو الذي ينتج مخرجات التصريف النهائية من كائنات الملفّات التي أنتجها المجمّع (Assembler)، وقد تكون المُخرجات إمّا مكتبة مشتركة (أو ديناميكية، رغم تشابه الأسماء، إلا أنّها تختلف عن المكتبات الساكنة المذكورة آنفًا)، أو يمكن أن تكون ملفّا تنفيذيًا. ويربط جميع الكائنات عن طريق استبدال العناوين الصحيحة بالمراجع التي تشير إلى رموز غير مُعرَّفة (undefined symbols). يمكن تعريف تلك الرموز في ملفّات كائنات أخرى أو في مكتبات أخرى، وإذا عُرِّفت في مكتبات غير المكتبة القياسية، فعليك إخبار الرابط بذلك. وتكون الأخطاء الأكثر شيوعًا في هذه المرحلة هي التعاريف المفقودة أو التعاريف المكرّرة، والأولى تعني أنّه إمّا أنّ التعاريف غير موجودة (أي أنّها غير مكتوبة)، أو أنّ ملفات الكائنات أو المكتبات التي عُرِّفت فيها لم تُعط للرابط، أمّا الأخطاء الأخيرة فهي واضحة: إذ أنّها تعني أنّ نفس الرمز قد عُرِّف في ملفّين أو مكتبتين مختلفتين. التصريف عبر Code::Blocks (واجهة رسومية) نزِّل Code::Blocks وثبِّته، وإذا كنت تستخدم ويندوز فاحرص على اختيار الملفّ الذي يحتوي اسمُه على ‎mingw‎، إذ لا تثبّت الملفّات الأخرى أي مُصرِّفات. افتح Code::Blocks وانقر على "Create a new project": اختر "Console application" وانقر على "Go": انقر على"Next"، واختر "C++‎"، انقر على "Next"، اختر اسمًا لمشروعك، واختر مجلدًا لحفظه، ثمّ انقر على "Next" ثمّ على "Finish". يمكنك الآن تعديل وتصريف شيفرتك، ستجد شيفرة افتراضية تطبع السلسلة النصية "Hello world!" في سطر الأوامر. اضغط على أحد الأزرار الثلاثة compile/run في شريط الأدواتل تصريف و/أو تشغيل البرنامج: للتصريف دون تشغيل، اضغط على ، وللتشغيل دون التصريف مرّة أخرى، اضغط على ، وللتصريف ثمّ التشغيل اضغط على . تصريف وتشغيل البرنامج الافتراضي "مرحبا العالم!" سيُنتِج الخرج التالي: مواصفات الربط Linkage specifications تخبر مواصفات الربط (linkage specification) المُصرِّفَ بأن يصرِّف التصريحات بطريقة تسمح بربطها بالتصريحات المكتوبة بلغة أخرى، مثل C. معالج الإشارات الخاصّ بأنظمة التشغيل المشابهة ليونيكس بما أن معالج الإشارة (signal handler) يُستدعى عن طريق النواة (kernel) باستخدام صيغة الاستدعاء في لغة C، فيجب أن نطلب من االمُصرِّف أن يستخدم تلك الصيغة عند تصريف الدالّة. volatile sig_atomic_t death_signal = 0; extern "C" void cleanup(int signum) { death_signal = signum; } int main() { bind(...); listen(...); signal(SIGTERM, cleanup); while (int fd = accept(...)) { if (fd == -1 && errno == EINTR && death_signal) { printf("Caught signal %d; shutting down\n", death_signal); break; } // ... } } إنشاء مكتبة بلغة C متوافقة مع C++‎ يمكن في العادةً تضمين ترويسات مكتبة C في برامج C++‎، لأنّ معظم التصريحات صالحة في كلا اللغتين. على سبيل المثال، انظر ملف ‎foo.h‎ التالي: typedef struct Foo { int bar; } Foo; Foo make_foo(int); سيصرَّف تعريف ‎make_foo‎ بشكل منفصل ويُوزّع مع الترويسة على هيئة كائن. يمكن لبرنامج C++‎ أن يُضمِّن ملف الترويسة ‎foo.h‎ عبر التعبير ‎#include <foo.h>‎، لكن لن يستطيع المُصرِّف أن يعرف أنّ الدالّة ‎make_foo‎ قد عُرِّفت وفق صيغة مكتوبة بلغة C، وسيحاول البحث عنه باسم آخر، وسيفشل في إيجاده. وحتى لو استطاع إيجاد تعريف ‎make_foo‎ في المكتبة، فليست كل المنصّات تستخدم نفس صيغات الاستدعاء في C و C++‎، وسوف يستخدم مصرّف C++‎ صيغة الاستدعاء الخاصة بـ C++‎ عند استدعاء ‎make_foo‎، وهذا من شأنه أن يتسبّب في حدوث خطأ تجزئة (segmentation fault) في حال كان يُتوقّع أن تُستدعى ‎make_foo‎ وفق صيغة الاستدعاء في C. يمكن حلّ هذه المشكلة عبر تغليف (wrap) كافة التصريحات في ترويسة في كتلة ‎extern "C"‎، انظر .. #ifdef __cplusplus extern "C" { #endif typedef struct Foo { int bar; } Foo; Foo make_foo(int); #ifdef __cplusplus } /* "extern C" نهاية الكتلة */ #endif والآن، عند تضمين ‎foo.h‎ من برنامج C، سيظهر كتصريح عادي، أمّا عند تضمين ‎foo.h‎ من برنامج مكتوب بلغة C++‎، فسيكون ‎make_foo‎ داخل كتلة ‎extern "C"‎، وسيعرف المصرّفُ كيف يبحث عن الأسماء، وسيستخدم صيغة الاستدعاء في C. هذا الدرس جزء من سلسلة دروس عن C++‎. ترجمة -بتصرّف- للفصل Chapter 138: Compiling and Building والفصل Chapter 134: Linkage specifications من كتاب C++ Notes for Professionals
  5. محدّدات أصناف التخزين هي كلمات مفتاحية يمكن استخدامها في التصريحات، ولا تؤثر على نوع التصريح لكنها تعدّل الطريقة التي تُخزّن بها الكيانات. extern محدّدُ صنف التخزين ‎extern‎ يستطيع التصريحَ بإحدى الطرق الثلاث التالية، وذلك وفقًا للسياق، فمثلًا: يمكن استخدامه للتصريح عن متغيّر دون تعريفه، يُستخدَم عادة في ملفات الترويسات الخاصة بالمتغيّرات التي تُعرّف في ملف تنفيذ منفصل. // نطاق عام int x; // بالقيمة الافتراضية x تعريف: سيهيأ extern int y; // معرَّف في مكان آخر، غالبًا في وحدة ترجمة أخرى y :تصريح. extern int z = 42; // أيّ تأثير هنا "extern" تعريف: ليس للكلمة المفتاحية إعطاء ارتباط خارجي (external linkage) لمتغيّر في نطاق فضاء الاسم، حتى لو كانت الكلمتان المفتاحيتان ‎const‎ و ‎constexpr‎ تتسبّبان في إنشاء ارتباط داخلي. // النطاق العام const int w = 42; // C وخارجي في ،C++ ارتباط داخلي في static const int x = 42; // C++ و C ارتباط داخلي في كل من extern const int y = 42; // C++ و C ارتباط خارجي في كل من namespace { extern const int z = 42; // ارتباط داخلي لأنّه في فضاء اسم غير مُسمّى } إعادة التصريح (redeclaring) عن متغيّر في نطاق الكتلة إن صُرِّح عنه مسبقًا عبر الارتباط (linkage). أما خلاف ذلك، سيُصرّح عن متغيّر جديد عبر الارتباط، وسيكون عضوًا في أقرب فضاء اسم محيط. // النطاق العام namespace { int x = 1; struct C { int x = 2; void f() { extern int x; // x إعادة التصريح عن std::cout << x << '\n'; // طباعة 1 وليس 2 } }; } void g() { extern int y; // العامّة المُعرّفة في موضع آخر y ارتباط خارجي، ويشير إلى قيمة y لدى } يمكن أيضًا التصريح عن الدالّة على أنها ‎extern‎، لكنّ هذا ليس له أيّ تأثير وإنّما يُستخدم في العادةً كتلميح للقارئ بأنّ الدالّة المُصرَّح عنها هنا ستعُرَّف في وحدة ترجمة أخرى. ففي المثال التالي، يكون ()void f تصريحًا لاحقًا يعني أن f ستُعرَّف في وحدة الترجمة هذه، أما ()extern void g ليس تصريحًا لاحقًا، مما يعني أن g تُعرَّف في وحدة ترجمة أخرى، انظر: void f(); extern void g(); في الشيفرة أعلاه، في حال التصريح عن ‎f‎ مع ‎extern‎ والتصريح عن ‎g‎ بدون ‎extern‎، لن يؤثر ذلك على صحة أو دلالات البرنامج، ولكن من المحتمل أن يربك القارئ. register الإصدار < C++‎ 17 ++‎> register هو محدّد صنف تخزين يخبر االمُصرِّف أنّ المتغيّر سيُستخدَم بكثرة، وتأتي الكلمة "register" من حقيقة أن المُصرِّف قد يختار تخزين هذا المتغيّر في سجل وحدة المعالجة المركزية (CPU register) لتسريع الوصول إليه. وقد أُهمِلت بدءًا من الإصدار C++‎ 11. register int i = 0; while (i < 100) { f(i); int g = i * i; i += h(i, g); } يُمكن تعريف كلٍّ من المتغيّرات المحلية ومُعامِلات الدوالّ بالكلمة المفتاحية ‎register‎، ولا تضع C++‎ قيودًا على ما يمكن فعله بالمتغيّرات المعرّفة بـ ‎register‎، على عكس لغة C. فكثلًا، يجوز أخذ عنوان متغيّر مصرّح عنه بـ ‎register‎، لكنّ هذا قد يمنع المُصرِّف من تخزين مثل هذا المتغيّر في السجل (register). الإصدار ≥ C++‎ 17 الكلمة المفتاحية ‎register‎ غير مستخدمة ومحفوظة، ولا ينبغي استخدامها. static لدى محدّد التخزين ‎static‎ ثلاث معانٍ مختلفة. يعطي ارتباطًا داخليًا لمتغيّر أو دالّة مُصرَّح عنها في نطاق فضاء الاسم. // دالة داخلية، لا يمكن الارتباط بها static double semiperimeter(double a, double b, double c) { return (a + b + c) / 2.0; } // التصدير إلى العميل double area(double a, double b, double c) { const double s = semiperimeter(a, b, c); return sqrt(s * (s - a) * (s - b) * (s - c)); } تصرّح بأنّ المتغيّر له مدة تخزين ساكنة - static storage duration - (ما لم تكن خيطًا محليًا ‎thread_local‎)، ومتغيرات نطاق فضاء الاسم تكون ساكنة ضمنيًا، وتُهيّأ المتغيّرات المحلية الساكنة مرّة واحدة فقط، إذ يمرّ التحكّم في المرّة الأولى عبر تعريفها، ولا تُدمَّر بعد الخروج من نطاقها. void f() { static int count = 0; std::cout << "f has been called " << ++count << " times so far\n"; } عند تطبيقها على تصريح عن عضو صنف (class member)، فإنّها تجعل ذلك العضو ساكنًا. struct S { static S* create() { return new S; } }; int main() { S* s = S::create(); } لاحظ أنّه إن كان العضو ساكنًا، ستَنطبق النقطتان 2 و 3 في نفس الوقت، إذ تضع الكلمة المفتاحية ‎static‎ عضو الصنف في عضو بيانات ساكن (static data member)، كما تجعله أيضًا في متغيّر ذي مدة تخزين ساكنة (static storage duration). auto الإصدار ≤ C++‎ 03 يصرّح هذا المحدّد بأنّ المتغيّر لديه مدة تخزين آلية (automatic storage duration)، وهو غير ضروري نظرًا لأنّ مدة التخزين التلقائي هي الإعداد الافتراضي في نطاق الكتلة، كما لا يُسمح باستخدام المحدّد auto في نطاق فضاء الاسم. void f() { auto int x; auto y; auto int z; في المثال السابق، auto int x تكافئ int x، ولا تجوز auto y في ++C لكنها جائزة في C89، كذلك لا تجوز auto int z، إذ لا يمكن أن تكون متغيرات نطاق فضاء الاسم آلية. تغيّر معنى ‎auto‎ في الإصدار C++‎ 11 بشكل كامل، ولم تعد محدّدًا للتخزين، وإنّما صارت تُستخدم في استنتاج الأنواع. mutable mutable هو محدّدٌ يمكن تطبيقه على تصريح عضو بيانات من صنف غير ساكن (non-static) وغير مرجعي (non-reference)، والعضو القابل للتغيير في صنفٍ لا يكون غيرَ ثابتٍ حتى لو كان الكائن ثابتًا. class C { int x; mutable int times_accessed; public: C(): x(0), times_accessed(0) {} int get_x() const { ++times_accessed; // لا بأس: يمكن للدوال التابعة الثابتة أن تعدّل أعضاء البيانات غير الثابتة return x; } void set_x(int x) { ++times_accessed; this -> x = x; } }; الإصدار ≥ C++‎ 11 أضيف معنى ثانٍ للكلمة المفتاحية ‎mutable‎ في C++‎ 11، فعندما تتبع قائمة المُعاملات الخاصة بتعبير لامدا، فإنها تقمع المؤهّل الثباتي ‎const‎ الضمني الموجود في مُعامل استدعاء لامدا (lambda's function call operator). ولذلك يمكن لتعابير لامدا القابلة للتغيير (mutable) أنّ تعدّل قيم الكيانات التي حصلت عليها عن طريق النسخ. std::vector < int > my_iota(int start, int count) { std::vector < int > result(count); std::generate(result.begin(), result.end(), [start]() mutable { return start++; }); return result; } لاحظ أنّ ‎mutable‎ لا تُعدُّ محدّدَ صنف تخزين إذا استخدِمَت بهذه الطريقة لتشكيل تعابير لامدا تقبل للتغيير (أي mutable). هذا الدرس جزء من سلسلة دروس عن C++‎. ترجمة -بتصرّف- للفصل Chapter 133: Storage class specifiers من كتاب C++ Notes for Professionals
  6. إدارة الموارد هي إحدى أصعب الأشياء في C و C++‎، لكن C++‎ توفّر لنا العديد من الطرق التي نصمم بها إدارة الموارد في برامجنا، وسنحاول في هذا المقال أن نشرح بعض هذه الطرق. تقنية RAII: تخصيص الموارد يكافئ التهيئة تقنية RAII وتدعى اكتساب أو تخصيص الموارد هي تهيئة (Resource Acquisition Is Initialization) هو مصطلح شائع في إدارة الموارد، ويستخدم المؤشّرات الذكية (smart pointer) لإدارة الموارد في حالة الذاكرة الديناميكية، وتُمنح الموارد المكتسبة ملكية مؤشر ذكي أو مدير موارد مكافئ مباشرة عند استخدام أسلوب RAII. ولا يمكن الوصول إلى المورد إلا من خلال ذلك المدير، مما يمكِّن المدير من تتبّع مختلف العمليات الجارية على المورد. على سبيل المثال، تحرِّر الكائنات من النوع ‎std::auto_ptr‎ مواردها تلقائيًا عندما تخرج عن النطاق، أو عندما تُحذف بطريقة أخرى. #include <memory> #include <iostream> using namespace std; int main() { { auto_ptr ap(new int(5)); cout << *ap << endl; // تطبع 5 } } في المثال السابق، كان المورد هو الذاكرة الديناميكية، ودُمِّر auto-ptr ثم حُرِّر مورده تلقائيًا. الإصدار ≥ C++‎ 11 كانت المشكلة الرئيسية في كائنات std::auto_ptr أنّها لا يمكن أن تُنسخ دون نقل الملكية: #include <memory> #include <iostream> using namespace std; int main() { auto_ptr ap1(new int(5)); cout << *ap1 << endl; // تطبع 5 auto_ptr ap2(ap1); cout << *ap2 << endl; // تطبع 5 cout << ap1 == nullptr << endl; } تفسير الشيفرة السابقة: (auto_ptr ap2(ap1: تنسخ ap2 من ap1 وتنتقل الملكية إلى ap2. cout << ap1 == nullptr << endl: تطبع القيمة 1، ويخسر ap1 ملكيته للمورد. وبسبب دلالات النسخ الغريبة تلك فإنّ هناك قيودًا على استخدام ‎std::auto_ptr‎، مثل أنها لا يمكن أن تُستخدم في الحاويات، وذلك لمنع حذف الذاكرة مرتين: فإذا كان لدينا كائنين من النوع ‎auto_ptrs‎ يملكان نفس المورد، سيحاول كلا الكائنين تحريره عند تدميرهما. ومن المهم منع تحرير المورد الذي سبق تحريره من قبل، إذ سيؤدي ذلك التحرير الثاني إلى حدوث مشاكل، لكن بأي حال فإن std::shared_ptr لديه طريقة لتجنب ذلك، مع عدم نقل الملكية أثناء النسخ. #include <memory> #include <iostream> using namespace std; int main() { shared_ptr sp2; { shared_ptr sp1(new int(5)); // sp1 إعطاء الملكيّة إلى cout << *sp1 << endl // تطبع 5 sp2 = sp1; // يملك كلاهما المورد ،sp1 من sp2 نسخ cout << *sp1 << endl; // تطبع 5 cout << *sp2 << endl; // تطبع 5 } // الملكية الحصرية للمورد sp2 عن النطاق وتدميره، أصبح لـ sp1 خروج cout << *sp2 << endl; } // عن النطاق، وتحرير المورد sp2 خروج القفل Locking هذا مثال عن قفل سيّء: std::mutex mtx; void bad_lock_example() { mtx.lock(); try { foo(); bar(); if (baz()) { mtx.unlock(); // ينبغي فتح القفل عند كل نقطة خروج return; } quux(); mtx.unlock(); // يحدث فتح القفل العادي هنا } catch (...) { mtx.unlock(); // ينبغي فرض فتح القفل في حال طرح اعتراض throw; // والسماح للاعتراض بالاستمرار } } تلك طريقة خاطئة لتنفيذ عمليتي القفل والفتح لكائنات المزامنة (mutex)، ولا بدّ أن يتحقّق المُبرمِجُ من أنّ جميع التدفّقات (flows) الناتجة عن إنهاء الدالّة تؤدّي إلَى استدعاء ‎unlock()‎، وذلك للتأكد أنّ فتح القفل باستخدام ‎unlock()‎ سيحرّر الكائن المزامنة الصحيح. وهذه عمليات هشة كما وضحنا أعلاه، لأنّها تتطّلب من المطوّرين متابعة النمط يدويًا. ويمكن حلّ هذه المشكلة باستخدام صنف مُصمّم خصّيصًا لتنفيذ تقنية RAII: std::mutex mtx; void good_lock_example() { std::lock_guard<std::mutex > lk(mtx); // المنشئ يقفل. // المدمِّر يفتح! // تضمن اللغة استدعاء المدمر. foo(); bar(); if (baz()) { return; } quux(); } ‎lock_guard‎ هو قالب صنف بسيط للغاية يستدعي ‎lock()‎ ويمرّر إليه الوسيط المُمرّر إلى مُنشئه ويحتفظ بمرجع إلى ذلك الوسيط، ثمّ يستدعي ‎unlock()‎ على الوسيط في مدمّره، ممّا يعني ضمان فتح قفل mutex عند خروج ‎lock_guard‎ عن النطاق. ولا يهم سبب الخروج عن النطاق سواءً كان ذلك بسبب اعتراض أو عودة مبكّرة، ذلك أن جميع الحالات سيتمّ التعامل معها، وسيفتح القفل بشكل صحيح بغض النظر عن سير البرنامج. ScopeSuccess الإصدار ≥ C++‎ 17 نستطيع باستخدام ‎int std::uncaught_exceptions()‎ أن ننفذ إجراءً لم يكن لينفَّذ إلّا في حالة النجاح (عدم رفع اعتراض في النطاق). وقد كانت ‎bool std::uncaught_exception()‎ فيما سبق تسمح برصد العمليّات الجارية لفكّ المكدّس (stack unwinding). انظر: #include <exception> #include <iostream> template < typename F> class ScopeSuccess { private: F f; int uncaughtExceptionCount = std::uncaught_exceptions(); public: explicit ScopeSuccess(const F &f): f(f) {} ScopeSuccess(const ScopeSuccess &) = delete; ScopeSuccess &operator=(const ScopeSuccess &) = delete; // f() might throw, as it can be caught normally. ~ScopeSuccess() noexcept(noexcept(f())) { if (uncaughtExceptionCount == std::uncaught_exceptions()) { f(); } } }; struct Foo { ~Foo() { try { ScopeSuccess logSuccess { []() { std::cout << "Success 1\n"; } }; // نجاح النطاق // أثناء فكّ المكدّس Foo حتى في حال تدمير // 0 < std::uncaught_exceptions() // std::uncaught_exception() == true } catch (...) {} try { ScopeSuccess logSuccess { []() { std::cout << "Success 2\n"; } }; تزيد القيمة المعادة من std::uncaught_exceptions، … throw std::runtime_error("Failed"); } وتنقص القيمة المعادة من std::uncaught_exceptions … catch (...) { } } }; int main() { try { Foo foo; throw std::runtime_error("Failed"); // std::uncaught_exceptions() == 1 } catch (...) { // std::uncaught_exceptions() == 0 } } سيكون الخرج: Success 1 ScopeFail ‏(C++‎ 17) الإصدار ≥ C++‎ 17 يمكننا تنفيذ إجراء معيّن لا يُنفَّذ إلّا عند الفشل (رفع اعتراض في النطاق) بفضل ‎int std::uncaught_exceptions()‎، وكانت ‎bool std::uncaught_exception()‎ تسمح سابقًا برصد إن كانت هناك عملية جارية لفك المكدّس. #include <exception> #include <iostream> template < typename F> class ScopeFail { private: F f; int uncaughtExceptionCount = std::uncaught_exceptions(); public: explicit ScopeFail(const F &f): f(f) {} ScopeFail(const ScopeFail &) = delete; ScopeFail &operator=(const ScopeFail &) = delete; يجب ألا ترفع ()f وإلا فستستدعى std::terminate، نتابع المثال … ~ScopeFail() { if (uncaughtExceptionCount != std::uncaught_exceptions()) { f(); } } }; struct Foo { ~Foo() { try { ScopeFail logFailure { []() { std::cout << "Fail 1\n"; } }; // نجاح النطاق // أثناء فكّ المكدّس Foo حتى في حال تدمير // 0 < std::uncaught_exceptions() في حال // std::uncaught_exception() == true أو سابقا } catch (...) {} try { ScopeFail logFailure { []() { std::cout << "Failure 2\n"; } }; تزيد القيمة المعادة من std::uncaught_exceptions …. throw std::runtime_error("Failed"); } تقل القيمة المعادة من std::uncaught_exceptions …. catch (...) { } } }; int main() { try { Foo foo; throw std::runtime_error("Failed"); // std::uncaught_exceptions() == 1 } catch (...) { // std::uncaught_exceptions() == 0 } } سيكون الخرج: Failure 2 Finally/ScopeExit إذا لم ترد كتابة أصناف خاصّة للتعامل مع بعض الموارد، فاكتب صنفًا عامًا على النحو التالي: template < typename Function> class Finally final { public: explicit Finally(Function f): f(std::move(f)) {} ~Finally() { f(); } // (1) انظر أدناه Finally(const Finally &) = delete; Finally(Finally &&) = default; Finally &operator=(const Finally &) = delete; Finally &operator=(Finally &&) = delete; private: Function f; }; // عندما يخرج الكائن المُعاد عن النطاق f تنفيذ الدالة template < typename Function> auto onExit(Function && f) { return Finally<std::decay_t < Function>> { std::forward<Function> (f) }; } وهذا مثال على استخدام ذلك الصنف: void foo(std::vector<int> &v, int i) { // ... v[i] += 42; auto autoRollBackChange = onExit([ &]() { v[i] -= 42; }); // ... `foo(v, i + 1)` شيفرة تكرارية } ملاحظة (1): يجب أخذ الملاحظات التالية حول تعريف المدمّر في الاعتبار عند التعامل مع الاعتراضات: Finally() noexcept { f(); }: std::terminate // تُستدعى في حال رفع اعتراض Finally() noexcept(noexcept(f())) { f(); } // إلّا في حال رفع اعتراض أثناء فك المكدّس terminate() لا تُستدعى Finally() noexcept { try { f(); } catch (...) { /* ignore exception (might log it) */} } ) // لا تُستدعى std::terminate، لكن لا نستطيع معالجة الخطأ (حتى في حالة عدم فك المكدّس كائنات المزامنة وأمان الخيوط Mutexes & Thread Safety قد تحدث مشكلة إن حاولت عدّة خيوط الوصول إلى نفس المورد، فلنفترض مثلًا أنّ لدينا خيطًا يضيف العدد 1 إلى متغيّر. فأوّل شيء يفعله هو أنّه يقرأ المتغيّر، ثم يضيف واحدًا إليه، ثم يعيد تخزينه مرّة أخرى. لنفترض الآن أنّنا هيّأنا ذلك المتغيّر عند القيمة 1، ثم أنشأنا نُسختين من ذلك الخيط، فبعد أن ينتهي كلا الخيطين، نتوقّع أن تكون قيمة ذلك المتغيّر هي 3، لكن هناك بعض المشاكل التي قد تحدث هنا، سنفصلها في الجدول التالي: الخيط 1 الخيط 2 المرحلة 1 قراءة 1 من المتغير المرحلة 2 قراءة 1 من المتغير المرحلة 3 إضافة 1 إلى 1 للحصول على 2 المرحلة 4 إضافة 1 إلى 1 للحصول على 2 المرحلة 5 تخزين 2 في المتغير المرحلة 6 تخزين 2 في المتغير table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; } في نهاية العملية، تُخزّن القيمة 2 في المتغيّر بدلًا من 3، ذلك أن الخيط 2 يقرأ المتغيّر قبل أن يُحدِّث الخيط 1 ذلك المتغيّر. ما الحلّ إذن؟ يكون الحل في كائنات المزامنة … . كائن المزامنة (mutex) هو كائن لإدارة الموارد، ومُصمّم لحل هذا النوع من المشاكل. فعندما يحاول خيط ما الوصول إلى مورد، فإنّه يستحوذ على كائن المزامنة لذلك المورد (resource's mutex). ويحرّر ذلك (releases) الخيطُ كائن المزامنة بمجرّد الانتهاء من العمل على المورد. وعند استحواذ خيط على كائن مزامنةٍ فإنّ كلّ الاستدعاءات للاستحواذ على ذلك الكائن لن تعود إلى أن يُحرَّر. لفهم ذلك بشكل أفضل، فكّر في كائن المزامنة كطابور انتظار في متجر كبير، إذ تنتقل الخيوط في الطابور لمحاولة الاستحواذ على كائن المزامنة، ثم تنتظر الخيوط التي تسبقُها حتى تنتهي، ثم تستخدم المورد، ثم تخرج عن الطابور عن طريق تحرير كائن المزامنة. فلو حَاول الجميع الوصول إلى المورد في نفس الوقت ستحدث فوضى كبيرة. الإصدار ≥ C++‎ 11 المكتبة std::mutexهي تطبيق كائن المزامنة في C++‎ 11. #include <thread> #include <mutex> #include <iostream> using namespace std; void add_1(int& i, const mutex& m) { // الدالة التي ستُنفّذ في الخيط m.lock(); i += 1; m.unlock(); } int main() { int var = 1; mutex m; cout << var << endl; // تطبع 1 thread t1(add_1, var, m); // إنشاء خيط مع وسائط thread t2(add_1, var, m); // إنشاء خيط آخر t1.join(); t2.join(); // انتظار انتهاء الخيطين cout << var << endl; // تطبع 3 } هذا الدرس جزء من سلسلة دروس عن C++‎. ترجمة -بتصرّف- للفصل Chapter 83: RAII: Resource Acquisition Is Initialization والفصل Chapter 132: Resource Management من كتاب C++ Notes for Professionals
  7. لغة C++‎، على غرار C، لها تاريخ طويل ومتنوّع بخصوص سير العمل التصريفي وعمليات البناء. واليوم، لدى لغة C++‎ العديد من أنظمة البناء التي تُستخدَم لتصريف البرامج لعدة منصات أحيانًا داخل نظام بناء واحد. وسننظر في هذا الدرس في بعض أنظمة البناء ونحلّلها. إنشاء بيئة بناء باستخدام CMake ينشئ CMake بيئات البناء للمُصرِّفات أو بيئات التطوير المتكاملة (IDE) انطلاقًا من تعريف مشروع واحد، وسوف توضّح الأمثلة التالية كيفية إضافة ملف CMake إلى برنامج "Hello World" متعدّد المنصات. تأخذ ملفات CMake دائمًا الاسم "CMakeLists.txt"، ويجب أن تكون موجودة في مجلّد الجذر لكل مشروع، وربما في المجلدات الفرعية أيضا. هذا مثال على ملف CMakeLists.txt: cmake_minimum_required(VERSION 2.4) project(HelloWorld) add_executable(HelloWorld main.cpp) يمكنك رؤية البرنامج يعمل مباشرة من هنا. هذا الملفُّ يخبر أداةَ CMake باسم المشروع، وإصدار الملف المتوقع، وبعض التعليمات الضرورية لتوليد ملف تنفيذي (executable) يُسمّى "HelloWorld"، والذي يتطلب ملف ‎main.cpp‎. في المثال التالي: أنشئ بيئة بناء للمُصرِّف أو بيئة تطوير متكاملة من سطر الأوامر: > cmake . ابن التطبيق بما يلي: > cmake --build . تولّد هذه الشيفرة بيئة البناء الافتراضية للنظام وفق نظام التشغيل والأدوات المُثبّتة، حافظ على الشيفرة المصدرية نظيفة من آثار البناء (build artifacts) باستخدام نمط تصميم "خارج المصدر" (out-of-source): > mkdir build > cd build > cmake .. > cmake --build . يمكن لـ CMake أيضًا تجريد (abstract) الأوامر الأساسية للصدفة (shell) الخاصة بالمنصة من المثال السابق: > cmake -E make_directory build > cmake -E chdir build cmake .. > cmake --build build يتضمّن CMake مولدّات لعدد من أدوات البناء الشائعة وبيئات التطوير المتكاملة، ولإنشاء ملفات makefiles لأجل nmake الخاصة بـ Visual Studio: > cmake -G "NMake Makefiles" .. > nmake التصريف باستخدام GNU GNU Make هو برنامج مخصّص لأتمتة تنفيذ أوامر الصدفة، وينتمي لعائلة برامج Make، ويتمتع Make بشعبية كبيرة لدى مستخدمي أنظمة التشغيل الشبيهة بيونكس وتلك الموافقة لمعايير POSIX، بما في ذلك الأنظمة المشتقة من نواة لينُكس ونظام Mac OSX ونظام BSD، ويتميّز GNU Make بأنّه مرتبط بمشروع جنو الذي يرتبط بدوره بأنظمة التشغيل المبنية على جنو\لينكس الشهير. هناك إصدارات أخرى من GNU Make متوافقة مع أنظمة تشغيل أخرى، مثل ويندوز و Mac OS X، وهذا البرنامج مستقر للغاية وله أثر تاريخي يبقيه مشهورًا، لهذا يُدرَّس GNU Make إلى جانب لغتي C و C++‎. القواعد الأساسية يجب أن تنشئ ملف Makefile في مجلّد المشروع لأجل التصريف بواسطة make. هذا مثال بسيط على ملف Makefile: سنقسم الملف أثناء سرد المثال للشرح …. Makefile # إعداد بعض المتغيرات لاستخدامها مع الأمر # g++ أولا، سنحدد المصرف CXX=g++ # وغيرها g++ ثم نصرّف مع التنبيهات الموصى بها في CXXFLAGS=-Wall -Wextra -pedantic # هذا هو ملف الخرج EXE=app SRCS=main.cpp يُستدَعى هذا الهدف عند استدعاء make في سطر الأوامر، وتقول (EXE)$ الموجودة على اليمين أن الهدف all يعتمد على الهدف (EXE)$. لاحظ أنه بما أن هذا هو الهدف الأول، فسيكون الهدف الافتراضي إن استُدعيَت make بدون هدف، نتابع المثال … all: $(EXE) # هذا يكافئ # app: $(SRCS) (SRCS)$ يمكن فصلها، مما يعني أن هذا الهدف سيعتمد على كل الملفات. لاحظ أن هذا الهدف له متن تابع (method body)، وهو الجزء المزاح بمسافة جدول واحدة (tab) وليس أربع مسافات فارغة. وسينفذ make الأمر التالي عند بناء هذا الهدف: g++ -Wall -Wextra -pedantic -o app main.cpp والذي يعني تصريف main.cpp مع التحذيرات، والإخراج إلى الملف app/.، نتابع … $(EXE): $(SRCS) @$(CXX) $(CXXFLAGS) -o $@ $(SRCS) هذا الهدف ينبغي له أن يعكس الهدف all، وإن استدعيْتَ make مع وسيط مثل make clean فسيُستَدعى الهدف gets الموافق له، … clean: @rm -f $(EXE) ملاحظة: تأكد من أن الإزاحات البادئة قد تمت بزر الجدول (tab)، ولا تستخدم 4 مسافات بيضاء من زر Space. وإلا فسيُطلق الخطأ ‎Makefile:10: *** missing separator. Stop.‎. لتنفيذ هذه الشيفرة من سطر الأوامر، عليك كتابة ما يلي: $ cd ~/Path/to/project $ make $ ls app main.cpp Makefile $ ./app Hello World! $ make clean $ ls main.cpp Makefile عمليات البناء التزايدية التزايدي Incremental builds تظهر فائدة make عندما تكثر الملفات، فماذا لو عدّلت الملف a.cpp دون الملف b.cpp؟ لن يكون من الحكمة إذًا أن تعيد تصريف b.cpp، لأنّ ذلك سيُهدر الوقت. في المثال أدناه، لنفرض أن لدينا الهيكل التالي لمجلد: . +-- src | +-- a.cpp | +-- a.hpp | +-- b.cpp | +-- b.hpp +-- Makefile فسيكون هذا ملف Makefile جيد: CXX=g++ CXXFLAGS=-Wall -Wextra -pedantic EXE=app SRCS_GLOB=src/*.cpp SRCS=$(wildcard $(SRCS_GLOB)) OBJS=$(SRCS:.cpp=.o) all: $(EXE) $(EXE): $(OBJS) @$(CXX) -o $@ $(OBJS) depend: .depend .depend: $(SRCS) @-rm -f ./.depend @$(CXX) $(CXXFLAGS) -MM $^>>./.depend clean: -rm -f $(EXE) -rm $(OBJS) -rm *~ -rm .depend include .depend مرّة أخرى، لاحظ إزاحات الجدول البادئة. سيضمن ملفّ Makefile الجديد ألّا تصرّف إلّا الملفات التي عُدِّلت، وهذا سيقلّل وقت التصريف. التوثيق للحصول على المزيد من المعلومات عن make، راجع التوثيق الرسمي -بالإنجليزية- من مؤسسة البرمجيات الحرة وإجابة dmckee التفصيلية على موقع stackoverflow. البناء بواسطة SCons يمكنك بناء شيفرة "Hello World" متعدّدة المنصات باستخدام أداة Scons، وهي أداة بناء برامج تستخدم لغة بايثون. أولاً، عليك إنشاء ملف يُسمّى ‎SConstruct‎ -سيبحث SCons عن هذا الملف افتراضيًا-، وينبغي أن يكون الملف -للآن- في مجلّد مع الملف ‎hello.cpp‎. اكتب السطر التالي في الملف الجديد: Program('hello.cpp') الآن، من سطر الأوامر، نفّذ الأمر ‎scons‎، ينبغي أن ترى شيئًا من هذا القبيل: $ scons scons: Reading SConscript files ... scons: done reading SConscript files. scons: Building targets ... g++ -o hello.o -c hello.cpp g++ -o hello hello.o scons: done building targets. رغم أن التفاصيل ستختلف وفقًا لنظام التشغيل والمُصرِّف المُستخدم. سيساعدك الصنفان ‎Environment‎ و ‎Glob‎ على إعداد ما يجب بناؤه. على سبيل المثال، سيبني الملف ‎SConstruct‎ الملف التنفيذي hello باستخدام جميع ملفات cpp في مجلد src، كما أن CPPPATH الخاص به سيكون ‎/usr/include/boost‎، كما يحدّد معيار C++‎ 11، انظر: env=Environment(CPPPATH='/usr/include/boost/', CPPDEFINES=[], LIBS=[], SCONS_CXX_STANDARD="c++11" ) env.Program('hello', Glob('src/*.cpp')) Autotools هي مجموعة من البرامج التي تُستخدَم لإنشاء نظام بناء جنو (GNU Build System) لأجل حزمة برامج معيّنة، وهي حزمة من الأدوات تعمل معًا لإنتاج العديد من موارد البناء، مثل ملفات Makefile -لتُستخدَم مع برنامج GNU Make، لهذا تُعدُّ هذه الحزمة المولّد المعياري لأنظمة البناء. فيما يلي بعض برامج Autotools الأساسية: Autoconf. Automake (لا تخلط بينها وبين ‎make‎). والهدف من حزمة Autotools بشكل عام، هو توليد برنامج نصّي متوافق مع يونيكس (Unix-compatible script)، إضافة إلى ملفّ Makefile للسماح للأمر التالي بإنجاز عملية البناء -وكذلك تثبيت- معظم الحُزم: ./configure && make && make install وترتبط حزمة Autotools على هذا النحو أيضًا ببعض مدراء الحزم (package managers)، خاصة تلك المرتبطة بأنظمة التشغيل التي تتوافق مع POSIX. Ninja يوصف نظام بناء Ninja في موقعه بأنه "نظام بناء صغير يركّز على السرعة"، وصُمِّم هذا النظام لكي تُبنى ملفاته بواسطة مولدات ملفات بناء النظام (build system file generators)، ويعتمد أسلوبًا منخفض المستوى (low-level) لبناء الأنظمة، وذلك على عكس مدراء أنظمة البناء عالية المستوى مثل CMake أو Meson. كُتِب نظام Ninja أساسًا بلغة C++‎ و Python، وقد أُنشِئ كبديل لنظام البناء SCons في مشروع Chromium. NMAKE (أداة صيانة برامج Microsoft) NMAKE هي أداة سطر أوامر طوّرتها ميكروسوفت لاستخدامها مع Visual Studio و / أو أدوات سطر أوامر Visual C++‎. ويُنظر إليها على أنها نظام بناء يندرج ضمن مجموعة عائلة Make لأنظمة البناء، ولكنّه يتميّز ببعض الميزات الخاصّة التي تميّزه عن برامج Make الشبيهة بيونكس الأخرى، مثل دعم صياغة مسارات ملفات Windows (والتي تختلف عن مسارات الملفات في يونيكس). أخطاء المصرف الشائعة (GCC) مرجعّ غير معرَّف إلى "***" تحدث أخطاء الرابط (linker) عندما يعجز عن العثور عن رمز مُستخدم، ويحدث هذا غالبًا عند عدم ربط إحدى المكتبات المستخدمة. qmake: LIBS += nameOfLib cmake: TARGET_LINK_LIBRARIES(target nameOfLib) استدعاء g++‎: g++ -o main main.cpp -Llibrary/dir -lnameOfLib قد ينسى المرء أيضًا تصريف وربط جميع ملفّات ‎.cpp‎ المستخدمة (تحدّد functionModule.cpp الدالّة المطلوبة): g++ -o binName main.o functionsModule.o error: '***' was not declared in this scope يحدث هذا الخطأ في حال استخدام كائن غير معروف. أخطاء متعلقة بالمتغيّرات لن تُصرّف الشيفرة التالية، لأن المتغير i غير موجود في نطاق الدالة main: #include <iostream> int main(int argc, char *argv[]) { { int i = 2; } std::cout << i << std::endl; return 0; } الحل: #include <iostream> int main(int argc, char *argv[]) { { int i = 2; std::cout << i << std::endl; } return 0; } أخطاء متعلقة بالدوال يحدث هذا الخطأ غالبًا في حال عدم تضمين الترويسة المطلوبة (على سبيل المثال استخدام ‎std::cout‎ دون ‎#include <iostream>‎). ففي المثال التالي، لن تُصرّف الشيفرة التالية: #include <iostream> int main(int argc, char *argv[]) { doCompile(); return 0; } void doCompile() { std::cout << "No!" << std::endl; } الحلّ: #include <iostream> void doCompile(); // تصريح لاحق للدالة int main(int argc, char *argv[]) { doCompile(); return 0; } void doCompile() { std::cout << "No!" << std::endl; } أو: #include <iostream> void doCompile() // تعريف الدالة قبل استخدامها { std::cout << "No!" << std::endl; } int main(int argc, char *argv[]) { doCompile(); return 0; } ملاحظة: يفسر المُصرِّف الشيفرة من الأعلى إلى الأسفل (للتبسيط فقط)، لذا يجب التصريح عن كل شيء (أو تعريفه) قبل استخدامه. fatal error: ***: No such file or directory يحدث هذا الخطأ عندما يعجز المُصرِّف عن العثور عن ملفّ معيّن (ملف مصدري يستخدم ‎#include "someFile.hpp"‎). qmake: INCLUDEPATH += dir / Of / File cmake: include_directories(dir/Of/File) استدعاء g++‎: g++ -o main main.cpp -Idir/Of/File عدم التوافقية مع لغة C سنتحدّث في هذا القسم عن شيفرات C غير المتوافقة مع لغة C++‎ والتي ستعطل مصرّفاتها‎. الكلمات المفتاحية المحجوزة هناك كلمات مفتاحية لها غرض خاص في C++‎، فالشيفرة التالية مثلًا جائزة في C، لكن غير جائزة في C++‎. int class = 5 وإصلاح هذه الأخطاء سهل، فكل ما عليك هو إعادة تسمية المتغيّر. المؤشّرات ذات النوعية الضعيفة Weakly Typed Pointers يمكن تحويل المؤشّرات في C إلى النوع ‎void*‎، أما في C++‎ فستحتاج إلى تحويل صريح explicit cast. انظر المثال التالي حيث تكون الشيفرة غير جائزة في C++‎، ولكن جائزة في C: void* ptr; int* intptr = ptr; إضافة تحويل صريح (explicit cast) ستحل المشكلة لكن قد يسبّب مشاكل أخرى. goto أو switch لا يجوز في ++C أن تتخطي التهيئة باستخدام ‎goto‎ أو ‎switch‎. انظر المثال التالي حيث تكون الشيفرة صحيحة في C، وغير صحيحة في C++‎: goto foo; int skipped = 1; foo; قد يتطلب إصلاح هذه الأخطاء إعادة تصميم البرنامج. هذا الدرس جزء من سلسلة دروس عن C++‎. ترجمة -بتصرّف- للفصول Chapter 130: Build Systems Chapter 139: Common compile/linker errors (GCC)‎ Chapter 136: C incompatibilities من كتاب C++ Notes for Professionals
  8. الصحة الثباتية (Const Correctness) أسلوب لتصميم الشيفرات، يعتمد على فكرة أنّه لا ينبغي أن تُتاح إمكانية تعديل نسخة معيّنة -على سبيل المثال الحق في الكتابة- إلّا للشيفرَات التي تحتاج إلى تعديل تلك النسخة، وبالمقابل، فإنّ أيّ شيفرة لا تحتاج إلى تعديل نُسخة ما لن تملك القدرة على تعديلها، أي سيكون لها حق القراءة فقط. تصون هذه المقاربة النُسخة من أن تُعدَّل عن غير قصد، لأنّ ذلك قد يجعل الشيفرة غير موثوقة، كما يوضّح للمبرمجين الذي يقرؤون الشيفرة ما إذا كانت الشيفرة تهدف إلى تغيير حالة النُسخة أم لا. وتسمح أيضًا بالتعامل مع النسخ ككائنات ثابتة (‎const‎) عندما لا تكون هناك حاجة إلى تعديلها، أو تعريفها ككائنات ثابتة إذا لم تكن بحاجة إلى تغييرها بعد التهيئة، وذلك دون التأثير على وظيفتها. يتم ذلك عن طريق إعطاء التوابعِ مؤهلات (const CV-qualifiers)، وعن طريق وسم معاملات المؤشّرات / المراجع بالكلمة المفتاحية ‎const‎. class ConstCorrectClass { int x; public: int getX() const { return x; } // الدالة ثابتة: أي أنها لن تعدل النسخة void setX(int i) { x = i; } // غير ثابتة: أي أنها ستعدل النسخة }; // المعامل ثابت، أي أنّه لن يُعدّل int const_correct_reader(const ConstCorrectClass& c) { return c.getX(); } // المعامل غير ثابت: سيُعدَّل void const_correct_writer(ConstCorrectClass& c) { c.setX(42); } const ConstCorrectClass invariant; // النسخة ثابتة: لن تُعدّل ConstCorrectClass variant; // النسخة غير ثابتة: يمكن أن تُعدَّل // … // صحيح: استدعاء دالة ثابتة غير مُعدِّلة على نسخة ثابتة const_correct_reader(invariant); // صحيح: استدعاء دالة ثابتة غير مُعدِّلة على نسخة قابة للتعديل const_correct_reader(variant); // صحيح: استدعاء دالة ثابتة مُعدِّلة على نسخة قابلة للتعديل const_correct_writer(variant); // خطأ: استدعاء دالة ثابتة مُعدِّلة على نسخة ثابتة const_correct_writer(invariant); نظرًا لطبيعة الصحة الثباتية، فإنّها تبدأ بتوابع الصنف، ثمّ تستمر إلى الخارج، وسيطلق المصرِّف خطأً إذا حاولت استدعاء تابع غير ثابت من نُسخة ثابتة، أو من نُسخة غير ثابتة تُعامل كنسخة ثابتة. الصحة الثباتية في تصميم الأصناف جميع الدوال التابعة في صنف صحيح ثباتيًا (const-correct class)، التي لا تُغيّر الحالة المنطقية، يكون مؤشّر ‎this‎ الخاص بها ثابتًا، للدلالة على أنّها لن تُعدّل الكائن، لكن هذا لا يمنع أن تكون هناك حقول متغيرة - ‎mutable‎ - والتي يمكن تعديلها حتى لو كانت الكائنات التي تنتمي إليها ثابتة. عندما تعيد دالةٌ تأهيلها ثابت (‎const‎) مرجعًا، فينبغي أن يكون ذلك المرجع ثابتًا. يتيح هذا لها إمكانية أن تُستدعى على كلٍّ من النُسخ الثابتة، وكذلك النسخ غير المؤهّلة (non-cv-qualified)، إذ ستكون ‎const T*‎ قادرة على الارتباط بـ ‎T*‎ أو ‎const T*‎. هذا يسمح بدوره للدوالّ بأن تصرّح عن المُعاملات المُمرّة بالمرجع (passed-by-reference) على أنّها ثابتة عندما لا تحتاج إلى تعديل، وذلك دون التأثير على وظيفتها. علاوة على ذلك، في الأصناف الصحيحة ثباتيًا، تكون جميع مُعاملات الدالة المُمرّة بالمرجع صحيحة ثباتيًا، إذ لن يمكن تعديلها إلّا عندما تحتاج الدالّة إلى تعديلها صراحة. أولاً، لننظر إلى مؤهِّلات للمؤشّر ‎this‎ : لنفرض أن لدينا صنف Field، له الدالة التابعة ;(void insert_value(int class ConstIncorrect { Field fld; public: ConstIncorrect(Field& f); // يعدِّل Field& getField(); // قد يعدِّل، كما تُكشَف الأعضاء كمراجع غير ثابتة وذلك لإتاحة التعديل غير // المباشر void setField(Field& f); // يعدِّل void doSomething(int i); // قد يعدِّل void doNothing(); // قد يعدِّل }; ConstIncorrect::ConstIncorrect(Field& f): fld(f) {} // يعدِّل Field& ConstIncorrect::getField() { return fld; } // لا يعدِّل void ConstIncorrect::setField(Field& f) { fld = f; } // يعدِّل void ConstIncorrect::doSomething(int i) { // يعدِّل fld.insert_value(i); } void ConstIncorrect::doNothing() {} // لا يعدِّل class ConstCorrectCVQ { Field fld; public: ConstCorrectCVQ(Field& f); // يعدِّل const Field& getField() const; // لا يعدِّل، تُكشف الأعضاء كمراجع ثابتة لمنع التعديل غير المباشر void setField(Field& f); // يعدِّل void doSomething(int i); // يعدِّل void doNothing() const; // لا يعدِّل }; ConstCorrectCVQ::ConstCorrectCVQ(Field& f): fld(f) {} Field& ConstCorrectCVQ::getField() const { return fld; } void ConstCorrectCVQ::setField(Field& f) { fld = f; } void ConstCorrectCVQ::doSomething(int i) { fld.insert_value(i); } void ConstCorrectCVQ::doNothing() const {} هذا لن يعمل إذا لا يمكن استدعاء الدوال التابعة على نُسخ ConstIncorrect، نتابع … void const_correct_func(const ConstIncorrect& c) { Field f = c.getField(); c.do_nothing(); } أما هذا فيعمل إذ يمكن استدعاء ()doNothing و ()getField على نسخ ConstCorrectCVQ … void const_correct_func(const ConstCorrectCVQ& c) { Field f = c.getField(); c.do_nothing(); } يمكننا بعد ذلك دمج هذا مع معاملات الدوال الصحيحة ثباتيا (‎Const Correct Function Parameters‎)، وسيجعل هذا الصنفَ صحيحًا ثباتيًا بشكل كامل. class ConstCorrect { Field fld; public: ConstCorrect(const Field& f); // تعدّل النسخة، ولكن لا تعدّل المعامل const Field& getField() const; // لا تعدِّل، وتكشف العضو كمرجع ثابت لمنع التعديل غير المباشر void setField(const Field& f); // تعدّل النسخة، ولكن لا تعدّل المعامل void doSomething(int i); // passed by value تعدّل، لكن لا تعدّل المعامل - ممرر بالقيمة void doNothing() const; // لا تعدّل }; ConstCorrect::ConstCorrect(const Field& f) : fld(f) {} Field& ConstCorrect::getField() const { return fld; } void ConstCorrect::setField(const Field& f) { fld = f; } void ConstCorrect::doSomething(int i) { fld.insert_value(i); } void ConstCorrect::doNothing() const {} يمكن أيضًا دمج هذا مع التحميل الزائد على أساس الصحة الثباتية إذا أردنا سلوك ما عندما تكون النُسخة ثابتة، وسلوكًا آخر مختلفًا في الحالات الأخرى. وإحدى الاستخدامات الشائعة لهذا هي الحاويات (constainers)، إذ توفّر دوال وصول (accessors) لا تسمح بالتعديل إلّا إن كانت الحاوية نفسها غير ثابتة. class ConstCorrectContainer { int arr[5]; public: // معامل تسجيل يوفر إمكانية القراءة إن كانت النسخة ثابتة، أو إمكانية القراءة/الكتابة خلاف ذلك int& operator[](size_t index) { return arr[index]; } const int& operator[](size_t index) const { return arr[index]; } // ... }; يُستخدم هذا كثيرًا في المكتبة القياسية، إذ توفّر معظم الحاويات تحميلات زائدة لأخذ الثباتية في الحسبان. معاملات الدوال الصحيحة ثباتيًا إذا كانت دالّة ما صحيحة ثباتيًا، فإنّ جميع المُعاملات المُمرَّرة إليها بالمرجع ستكون ثابتة، ما لم تعدّلها الدالّة بشكل مباشر أو غير مباشر. يفيد هذا في منع المبرمج من إجراء تعديل غير مقصود، ويسمح للدالّة بأن تقبل النُسخ الثابتة، وكذلك النسخ غير المؤهّلة، وتجعل مؤشّر النسخة ‎this‎ من النوع ‎const T*‎ عندما تُستدعى دالة تابعة ما، حيث ‎T‎ يمثّل هنا نوع الصنف. struct Example { void func() { std::cout << 3 << std::endl; } void func() const { std::cout << 5 << std::endl; } }; void const_incorrect_function(Example& one, Example* two) { one.func(); two->func(); } void const_correct_function(const Example& one, const Example* two) { one.func(); two->func(); } int main() { Example a, b; const_incorrect_function(a, &b); const_correct_function(a, &b); } هذا هو خرج البرنامج 3 3 5 5 تمسك الدوال الصحيحة ثباتيًا الكثير من الأخطاء التي قد لا تمسكها الدوال غير الصحيحة ثباتيًا كما هو مبيّن أدناه، وذلك رغم أنّ تأثيرات هذا السلوك أقل وضوحًا من التأثيرات الناجمة عن تصميم الأصناف الصحيحة ثباتيًا التي رأيناها من قبل، في أنّ الدوال الصحيحة ثباتيا والأصناف غير الصحيحة ثباتيًا تتسبّب في حدوث أخطاء تصريفية، في حين أنّ الأصناف الصحيحة ثباتيًا والدوال غير الصحيحة ثباتيًا تُصرَّف بلا مشاكل. لاحظ أنّ الدوالّ غير الصحيحة ثباتيًا تتسبب في حدوث أخطاء تصريفية في حالة تمرير نُسخة ثابتة إليها. // قراءة القيمة من المتجهة ثم حساب وإعادة قيمة // إمساك القيمة المعادة لأجل تسريع البرنامج template<typename T> const T& bad_func(std::vector<T>& v, Helper<T>& h) { // إمساك القيم لإستخدامها مستقبلا // بعد حساب القيمة المعادة، تُخزّن ويُسجل فهرسها static std::vector<T> vals = {}; int v_ind = h.get_index(); // v الفهرس الحالي في سيكون السطر التالي مساويًا لـ 1- إن كان فهرس المخزن المؤقت غير مسجلًا … int vals_ind = h.get_cache_index(v_ind); if (vals.size() && (vals_ind != -1) && (vals_ind < vals.size()) && !(h.needs_recalc())) { return vals[h.get_cache_index(v_ind)]; } T temp = v[v_ind]; temp -= h.poll_device(); temp *= h.obtain_random(); temp += h.do_tedious_calculation(temp, v[h.get_last_handled_index()]); if (vals_ind != -1) { vals[vals_ind] = temp; } else { v.push_back(temp); // لا ينبغي الدخول إلى القيم vals_ind = vals.size() - 1; h.register_index(v_ind, vals_ind); } return vals[vals_ind]; } // النسخة الصحيحة ثباتيا تماثل النسخة أعلاه، لذا سنتجاوز معظمها template<typename T> const T& good_func(const std::vector<T>& v, Helper<T>& h) { // ... if (vals_ind != -1) { vals[vals_ind] = temp; } else { v.push_back(temp); // Error: discards qualifiers. vals_ind = vals.size() - 1; h.register_index(v_ind, vals_ind); } return vals[vals_ind]; } الصحة الثباتية كأداة للتوثيق من فوائد مفهوم الصحة الثباتية (const correctness) هي أنّه يمكن استخدامها لتوثيق الشيفرات، عبر توفير ضمانات وتوجيهات للمبرمجين والمستخدمين الآخرين. هذه الضمانات يفرضها المُصرِّف بسبب الثباتية، مع غيابٍ للثباتية يوحي أن الشيفرة لا توفرها. التوابع المؤهّلة ثباتيًا يمكن أن نفترض أنّ المبرمج الذي كتب الشيفرة يقصد أنّ الدوال التابعة الثابتة ستقرأ النسخة، و: لن تعدّل الحالة المنطقية للنُسخة التي استُدعيت عليها. نتيجة لذلك، فلن تعدّل أيّ حقل من حقول النُسخة التي استُدعيت عليها، باستثناء المتغيّرات القابلة للتغيير ‎mutable‎. لن تستدعي دوال أخرى من شأنّها تعديل أيّ حقل من حقول النُسخة، باستثناء المتغيّرات القابلة للتغيير ‎mutable‎. بالمقابل، يمكن افتراض أنّ أيّ دالة تابعة غير ثابتة سوف تسعى إلى تعديل النُسخة، و: قد تعدّل الحالة المنطقية أو لا. قد تستدعي دوال أخرى يمكن أن تعدّل الحالة المنطقية أو لا. يمكن استخدام هذا لأجل بناء افتراضات حول حالة الكائن بعد استدعاء دالة تابعة معيّنة عليه دون الحاجة إلى رؤية تعريف تلك الدالّة: // ConstMemberFunctions.h class ConstMemberFunctions { int val; mutable int cache; mutable bool state_changed; public: // يعدّل المنشئ الحالة المنطقية، لذا لا ضرورة لأيّ افتراضات ConstMemberFunctions(int v = 0); يمكن أن نفترض أن الدالة التالية لن تعدل الحالة المنطقية، ولن تستدعي ()set_val، ويمكن كذلك أن تستدعي ()bad_func أو ()squared_calc … int calc() const; يمكن أن نفترض أن الدالة التالية لن تعدل الحالة المنطقية، ولن تستدعي ()set_val، ويمكن كذلك أن تستدعي ()bad_func أو ()calc … int squared_calc() const; يمكن أن نفترض أن الدالة التالية لن تعدل الحالة المنطقية، ولن تستدعي ()set_val، ويمكن كذلك أن تستدعي ()calc أو ()squared_calc … void bad_func() const; يمكن أن نفترض أن الدالة التالية لن تعدل الحالة المنطقية، ولن تستدعي ()set_val، ويمكن كذلك أن تستدعي ()bad_func أو ()squared_calc أو ()calc … void set_val(int v); }; وفقًا لقواعد الصحة الثباتية، ستُفرض هذه الافتراضات في الواقع بواسطة المُصرِّف. // ConstMemberFunctions.cpp ConstMemberFunctions::ConstMemberFunctions(int v /* = 0*/) : cache(0), val(v), state_changed(true) {} // لقد كان افتراضنا صحيحا int ConstMemberFunctions::calc() const { if (state_changed) { cache = 3 * val; state_changed = false; } return cache; } // لقد كان افتراضنا صحيحا int ConstMemberFunctions::squared_calc() const { return calc() * calc(); } // لقد كان افتراضنا غير صحيح // للمؤهّلات `this` فشل تصريف الدالة بسبب فقدان void ConstMemberFunctions::bad_func() const { set_val(863); } // لقد كان افتراضنا صحيحا void ConstMemberFunctions::set_val(int v) { if (v != val) { val = v; state_changed = true; } } مُعاملات الدالة الثابتة يمكن أن نفترض أنّ الدوالّ التي تقبل مُعاملًا واحدًا ثابتًا أو أكثر تهدف إلى قراءة تلك المُعاملات، و: أنّها لن تعدّل تلك المُعاملات، أو تستدعي عليها تابعًا يمكن أن يُعدّلها. لن تمرّر تلك المُعاملات إلى أيّ دالّة أخرى من شأنها تعديلها و/أو استدعاء أي توابع أخرى من شأنها تعديلها. بالمقابل، يمكن افتراض أنّ أي دالّة تقبل مُعاملًا واحدًا غير ثابت أو أكثر ستسعى لتعديل تلك المُعاملات، و: قد تعدِّل أو لا تعدّل تلك المُعاملات، أو قد تستدعي عليها توابع يمكن أن تعدّلها. قد تمرّر أو قد لا تمرّر تلك المُعاملات إلى دوال أخرى يمكن أن تعدّلها و / أو تستدعي عليها توابع قد تعدّلها. يمكن استخدام هذا الأمر لأجل بناء افتراضات حول الحالة التي ستكون عليها المُعاملات بعد تمريرها إلى دالّة معيّنة، حتى دون النظر في تعريف تلك الدالّة. فيما يلي، يمكن أن نفترض أن c لم تُعدَّل وأن ()c.set_val لم تُستدعى ولم تمرَّر إلى ()non_qualified_function_parameter، وإذا مُرِّرَت إلى ()one_const_one_not فستكون أول المعامِلات … // function_parameter.h void const_function_parameter(const ConstMemberFunctions& c); يمكن أن نفترض أن c عُدِّلَت و/أو أن ()c.set_val استدعيَت، وقد تكون مرِّرَت إلى أي من تلك الدوال، وإن مُرِّرَت إلى ()one_const_one_not فقد تكون أيًا من المعامِلات … void non_qualified_function_parameter(ConstMemberFunctions& c); نستطيع افتراض أن: l لم تُعدَّل، وأن ()l.set_val لن تُستدعى. l قد تُمرَّر أو لا إلى ()const_function_parameter. تم تعديل r و/أو قد تستدعى ()r.set_val. قد تُمرَّر r أو لا إلى أي من الدوال السابقة. نتابع … void one_const_one_not(const ConstMemberFunctions& l, ConstMemberFunctions& r); يمكن أن نفترض أن c لم تُعدَّل وأن ()c.set_val لم تُستدعى، وأنما لم تُمرَّر إلى ()non_qualified_function_parameter، وإن مُرِّرَت إلى ()one_const_one_not فقد تكون أيًا من المعامِلات … void bad_parameter(const ConstMemberFunctions& c); وفقًا لقواعد الصحة الثباتية، ستُفرض هذه الافتراضات من قِبل المُصرِّف. // function_parameter.cpp // افتراضنا كان صحيحا void const_function_parameter(const ConstMemberFunctions& c) { std::cout << "With the current value, the output is: " << c.calc() << '\n' << "If squared, it's: " << c.squared_calc() << std::endl; } // افتراضنا كان صحيحا void non_qualified_function_parameter(ConstMemberFunctions& c) { c.set_val(42); std::cout << "For the value 42, the output is: " << c.calc() << '\n' << "If squared, it's: " << c.squared_calc() << std::endl; } // افتراضنا كان صحيحا // لاحظ أنّ الصحة الثباتية لا تحصِّن التغليف من أن يُكسر، وإنما تمنع الحق في الكتابة // إلا عند الحاجة إليها void one_const_one_not(const ConstMemberFunctions& l, ConstMemberFunctions& r) { struct Machiavelli { int val; int unimportant; bool state_changed; }; reinterpret_cast<Machiavelli&>(r).val = l.calc(); reinterpret_cast<Machiavelli&>(r).state_changed = true; const_function_parameter(l); const_function_parameter(r); } افتراضنا فيما يلي كان خطأ، وتفشل الدالة في التصريف لأن this فقد مؤهلاتٍ في ()c.set_val، … void bad_parameter(const ConstMemberFunctions& c) { c.set_val(18); } رغم أنّه يمكن التحايل على الصحة الثباتية وكسر الضمانات التي تقدّمها، إلا أنّه ينبغي توخّي الحذر عند فعل ذلك، ويجب أن يكون المبرمج على علم بما يفعل، فمن المحتمل أن يتسبّب في حدوث سلوك غير معرَّف. class DealBreaker : public ConstMemberFunctions { public: DealBreaker(int v = 0); // اسم غير مسموح به، لكنه ثابت void no_guarantees() const; } DealBreaker::DealBreaker(int v /* = 0 */) : ConstMemberFunctions(v) {} // افتراضنا كان خاطئا // الصحة الثباتية، وتجعل المصرف يعتقد أننا على علم بتبِعات ما نفعل const_cast تحذف void DealBreaker::no_guarantees() const { const_cast<DealBreaker*>(this)->set_val(823); } // ... const DealBreaker d(50); d.no_guarantees(); // ثابتة، وقد تُعدّل d سلوك غير معرَّف، إذ أنّ على أي حال، ولأن هذا يتطلب من المبرمج أن يخبر المصرِّف أنه ينوي تجاهل الثباتية (Constness)، ولعدم التماثل (Consistency) بين المصرِّفات، فمن الآمن افتراض أن الشيفرة الصحيحة ثباتيًا لن تتجاهل الصحة الثباتية أو التماثل بين المصرِّفات إلا إن حُدِّد خلاف ذلك. هذا الدرس جزء من سلسلة دروس عن C++‎. ترجمة -بتصرّف- للفصل Chapter 128: Const Correctness من كتاب C++ Notes for Professionals
  9. سوف نستعرض في هذا الدرس بعض الأمثلة على كيفية التعامل مع خادم العميل (Client server). مثال Hello TCP Client هذا البرنامج مكمّل لبرنامج Hello TCP Server، ويمكنك تشغيل أي منهما للتحقق من صلاحيتهما. انظر شيفرة المثال فيما يلي: #include <cstring> #include <iostream> #include <string> #include <arpa/inet.h> #include <netdb.h> #include <sys/socket.h> #include <sys/types.h> #include <unistd.h> int main(int argc, char *argv[]) { سنأخذ هنا عنوان IP ورقم بوابة كوسائط لبرنامجنا، نتابع … if (argc != 3) { std::cerr << "Run program as 'program <ipaddress> <port>'\n"; return -1; } auto &ipAddress = argv[1]; auto &portNum = argv[2]; addrinfo hints, *p; memset(&hints, 0, sizeof(hints)); hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; hints.ai_flags = AI_PASSIVE; int gAddRes = getaddrinfo(ipAddress, portNum, &hints, &p); if (gAddRes != 0) { std::cerr << gai_strerror(gAddRes) << "\n"; return -2; } if (p == NULL) { std::cerr << "No addresses found\n"; return -3; } ينشئ استدعاء ()socket قناة socket جديدة ويعيد الواصف الخاص بها … int sockFD = socket(p->ai_family, p->ai_socktype, p->ai_protocol); if (sockFD == -1) { std::cerr << "Error while creating socket\n"; return -4; } لاحظ عدم وجود استدعاء ()bind كما في برنامج Hello TCP Server، فذلك غير ضروري رغم أنك تستطيع استدعاءها، لأنه لا يلزم العميل أن يكون له رقم منفذ (port number) ثابت، لذا فإن الاستدعاء التالي سيربطه برقم عشوائي. سيحاول استدعاء ()connect أن ينشئ اتصال TCP بالخادم المحدد … int connectR = connect(sockFD, p -> ai_addr, p -> ai_addrlen); if (connectR == -1) { close(sockFD); std::cerr << "Error while connecting socket\n"; return -5; } std::string reply(15, ' '); هنا يحاول استدعاء ()recv الحصول على إجابة من الخادم، لكن الإجابة قد تحتاج إلى عدة استدعاءات لـ ()recv قبل أن تُستقبَل بالكامل، سنحاول حل هذا لاحقًا … auto bytes_recv = recv(sockFD, &reply.front(), reply.size(), 0); if (bytes_recv == -1) { std::cerr << "Error while receiving bytes\n"; return -6; } std::cout << "\nClient recieved: " << reply << std::endl; close(sockFD); freeaddrinfo(p); return 0; } مثال Hello TCP Server نقترح أن تلقي نظرة سريعة على كتاب Beej's Guide to Network Programming، والذي سيفسر لك معظم المفاهيم المُستخدَمة هنا. سننشئ خادم TCP بسيطًا هنا يعرض الجملة "Hello World" لكل الاستدعاءات الواردة، ثم يُغلقها. كما أنّ الخادم سيتواصل مع العملاء تكراريًا (iteratively)، أي عميلًا واحدًا في كل مرّة. سنشغّل الخادم عبر منفذ محدّد، لذلك سنأخذ رقم المنفذ كوسيط. انظر: #include <cstring> // sizeof() #include <iostream> #include <string> الترويسات التالية من أجل ()getaddrinfo و ()socket والدوال الصديقة … #include <arpa/inet.h> #include <netdb.h> #include <sys/socket.h> #include <sys/types.h> #include <unistd.h> // close() int main(int argc, char *argv[]) { هنا نتحقق مما إذا كان رقم المنفذ قد أُعطي أم لا … if (argc != 2) { std::cerr << "Run program as 'program <port>'\n"; return -1; } auto &portNum = argv[1]; في السطر التالي، عدد الاتصالات المسموح بها في طابور الوارد … const unsigned int backLog = 8; نحتاج إلى مؤشرين، res سيأخذ القيمة وp سيكرر عليها … addrinfo hints, *res, *p; memset( &hints, 0, sizeof(hints)); // راجع الكتاب لمزيد من التوضيحات hints.ai_family = AF_UNSPEC; // التي ستستخدم IP لا تحدّد بعدُ نسخة في السطر التالي، يشير SOCK_STREAM إلى TCP … hints.ai_socktype = SOCK_STREAM; hints.ai_flags = AI_PASSIVE; int gAddRes = getaddrinfo(NULL, portNum, &hints, &res); if (gAddRes != 0) { std::cerr << gai_strerror(gAddRes) << "\n"; return -2; } std::cout << "Detecting addresses" << std::endl; unsigned int numOfAddr = 0; يحرص طول ipv6 على إمكانية تخزين عنواني ipv4/6 في هذا المتغير، وبما أن ()getaddrinfo قد أعطتنا قائمة من العناوين، فسنكرر عليها ونسأل المستخدم أن يختار أحدها لربطها بالبرنامج … char ipStr[INET6_ADDRSTRLEN]; for (p = res; p != NULL; p = p -> ai_next) { void *addr; std::string ipVer; // ipv4 إن كان العنوان من النوع if (p -> ai_family == AF_INET) { ipVer = "IPv4"; sockaddr_in *ipv4 = reinterpret_cast < sockaddr_in *> (p -> ai_addr); addr = &(ipv4 -> sin_addr); ++numOfAddr; } // ipv6 إن كان العنوان من النوع else { ipVer = "IPv6"; sockaddr_in6 *ipv6 = reinterpret_cast < sockaddr_in6 * > (p -> ai_addr); addr = &(ipv6 -> sin6_addr); ++numOfAddr; } // من الشكل الثنائي إلى الشكل النصي IPv4/6 تحويل عناوين inet_ntop(p -> ai_family, addr, ipStr, sizeof(ipStr)); std::cout << "(" << numOfAddr << ") " << ipVer << " : " << ipStr << std::endl; } // في حال عدم العثور على أيّ عنوان if (!numOfAddr) { std::cerr << "Found no host address to use\n"; return -3; } // اسأل المستخدم أن يختار عنوانا std::cout << "Enter the number of host address to bind with: "; unsigned int choice = 0; bool madeChoice = false; do { std::cin >> choice; if (choice > (numOfAddr + 1) || choice < 1) { madeChoice = false; std::cout << "Wrong choice, try again!" << std::endl; } else madeChoice = true; } while (!madeChoice); p = res; // كواصف socketFD لننشئ قناة جديدة، ستُعاد // تعيد هذه الاستدعاءات في العادة القيمة -1 في حال وقوع خطأ ما int sockFD = socket(p->ai_family, p->ai_socktype, p->ai_protocol); if (sockFD == -1) { std::cerr << "Error while creating socket\n"; freeaddrinfo(res); return -4; } // لنربط العنوان بالقناة التي أنشأناها للتو int bindR = bind(sockFD, p->ai_addr, p->ai_addrlen); if (bindR == -1) { std::cerr << "Error while binding socket\n"; // في حال وقوع خطأ، احرص على إغلاق القناة وتحرير الموارد close(sockFD); freeaddrinfo(res); return -5; } // وأخيرا، ابدأ بالإنصات إلى الاتصالات الواردة عبر قناتنا int listenR = listen(sockFD, backLog); if (listenR == -1) { std::cerr << "Error while Listening on socket\n"; // في حال وقوع خطأ، احرص على إغلاق القناة وتحرير الموارد close(sockFD); freeaddrinfo(res); return -6; } // البنية كبيرة كفاية لتحمل عنوان العميل sockaddr_storage client_addr; socklen_t client_addr_size = sizeof(client_addr); const std::string response = "Hello World"; فيما يلي حلقة لا نهائية للتواصل مع الاتصالات الواردة، وستتعامل معها بالتتابع أي واحدة في كل مرة. كذلك فيما يلي من الأمثلة سنستدعي ()fork لكل اتصال مع العملاء … while (1) { // يعيد واصف قناة جديد accept استدعاء int newFD = accept(sockFD, (sockaddr *) &client_addr, &client_addr_size); if (newFD == -1) { std::cerr << "Error while Accepting on socket\n"; continue; } فيما يلي، يعيد استدعاء send البيانات التي مرَّرتها أنت كمعامِل ثاني، وطولها كمعامل ثالث، ويعيد عدد البِتَّات المرسلة فعليًا … auto bytes_sent = send(newFD, response.data(), response.length(), 0); close(newFD); } close(sockFD); freeaddrinfo(res); return 0; } سيعمل البرنامج على النحو التالي: Detecting addresses (1) IPv4 : 0.0.0.0 (2) IPv6 : :: Enter the number of host address to bind with: 1 هذا الدرس جزء من سلسلة دروس عن C++‎. ترجمة -بتصرّف- للفصل Chapter 127: Client server examples من كتاب C++ Notes for Professionals
  10. تطبيقات على التعاود يمكن استعمال التعاود في الكثير من التطبيقات المفيدة إذ يساعدنا على تبسيط الشيفرة ويعطيها قوة وفعالية على عكس لو لم نعتمد على مفهوم التعاود وإليك بعض هذه التطبيقات مع شيفراتها. حساب تسلسلات فيبوناتشي Fibonnaci sequence هذه هي الطريقة الأبسط لاستخدام التكرارية للحصول على العنصر رقم N من سلسلة Fibonnaci: int get_term_fib(int n) { if (n == 0) return 0; if (n == 1) return 1; return get_term_fib(n - 1) + get_term_fib(n - 2); } لكن المشكلة في هذه الطريقة أنّها غير مناسبة للأعداد الكبيرة، فكلما زادت قيمة ‎n‎، زاد عدد استدعاءات الدالة بشكل أسي (exponentially). يمكن الاستعاضة عن هذه الطريقة بالتكرارية المُذيّلة (tail recursion) على النحو التالي: int get_term_fib(int n, int prev = 0, int curr = 1) { if (n == 0) return prev; if (n == 1) return curr; return get_term_fib(n - 1, curr, prev + curr); } سيحسُب كل استدعاء للدالّة الحدّ التالي في تسلسل Fibonnaci، وعليه فإنّ عدد استدعاءات الدالّة يزيد خطيًا مع ‎n‎. التكرارية مع التذكّر Recursion with memoization قد تصبح الدوال التكرارية مكلفة للغاية، ويمكن جعلها أسرع بكثير عن طريق تخزين القيم التي سبق حسابها، إذا كانت دوالًّا خالصة -أي دوالًّا تُعيد دائمًا نفس القيمة عند استدعائها مع نفس الوسائط، ولا تعتمد على الحالة الخارجية ولا تعدّلها-، لكن هذا سيكون على حساب استخدام مزيد من الذاكرة. يوضّح المثال التالي كيفية حساب تسلسل فيبوناتشي باستخدام التكرارية التذكيرية: #include <map> int fibonacci(int n) { static std::map < int, int > values; if (n == 0 || n == 1) return n; std::map<int,int>::iterator iter = values.find(n); if (iter == values.end()) { return values[n] = fibonacci(n - 1) + fibonacci(n - 2); } else { return iter->second; } } لاحظ أنّه على الرغم من استخدام صيغة التكرارية البسيطة، إلّا أنّ تعقيد الدالّة يساوي O(n)‎ في الاستدعاء الأول، وسيكون التعقيد ثابتًا في الاستدعاءات اللاحقة مع نفس القيمة، أي O(1)‎. واعلم أنّ هذا التنفيذ ليس متعدّد المداخل (reentrant)، كما لا يسمح بالتخلص من القيم المُخزّنة. هناك تقديم بديل يعتمد على السماح بتمرير قاموس كوسيط إضافي: #include <map> int fibonacci(int n, std::map<int, int> values) { if (n==0 || n==1) return n; std::map<int,int>::iterator iter = values.find(n); if (iter == values.end()) { return values[n] = fibonacci(n-1) + fibonacci(n-2); } else { return iter->second; } } في هذا المثال، يُطلَب من المُستدعي أن يحفظ القاموس الذي يحتوي القيم المُخزّنة. ميزة هذا أنّ الدالّة أصبحت الآن متعدّدة المداخل (reentrant)، وأنّه المستدعي يستطيع إزالة القيم التي لم تعد مطلوبة، وهذا يحسّن استخدام الذاكرة. بالمقابل، يعيب هذه الطريقة أنّها تكسر التغليف (breaks encapsulation). إذ يمكن للمُستدعي تغيير الخرج عن طريق ملء القاموس بقيم غير صحيحة. الكائنات القابلة للاستدعاء Callable Objects الكائنات القابلة للاستدعاء هي هياكل C++‎ يمكن استخدامها كدوال، وتدخل فيها كل الأشياء التي يمكنك تمريرها إلى دالة المكتبة القياسية في C++‎ 17‏ invoke()‎ أو التي يمكن استخدامها في مُنشئ std::function، وهذا يشمل: مؤشّرات الدوال، والأصناف التي تحتوي على operator()‎، والأصناف ذات التحويلات الضمنية، ومراجع الدوال، ومؤشّرات الدوال التابعة ومؤشّرات الحقول (Pointers to member data)، وتعابير لامبدا. تُستخدَم الكائنات القابلة للاستدعاء في العديد من خوارزميات مكتبة القوالب القياسية STL كدوال شرطية (predicate). مؤشّرات الدوال Function Pointers تعدّ مؤشّرات الدوال الطريقة الأبسط لتمرير الدوال، ويمكن استخدامها أيضًا في لغة C، وإن أردت استخدامها ككائنات قابلة للاستدعاء، فيمكنك تعريف مؤشّر الدالّة على النحو التالي: typedef returnType(*name)(arguments); // All using name = returnType(*)(arguments); // <= C++11 using name = std::add_pointer<returnType(arguments)>::type; // <= C++11 using name = std::add_pointer_t<returnType(arguments)>; // <= C++14 يستخدم المثال التالي مؤشّر دالة لكتابة دالة مخصّصة لترتيب المتجهات: using LessThanFunctionPtr = std::add_pointer_t<bool(int, int)>; void sortVectorInt(std::vector<int>&v, LessThanFunctionPtr lessThan) { if (v.size() < 2) return; if (v.size() == 2) { if (!lessThan(v.front(), v.back())) // استدعاء مؤشر الدالة std::swap(v.front(), v.back()); return; } std::sort(v, lessThan); } bool lessThanInt(int lhs, int rhs) { return lhs < rhs; } sortVectorInt(vectorOfInt, lessThanInt); // تمرير المؤشر إلى دالة حرّة struct GreaterThanInt { static bool cmp(int lhs, int rhs) { return lhs > rhs; } }; sortVectorInt(vectorOfInt, &GreaterThanInt::cmp); // تمرير المؤشر إلى دالة تابعة ساكن كان بإمكاننا استدعاء مؤشّر الدالّة بدلًا من ذلك، بإحدى الطرق التالية : (*lessThan)(v.front(), v.back()) std::invoke(lessThan, v.front(), v.back()) // <= C++17 الأصناف ذات التابع operator()‎‏ (الكائنات الداليّة Functors) يمكن استخدام كل الأصناف التي تحمّل ‎operator()‎ تحميلًا زائدًا ككائنات دوال، ويمكن كتابة هذه الأصناف يدويًا (يشار إليها غالبًا باسم الكائنات الدالية - functors)، أو إنشاؤها تلقائيًا بواسطة المُصرِّف عن طريق كتابة تعابير لامدا (منذ C++‎ 11 وما بعده). struct Person { std::string name; unsigned int age; }; // كائن دالي، يعثر على الشخص باسمه struct FindPersonByName { FindPersonByName(const std::string &name) : _name(name) {} // التابع المزاد تحميله والذي سيُستدعى bool operator()(const Person &person) const { return person.name == _name; } private: std::string _name; }; std::vector<Person> v; // نفترض أنّه يحتوي البيانات std::vector<Person>::iterator iFind = std::find_if(v.begin(), v.end(), FindPersonByName("Foobar")); // ... بحكم أنّ للكائنات الدالية هويّتها الخاصة، فلا يمكن وضعها في التعريفات النوعية typedef، ويجب قبولها عبر وسيط القالب. يمكن أن يكون تعريف ‎std::find_if‎ على الشكل التالي: template < typename Iterator, typename Predicate > Iterator find_if(Iterator begin, Iterator end, Predicate &predicate) { for (Iterator i = begin, i != end, ++i) if (predicate( *i)) return i; return end; } يمكن استدعاء الدالة الشرطية عبر استدعاء: ‎std::invoke(predicate, *i)‎، وذلك منذ الإصدار C++ 17. هذا الدرس جزء من سلسلة دروس عن C++‎. ترجمة -بتصرّف- للفصل Chapter 124: Recursion in C++‎ والفصل Chapter 126: Callable Objects من كتاب C++ Notes for Professionals
  11. السمة [[fallthrough]] الإصدار ≥ C++‎ 17 عند إنهاء تعليمة case بكلمة switch ستنفَّذ الشيفرة التي تليها، وإن أردت منع هذا السلوك، فاستخدم تعليمة ‎‎break‎‎. يُسمّى هذا السلوك بـ "السقطة" (fallthrough)، وقد ينجم عنه أخطاء غير مقصودة، لذا تطلق العديد من المُصرِّفات والمحلّلات الساكنة (static analyzers) تحذيرًا منه. وقد أُدخلت سمة قياسية (standard attribute) في C++ 17 من أجل الإشارة إلى أنّ التحذير ليس ضرويًا إذا كانت السقطة مقصودة في الشيفرة، ويمكن للمصرّفات إعطاء تحذيرات بأمان إذا أنهيت case بالكلمةُ المفتاحيةُ ‎break‎ أو ‎[[fallthrough]]‎، وكانت مؤلفة من تعليمة واحد على الأقل. switch (input) { case 2011: case 2014: case 2017: std::cout << "Using modern C++" << std::endl; [[fallthrough]]; // > لا يوجد تحذير case 1998: case 2003: standard = input; } راجع هذا الاقتراح لمزيد من الأمثلة حول كيفية استخدام ‎[[fallthrough]]‎. السمة [[nodiscard]] الإصدار ≥ C++‎ 17 يمكن استخدام السمة ‎[[nodiscard]]‎ للإشارة إلى أنّه لا ينبغي تجاهل القيمة المُعادة من دالة عند استدعاء دالة، ويجب على االمُصرِّف في حال تجاهلها إعطاء تحذير بهذا الشأن. يمكن إضافة هذه السمة إلى: تعريف دالّة نوع ينتج عن إضافة السمة إلى نوع ما نفسُ السلوكِ الناتج عن إضافة السمة إلى كل الدوالّ التي تُعيد ذلك النوع. template<typename Function> [[nodiscard]] Finally<std::decay_t<Function>> onExit(Function &&f); void f(int &i) { assert(i == 0); // لجعل التعليقات واضحة ++i; // i == 1 auto exit1 = onExit([&i]{ --i; }); // f() التخفيض بـ 1 عند الخروج من ++i; // i == 2 onExit([&i]{ --i; }); // خطأ: محاولة التخفيض بـ 1 مباشرة // يُتوقع أن يصدر المصرف تنبيها std::cout << i << std::end; // القيمة المتوقعة 2، لكن القيمة الحقيقية هي 1 } راجع هذا الاقتراح لمزيد من الأمثلة حول كيفية استخدام ‎[[nodiscard]]‎. السمة [[deprecated]] والسمة [[deprecated("reason")‎]] الإصدار ≥ C++‎ 14 قدَّمت C++‎ 14 طريقة قياسية لإهمال (deprecating) الدوال عبر السمات. يمكن استخدام ‎[[deprecated]]‎ للإشارة إلى إهمال الدالّة. ويسمح التعبير ‎[[deprecated("reason")]]‎بتحديد سبب للإهمال يمكن أن نجعل المُصرِّف يظهِره. void function(std::unique_ptr<A> &&a); // توفير رسالة محدّدة تساعد المبرمجين الآخرين على تصحيح شيفراتهم [[deprecated("Use the variant with unique_ptr instead, this function will be removed in the next release")]] void function(std::auto_ptr<A> a); // في حال عدم وجود رسالة، سيُطلق تنبيه عام عند الاستدعاء [[deprecated]] void function(A *a); يُمكن تطبيق هذه السمة على: تصريح صنف. اسم typedef. متغيّر. حقل غير ساكن (non-static data member). دالة. تعداد (enum). تخصيص قالب (template specialization). (المرجع: c++14 standard draft: 7.6.5 Deprecated attribute) السمة [[maybe_unused]] تُستعمل السمة ‎[[maybe_unused]]‎ للدلالة على احتمال عدم استخدام منطق بعينه داخل الشيفرة، وترتبط غالبًا بشروط المعالج الأَولي (preprocessor). ويمكن استخدام هذه السمة لمنع هذا السلوك من خلال الإشارة إلى أنّ هذا الأمر مقصود، نظرًا لأنّ المُصرِّفات قد تبعث تحذيرات بشأن المتغيّرات غير المستخدمة. وتُعد القيم المعادة التي تشير إلى نجاح عملية التنقيح من الأمثلة التطبيقية للمتغيّرات التي تُستخدم أثناء عمليات تنقيح بنيات الشيفرة (debug builds)، ولكن لا تبقى إليها حاجة في مرحلة الإنتاج. ويجب التحقق من شرط النجاح أثناء تنقيح أخطاء البُنيات، رغم أن تلك التحققات ستُحذف في مرحلة الإنتاج لعدم الحاجة إليها. [[maybe_unused]] auto mapInsertResult = configuration.emplace("LicenseInfo", stringifiedLicenseInfo); assert(mapInsertResult.second); // لا تُستدعى إلا خلال الإطلاق، لذا لا ينبغي أن تكون في القاموس هناك مثال أكثر تعقيدًا، وهي الدوال المساعدة التي توضع في فضاء اسم غير مُسمّى (unnamed namespace)، وتلك الدوال إن لم تُستخدَم أثناء التصريف، فقد يطلق االمُصرِّف تحذيرًا بهذا الشأن، لذا قد ترغب في حمايتها باستخدام نفس وسوم المعالج الأولي التي تُستخدَم مع المُستدعي (caller)، لكن قد يكون هذا معقدًا، لهذا يُعدُّ استخدام السمة ‎[[maybe_unused]]‎ بديلاً أسهل في الصيانة. namespace { [[maybe_unused]] std::string createWindowsConfigFilePath(const std::string &relativePath); // TODO: … BSD, MAC أعد استخدام هذا على [[maybe_unused]] std::string createLinuxConfigFilePath(const std::string &relativePath); } std::string createConfigFilePath(const std::string &relativePath) { #if OS == "WINDOWS" return createWindowsConfigFilePath(relativePath); #elif OS == "LINUX" return createLinuxConfigFilePath(relativePath); #else #error "OS is not yet supported" #endif } راجع هذا الاقتراح لمزيد من الأمثلة حول كيفية استخدام ‎[[maybe_unused]]‎. السمة [[noreturn]] الإصدار ≥ C++‎ 11 قدّمت C++‎ 11 سمةَ ‎[[noreturn]]‎ التي يمكن استخدامها مع دالّة للإشارة إلى أنّ تلك الدالّة لا تُعاد إلى المُستدعي، سواء عبر تعليمة return، أو عند الوصول إلى نهاية متنها -من المهم الإشارة إلى أن هذا لا ينطبق على الدوال الفارغة ‎void‎، نظرًا لأنّها تعود إلى المُستدعي، ولكن لا تعيد أيّ قيمة-. قد تنتهي مثل هذه الدوالّ عبر استدعاء ‎std::terminate‎ أو ‎std::exit‎، أو عبر رفع اعتراض (throwing an exception). تجدر الإشارة أيضًا إلى أنّ مثل هذه الدوالّ يمكن أن تُعاد عبر تنفيذ ‎longjmp‎. على سبيل المثال، الدالّة أدناه إمّا أن ترفع اعتراضًا أو تستدعي ‎std::terminate‎، لذلك فهي مرشح جيد لاستخدام ‎[[noreturn]]‎: [[noreturn]] void ownAssertFailureHandler(std::string message) { std::cerr << message << std::endl; if (THROW_EXCEPTION_ON_ASSERT) throw AssertException(std::move(message)); std::terminate(); } يسمح هذا السلوك للمُصرِّف بإنهاء الدوال التي لا تحتوي على تعليمة return إذا كان يعلم أنّ الشيفرة لن تُنفَّذ. هنا، لن يحتاج المُصرِّف إلى إضافة الشيفرة الموجودة أسفل ذلك الاستدعاء نظرًا لأنّ استدعاء ‎ownAssertFailureHandler‎ في الشيفرة أدناه لن يعود أبدًا: std::vector < int > createSequence(int end) { if (end > 0) { std::vector < int > sequence; sequence.reserve(end + 1); for (int i = 0; i <= end; ++i) sequence.push_back(i); return sequence; } ownAssertFailureHandler("Negative number passed to createSequence()"s); // return std::vector<int>{}; //< Not needed because of [[noreturn]] } سيكون السلوك غير معرَّف إذا كانت الدالّة ستعاد، ونتيجة لذلك لا يُسمح بما يلي: [[noreturn]] void assertPositive(int number) { if (number >= 0) return; else ownAssertFailureHandler("Positive number expected"s); //< [[noreturn]] } لاحظ أنّ ‎[[noreturn]]‎ تُستخدم في الغالب في الدوال الفارغة، لكنها ليست ضرورية، وهذا يسمح باستخدام الدوال في البرمجة العامة (generic programming): template<class InconsistencyHandler> double fortyTwoDivideBy(int i) { if (i == 0) i = InconsistencyHandler::correct(i); return 42. / i; } struct InconsistencyThrower { static [[noreturn]] int correct(int i) { ownAssertFailureHandler("Unknown inconsistency"s); } } struct InconsistencyChangeToOne { static int correct(int i) { return 1; } } double fortyTwo = fortyTwoDivideBy<InconsistencyChangeToOne>(0); double unreachable = fortyTwoDivideBy<InconsistencyThrower>(0); تحتوي دوال المكتبة القياسية التالية على هذه السمات: std::abort std::exit std::quick_exit std::unexpected std::terminate std::rethrow_exception std::throw_with_nested std::nested_exception::rethrow_nested هذا الدرس جزء من سلسلة دروس عن C++‎. ترجمة -بتصرّف- للفصل Chapter 123: Attributes من كتاب C++ Notes for Professionals
  12. ‎constexpr‎ هي كلمة مفتاحية يمكن استخدامها مع متغيّر لجعل قيمته تعبيرًا ثابتًا (constant expression)، أو دالةً لأجل استخدامها في التعبيرات الثابتة، أو (منذ C++‎ 17) تعليمة if حتى يُصرَّف فرع واحد فقط من فروعها. المصادقة عبر الدالة static_assert تقتضي المصادقات (Assertations) وجوب التحقق من شرط معيّن، وإطلاق خطأ إن كانت خطأً (false)، ويحدث هذا في وقت التصريف بالنسبة لـ ‎static_assert()‎. template<typename T> T mul10(const T t) { static_assert( std::is_integral<T>::value, "mul10() only works for integral types" ); return (t << 3) + (t << 1); } المعاملات التي تقبلها الدالة ‎static_assert()‎: المعامِل التفاصيل bool_constexpr التعبير المراد التحقق منه message الرسالة المُراد طباعتها عندما تساوي bool_constexpr القيمة false table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; } المعامل الأوّل إلزامي ويمثّل الشرط، وهو تعبير منطقي ثابت constexpr. كذلك يمكن أن تقبل الدالة أيضًا مُعاملًا ثانيًا، والذي يمثّل الرسالة، وهي سلسلة نصية مجردة. صار المعامِل الثاني اختياريًا ابتداءً من C++‎ 17، أما قبل ذلك كان إلزاميًا. الإصدار ≥ C++‎ 17 template < typename T > T mul10(const T t) { static_assert(std::is_integral < T > ::value); return (t << 3) + (t << 1); } تُستخدم المصادقات في حال: لزِم التحقق في وقت التصريف من نوع معيّن في تعبير ثابت constexpr. احتاجت دالّة القالب إلى التحقق من خاصّيات معيّنة من النوع المُمرّر إليها. إذا أردت كتابة حالات اختبارية لما يلي: الدوال الوصفية للقوالب template metafunctions دوال التعبيرات الثابتة constexpr functions شيفرة وصفية جامعة macro metaprogramming إن كانت بعض التعريفات مطلوبة (على سبيل المثال، إصدار C++‎) نقل الشيفرات القديمة (Porting legacy code)، والمصادقة (assertation) على ‎sizeof(T)‎ (على سبيل المثال، ‎32-bit int) إن كانت بعض ميزات المصرّف مطلوبة لعمل البرنامج (التحزيم - packing - أو تحسين الأصناف الأساسية الفارغة، وما إلى ذلك) لاحظ أنّ ‎static_assert()‎ لا تشارك في قاعدة SFINAE: إذا كانت التحميلات الزائدة/ التخصيصات الإضافية ممكنة، فلا ينبغي استخدامها بدلًا من تقنيات قوالب البرمجة الوصفية - template metaprogramming - (مثل ‎std::enable_if<>‎)، وقد تُستخدَم -مع لزوم التحقق منها- في شيفرة القالب في حال إيجاد ([التحميل الزائد](رابط الفصل 35) / التخصيص) المتوقع، وفي مثل هذه الحالات قد تُوفِّر رسالة خطأ أو أكثر تكون أوضح مما لو كنا اعتمدنا على قاعدة SFINAE. متغيّرات التعبيرات الثابتة (constexpr variables) إن صُرِّخ عن متغيّر بالكلمة المفتاحية ‎constexpr‎، فسيكون ثابتًا (‎const‎) ضمنيًا، وسيكون من الممكن استخدام قيمته كتعبير ثابت. المقارنة مع define يمكن استخدام ‎constexpr‎ كبديل آمن نوعيًا (type-safe) للتعبيرات التي تعتمد على ‎#define‎ في وقت التصريف، وعند استخدام ‎constexpr‎، سيُستبدَل التعبير المُقيَّم في وقت التصريف بنتيجته. انظر المثال التالي: الإصدار ≥ C++‎ 11 int main() { constexpr int N = 10 + 2; cout << N; } سينتج عن المثال أعلاه الشيفرة التالية: cout << 12; ستختلف الشيفرة الجامعة التي تعتمد على المعالج الأولى في وقت التصريف (pre-processor based compile-time macro)، انظر المثال التالي: #define N 10 + 2 int main() { cout << N; } هذا سيُنتِج الشيفرة التالية: cout << 10 + 2; والذي سيُحوَّل إلى cout << 10 + 2 لكن سيكون على المُصرِّف أن يقوم بالمزيد من العمل، كما قد تحدث مشكلة في حال لم تستخدم بشكل صحيح. على سبيل المثال (مع ‎#define‎): cout << N * 2; سينتج: cout << 10 + 2 * 2; // 14 سيعيد التقييم الأولي (pre-evaluated) القيمة ‎24‎ للتعبير الثابت ‎constexpr‎، كما هو مُتوقّع. مقارنة مع const تحتاج المتغيرات الثابتة (‎const‎) إلى ذاكرة لتخزينها، وذلك على خلاف التعبيرات الثابتة ‎constexpr‎، وتنتج التعبيرات الثابتة ‎constexpr‎ قيمًا ثابتة في وقت التصريف وغير قابلة للتغيير. قد يقال أيضًا أنّ القيمة الثابتة (‎const‎) هي أيضًا غير قابلة للتغيير، لكن انظر المثال التالي لتوضيح الفرق بينهما: int main() { const int size1 = 10; const int size2 = abs(10); int arr_one[size1]; int arr_two[size2]; } ستفشل التعليمة الثانية في معظم المُصرِّفات -رغم أنها قد تعمل في GCC-، إذ يجب أن يكون حجم أيّ مصفوفة تعبيرًا ثابتًا (أي ينتُج عنه قيمة في وقت التصريف). وكما ترى في الشيفرة أعلاه، فقد أُسنِد إلى المتغيّر الثاني ‎size2‎ قيمة ستُحدَّد في وقت التشغيل (runtime) رغم أنّها تساوي ‎10‎، إلا أنّ المُصرِّف لا يعدُّها قيمة تصريفية (تصريفية، من وقت التصريف، compile-time). هذا يعني أنّ ‎const‎ قد تكون أو لا تكون ثابتة تصريفية حقيقية، ولا تستطيع أن تضمن لقيمة ثابتة ‎const‎ معيّنة أن تكون تصريفيةً، ولك أن تستخدم ‎#define‎ رغم أنها لا تخلو من بعض المشاكل. وعليه، يمكنك استخدام الحلّ التالي: الإصدار ≥ C++‎ 11 int main() { constexpr int size = 10; int arr[size]; } يجب تقييم التعابير الثابتة ‎constexpr‎ إلى قيم تصريفية، لذا لا يمكن استخدام ما يلي … الإصدار ≥ C++‎ 11 constexpr int size = abs(10); … ما لم تكن الدالة (‎abs‎) نفسها تعيد تعبيرًا ثابتًا. يجوز تهيئة جميع الأنواع الأساسية باستخدام ‎constexpr‎. الإصدار ≥ C++‎ 11 constexpr bool FailFatal = true; constexpr float PI = 3.14 f; constexpr char* site = "StackOverflow"; يمكنك أيضًا استخدام ‎auto‎ كما في المثال التالي: الإصدار ≥ C++‎ 11 constexpr auto domain = ".COM"; // const char * const domain = ".COM" constexpr auto PI = 3.14; // constexpr double تعليمة if الساكنة الإصدار ≥ C++‎ 17 يمكن استخدام عبارة ‎if constexpr‎ للتحكّم في تصريف الشيفرة، لكن يجب أن يكون الشرط تعبيرًا ثابتًا. ستُتجَاهل الفروع غير المُختارة، ولن تُستنسَخ التعليمات التي تمّ تجاهلها داخل القالب. مثلًا: template<class T, class ... Rest> void g(T &&p, Rest &&...rs) { // ... p معالجة if constexpr (sizeof...(rs) > 0) g(rs...); // لا تقم بالتهيئة باستخدام قائمة وسائط فارغة } لا يلزم تعريف المتغيّرات والدوال التي استُخدَمت قيمتها (odr-used) حصرًا داخل العبارات المُتجاهلَة (discarded statements)، كما لا تُستخدَم عبارات ‎return‎ المُتجاهلة في استنتاج النوع المعاد من الدالّة. وتختلف العبارة ‎if‎ ‎constexpr‎ عن شيفرات التصريف الشرطية ‎#ifdef. #ifdef، إذ تعتمد حصرًا على الشروط التي يمكن تقييمها في وقت المعالجة الأولية. فلا يمكن استخدام ‎#ifdef‎ للتحكم في تصريف الشيفرة بناءً على قيمة مُعامل القالب، لكن من ناحية أخرى، لا يمكن استخدام ‎if constexpr‎ لتجاهل الشيفرات ذات الصياغة غير الصحيحة، وذلك على خلاف ‎#ifdef‎. if constexpr(false) { foobar; // error; foobar has not been declared std::vector < int > v("hello, world"); // error; no matching constructor } دوال التعبيرات الثابتة (constexpr functions) ستكون الدوالّ المُصرَّح عنها بالكلمة المفتاحية ‎constexpr‎ مُضمّنة (inline) ضمنيًا، وسينتج عن استدعائها تعابير ثابتة، فسيعاد تعبير ثابت إذا كانت الوسائط المُمرّرة إلى الدالة التالية تعابير ثابتة أيضًا: الإصدار ≥ C++‎ 11 constexpr int Sum(int a, int b) { return a + b; } يمكن استخدام نتيجة استدعاء الدالّة كمصفوفة مربوطة (array bound) أو وسيط قالب، كما يمكن استخدامها لتهيئة متغيّر تعبير ثابت (constexpr variable): الإصدار ≥ C++‎ 11 int main() { constexpr int S = Sum(10, 20); int Array[S]; int Array2[Sum(20, 30)]; // مصفوفة مؤلفة من 50 عنصرا في وقت التصريف } لاحظ أنّك إذا أزلت الكلمة المفتاحية ‎constexpr‎ من تعريف النوع المُعاد الخاص بالدالّة، فلن يعمل الإسناد إلى ‎S‎، لأنّ ‎S‎ متغيّر تعبير ثابت ويجب أن تُسند إليه قيمة تصريفية. وبالمثل، لن يكون حجم المصفوفة تعبيرًا ثابتًا إذا لم تكن ‎Sum‎ دالةَ تعبير ثابت. كذلك تستطيع استخدام دوال التعبيرات الثابتة (‎‎constexpr‎ functions) كما لو كانت دوال عادية: الإصدار ≥ C++‎ 11 int a = 20; auto sum = Sum(a, abs(-20)); لن تكون ‎Sum‎ دالّة تعبير ثابت الآن، وستُصرَّف كدالة عادية، وستأخذ وسائط متغيّرة (غير ثابتة)، وتعيد قيمة غير ثابتة، لذا لا تحتاج إلى كتابة دالّتين. هذا يعني أيضًا أنّه إذا حاولت إسناد هذا الاستدعاء إلى متغيّر غير ثابت، فلن ينجح التصريف: الإصدار ≥ C++‎ 11 int a = 20; constexpr auto sum = Sum(a, abs(-20)); وذلك لأنه لا ينبغي أن يُسنَد إلى تعبير ثابت إلّا ثابتة تصريفية (compile-time constant). بالمقابل فإن استدعاء الدالة أعلاه يجعل ‎Sum‎ تعبيرًا غير ثابت (القيمة اليمينية غير ثابتة، على خلاف القيمة اليسارية التي صُرَّح عنها كتعبير ثابت). يجب أيضًا أن تُعيد دالة التعبير الثابت ثابتةً تصريفية (compile-time constant). في المثال التالي، لن تُصرَّف الشيفرة: الإصدار ≥ C++‎ 11 constexpr int Sum(int a, int b) { int a1 = a; // خطأ return a + b; } لأنّ ‎a1‎ متغيّر غير ثابت، ويمنع الدالّة من أن تكون دالة تعبير ثابت ‎constexpr‎ حقيقية، ولن تنجح محاولة جعلها تعبيرًا ثابتًا وإسناد قيمة a لها - نظرًا لأنّ قيمة a (المُعامل الوارد - incoming parameter) ما تزال غير معروفة بعد: الإصدار ≥ C++‎ 11 constexpr int Sum(int a, int b) { constexpr int a1 = a; // خطأ .. وكذلك لن تُصرَّف الشيفرة التالية: الإصدار ≥ C++‎ 11 constexpr int Sum(int a, int b) { return abs(a) + b; // abs(a) + abs(b) أو } وبما أن ‎abs(a)‎ ليست تعبيرًا ثابتًا -ولن تعمل ‎abs(10)‎، إذ لن تعيد ‎abs‎ قيمة من النوع ‎constexpr int‎- فماذا عن الشيفرة التالية؟: الإصدار ≥ C++‎ 11 constexpr int Abs(int v) { return v >= 0 ? v : -v; } constexpr int Sum(int a, int b) { return Abs(a) + b; } لقد صمّمنا الدالّة ‎Abs‎ وجعلناها دالة تعبير ثابت، كما أنّ جسم ‎Abs‎ لن يخرق أيّ قاعدة. كذلك تعطي نتيجة تقييم التعبير هي تعبير ثابت ‎constexpr‎، وذلك في موضع الاستدعاء (داخل ‎Sum‎). ومن ثم يكون استدعاء Sum(-10, 20)‎‎ تعبيرًا ثابتًا في وقت التصريف (compile-time constexpr) ينتج عنه القيمة ‎30‎. هذا الدرس جزء من سلسلة دروس عن C++‎. ترجمة -بتصرّف- للفصلين Chapter 118: static_assert و Chapter 119: constexpr من كتاب C++ Notes for Professionals
  13. نماذج الذاكرة إن حاوَلَت عدّة خيوط الوصول إلى نفس الموضع من الذاكرة، فستدخل في تسابق على البيانات (data race) إذا تسبب عملية واحدة على الأقل من العمليات المُنفّذة في تعديل البيانات -تُعرف باسم عمليات التخزين store operation-، وتتسبّب سباقات البيانات تلك في سلوك غير معرَّف. ولكي نتجنبها، امنع الخيوط من تنفيذ عمليات متضاربة (Conflicting) بشكل متزامن. يمكن استخدام أساسيات التزامن (مثل كائنات التزامن - mutex - وما شابه) لتأمين عمليات الوصول المتضاربة، وقد قدّم الإصدار C++‎ 11 نموذج ذاكرة (Memory Model) جديد، هذا النموذج قدّم طريقتين محمولتين جديدتين لمزامنة الوصول إلى الذاكرة في بيئة متعددة الخيوط، وهما: العمليات الذرية (atomic operations) والأسوار (fences). العمليات الذرية أصبح من الممكن الآن القراءة من موضع معيّن من الذاكرة أو الكتابة فيها باستخدام التحميل الذري (atomic load) وعمليات التخزين الذرية (atomic store)، والتي تُغلَّف في صنف القالب ‎std::atomic<t>‎ من باب التيسير، ويغلّف هذا الصنف قيمةً من النوع ‎t‎، لكن تحميلها وتخزينها إلى الكائن يكون ذريًّا. وهذا القالب ليس متاحًا لجميع الأنواع بل هو متعلق بالتنفيذ، لكن في العادةً يكون متاحًا لمعظم (أو جميع) الأنواع العددية الصحيحة وأنواع المؤشّرات بحيث تكون الأنواع ‎std::atomic<unsigned>‎‎ و std::atomic<std::vector<foo> *>‎ متاحة، على خلاف ‎‎std::atomic<std::pair<bool,char>‎>‎‎‎. تتميّز العمليات الذرية بالخاصّيات التالية: يمكن إجراء جميع العمليات الذرية بشكل متزامن في عدّة خيوط دون الخوف من التسبب في سلوك غير معرَّف. سيرى التحميل الذري (atomic load) القيمة الأولية التي بُنِي الكائن الذري عليها، أو القيمة المكتوبة فيه عبر عملية التخزين الذرية. تُرتَّب عمليات التخزين الذرية (Atomic stores) في كائن ذري معيّن بنفس الطريقة في جميع الخيوط، وإذا رأى خيطٌ قيمةَ عملية تخزين ذرية ما من قبل، فإنّ عمليات التحميل الذري اللاحقة سترى إما القيمة نفسها أو القيمة المُخزّنة بواسطة عملية التخزين الذرية اللاحقة. تسمح عمليات القراءة-التعديل-الكتابة (read-modify-write) الذرية بالتحميل الذري والتخزين الذري دون حدوث أي تخزين ذرّي آخر بينهما. على سبيل المثال، يمكن للمرء أن يزيد العدّادَ ذريًّا (atomically increment) عبر عدة خيوط تلقائيًا، ولن تُفقد أيّ زيادة حتى لو كان هناك تنافر (contention) بين الخيوط. تتلقى العمليات الذرية مُعاملًا اختياريًا من نوع ‎std::memory_order‎، يُعرِّف الخاصّيات الإضافية للعملية بخصوص مواضع الذاكرة الأخرى. std::memory_order الشرح std::memory_order_relaxed لا قيود إضافية std::memory_order_release-std::memory_order_acquire‎ إذا رأت ‎load-acquire‎ القيمة المُخزّنة بواسطة ‎store-release‎ فإنّ التخزينات المتسلسلة (stores sequenced) قبل ‎store-release‎ ستحدث قبل التحميلات المتسلسلة (loads sequenced) بعد اكتساب التحميل (load acquire). std::memory_order_consume مثل ‎memory_order_acquire‎ ولكن تعمل مع الأحمال غير المستقلة (dependent loads) وحسب std::memory_order_acq_rel تجمع ‎load-acquire‎ و ‎store-release‎ std::memory_order_seq_cst تناسق تسلسلي (sequential consistency) table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; } تسمح وسوم ترتيب الذاكرة أعلاه بثلاث نماذج من ترتيب الذاكرة: الاتساق المتسلسل sequential consistency. الترتيب المتراخي Relaxed Ordering. ترتيب اكتساب-تحرير release-acquire واكتساب-استهلاك release-consume. الاتساق المتسلسل (Sequential Consistency) إذا لم يعُرِّف ترتيب الذاكرة الخاص بعملية ذرية معيّنة فسيتمّ اللجوء إلى الترتيب الافتراضي، والذي هو الاتساق المتسلسل. يمكن أيضًا اختيار هذا الوضع صراحة عبر الوسْم ‎std::memory_order_seq_cst‎. وفي هذا الترتيب، لا يمكن لأيّ عملية على الذاكرة أن تتجاوز العملية الذرية، وستحدث جميع عمليات الذاكرة التسلسلية السابقة للعملية الذرية قبل العملية الذرية، كما ستحدث العملية الذرية قبل جميع عمليات الذاكرة المتسلسلة اللاحقة. هذا هو الوضع الأسهل والأوضح، لكنه قد يؤدّي إلى إضعاف الأداء ويمنع كل تحسينات المصرّف التي قد تحاول إعادة ترتيب العمليات التي تأتي بعد العملية الذرية. الترتيب المتراخي (Relaxed Ordering) الترتيب المتراخي للذاكرة هو نقيض ترتيب الاتساق المتسلسل، ويمكن تعريفه باستخدام الوسم std::memory_order_relaxed. لا تفرض العمليات الذرية المتراخية أيّ قيود على عمليات الذاكرة الأخرى، ولا يبقى تأثير سوى أنّ العملية بحد ذاتها ستبقى ذرية. ترتيب التحرير-الاكتساب (Release-Acquire Ordering) يمكن وسم عملية تخزين ذرية باستخدام ‎std::memory_order_release‎، كما يمكن وسم عملية تحميل ذري باستخدام ‎std::memory_order_acquire‎. وتُسمّى العملية الأولى تخزين-تحرير (ذري) (‎(atomic) store-release) في حين تسمّى العملية الثانية تحميل-اكتساب (ذري) (‎(atomic) load-acquire‎). وعندما ترى عمليةُ "التحميل-الاكتساب" القيمةَ المكتوبة من قبل عملية (التخزين-التحرير) فسيحدث ما يلي: ستصبح جميع عمليات التخزين المُسلسلة التي تسبق عملية (التخزين-التحرير) مرئيّة لعمليات التحميل المُسلسلة بعد عملية (التحميل-الاكتساب). يمكن أن تحصل عمليات (القراءة-التعديل-الكتابة) الذرية أيضًا على الوسم التراكمي ‎std::memory_order_acq_rel‎. هذا يجعل الجزء المتعلّق بالتحميل الذري من العملية عبارة عن عملية تحميل-اكتساب ذرّية، بينما يصبح الجزء المتعلق بالتخزين الذري عبارة عن عملية تخزين-تحرير ذرّية. لا يُسمح للمُصرِّف بنقل عمليات التخزين الموجودة بعد عملية تخزين-تحرير ذرّية ما، كما لا يُسمح له أيضًا بنقل عمليات التحميل التي تسبق عملية تحميل-اكتساب ذرية (أو تحميل-استهلاك). لاحظ أيضًا أنّه لا توجد عملية تحميل-تحرير ذرّية (atomic load-release)، ولا عملية تخزين-اكتساب ذرّية (atomic store-acquire)، وأيّ محاولة لإنشاء مثل هذه العمليات سينجم عنها عمليات متراخية (relaxed operations). ترتيب تحرير-استهلاك (Release-Consume Ordering) عمليات تحرير-استهلاك تشبه عمليات تحرير-اكتساب، لكن هذه المرّة يوسَم الحمل الذري باستخدام std::memory_order_consume لكي يصبح عملية تحميل-استهلاك (ذرية) - ‎(atomic) load-consume operation. هذا الوضع يشبه عملية تحرير-اكتساب، مع اختلاف وحيد هو أنّه من بين عمليات التحميل المُسلسلة الموجودة بعد عملية تحميل-استهلاك، فلن تُرتَّب إلا تلك التي تعتمد على القيمة التي حُمِّلت بواسطة عملية تحميل-استهلاك. الأسوار (Fences) تسمح الأسوار بترتيب عمليات الذاكرة بين الخيوط، وقد يكون السور إمّا سور تحرير (release fence)، أو سور اكتساب (acquire fence). وإذا حدث سور التحرير قبل سور الاكتساب، فستكون المخازن المتسلسلة قبل سور التحرير مرئية للأحمال المتسلسلة بعد سور الاكتساب، وإن أردت ضمان تقديم سور التحرير قبل سور الاكتساب فاستخدام بدائل التزامن الأخرى، بما في ذلك العمليات الذرية المتراخية. فائدة نموذج الذاكرة انظر المثال التالي: int x, y; bool ready = false; void init() { x = 2; y = 3; ready = true; } void use() { if (ready) std::cout << x + y; } يستدعي أحد الخيطين دالة ‎init()‎، بينما يستدعي الخيط الآخر (أو معالج الإشارة) الدالةَ ‎use()‎. قد يتوقع المرء أنّ الدالّة ‎use()‎ إمّا ستطبع القيمة ‎5‎، أو لن تفعل أيّ شيء، لكن هذه الحالة قد لا تحدث كل مرة، لعدّة أسباب: قد تعيد وحدة المعالجة المركزية ترتيب عمليات الكتابة التي تحدث في ‎init()‎ بحيث تبدو الشيفرة التي ستنفذ على الشكل التالي: void init() { ready = true; x = 2; y = 3; } قد تعيد وحدة المعالجة المركزية ترتيب عمليّات القراءات التي تحدث في ‎use()‎ بحيث تصبح الشيفرة التي ستُنفّذ على الشكل التالي: void use() { int local_x = x; int local_y = y; if (ready) std::cout << local_x + local_y; } قد يقرّر مصرّف C++‎ إعادة ترتيب البرنامج بطريقة مماثلة لتحسين الأداء. لو كان البرنامج يُنفّذ في خيط واحد فلا يمكن أن يتغيّر سلوك البرنامج نتيجة لإعادة الترتيب، ذلك لأنّ الخيط لا يمكن أن يخلط بين استدعائي ‎init()‎ و ‎use()‎. أمّا في حالة الخيوط المتعددة، فقد يرى أحد الخيوط جزءًا من عمليات الكتابة التي يؤدّيها الخيط الآخر، حيث أنّ ‎use()‎ قد ترى ‎ready==true‎ وكذلك المُهملات (garbage) في ‎x‎ أو ‎y‎ أو كليهما. يسمح نموذج الذاكرة في C++‎ للمبرمج بأن يعيد تعريف عمليات إعادة الترتيب المسموح بها أو غير المسموح بها، بحيث يمكن توقّع سلوك البرامج متعددة الخيوط. يمكن إعادة كتابة المثال أعلاه بطريقة ملائمة لتعدّد الخيوط على النحو التالي: int x, y; std::atomic < bool > ready { false }; void init() { x = 2; y = 3; ready.store(true, std::memory_order_release); } void use() { if (ready.load(std::memory_order_acquire)) std::cout << x + y; } في المثال أعلاه، تُجري دالة ‎init()‎ عملية تخزين-تحرير ذرية، هذا لن يخزّن قيمة ‎true‎ في ‎ready‎ وحسب، ولكن سيُخطِر أيضًا المُصرِّف بأنّه لا يمكن نقل هذه العملية قبل عمليات الكتابة المتسلسلة التي تسبقها. كذلك تُجري الدالّة ‎use()‎ عملية تحميل-اكتساب ذرية. إذ تقرأ القيمة الحالية لـ ‎ready‎، وتمنع المُصرِّف من تقديم عمليات القراءة المتسلسلة اللاحقة قبل عملية تحميل-اكتساب الذرية. هذه العمليات الذرية تجعل المصرّف يضع الإرشادات الضرورية المتعلقة بالجهاز لإبلاغ وحدة المعالجة المركزية بالامتناع عن تقديم عمليات إعادة الترتيب غير المرغوب فيها. ونظرًا لأنّ عملية تخزين-تحرير الذرية تقع في نفس موقع الذاكرة الخاص بعملية تحميل-اكتساب، ينصّ نموذج الذاكرة على أنّه إذا كانت عملية تحميل-اكتساب ترى القيمة المكتوبة بواسطة عملية تخزين-تحرير، فإنّ جميع عمليات الكتابة التي تُنفّذ بواسطة دالة الخيط ‎init()‎ التي تسبق عملية التخزين-التحرير (store-release) ستكون مرئية للتحميلات التي تنفّذها دالة الخيط ‎use()‎ بعد عملية تحميل-اكتساب. أي أنّه في حال رأت الدالةُ ‎use()‎ تعليمة ‎ready==true‎، فسترى كذلك بالضرورة ‎x==2‎ و ‎y==3‎. لاحظ أنّ المُصرِّف ووحدة المعالجة المركزية لا يزالان يستطيعان الكتابة في ‎y‎ قبل الكتابة في ‎x‎، وبالمثل يمكن أن تحدث عمليات القراءة من المتغيّرات في ‎use()‎ وفق أيّ ترتيب. مثال على الأسوار (Fences) يمكن أيضًا تقديم المثال أعلاه باستخدام الأسوار والعمليات الذرية المتراخية: int x, y; std::atomic < bool > ready { false }; void init() { x = 2; y = 3; atomic_thread_fence(std::memory_order_release); ready.store(true, std::memory_order_relaxed); } void use() { if (ready.load(std::memory_order_relaxed)) { atomic_thread_fence(std::memory_order_acquire); std::cout << x + y; } } إذا رأت عملية التحميل الذرية القيمةَ المكتوبة بواسطة عملية التخزين الذري، فسيحدث التخزين قبل التحميل، وكذلك الحال مع الأسوار: يحدُث تحرير السور قبل اكتساب السور، ما يجعل عملية الكتابة في ‎x‎ و ‎y‎ التي تسبق سور التحرير مرئية للعبارة ‎std::cout‎ التي تلي اكتساب السور. قد يكون استخدام السور مفيدًا في حال كان يقلّل العدد الإجمالي لعمليات الاكتساب أو التحرير أو عمليات المزامنة الأخرى. مثلّا: void block_and_use() { while (!ready.load(std::memory_order_relaxed)) ; atomic_thread_fence(std::memory_order_acquire); std::cout << x + y; } ويستمرّ تنفيذ الدالّة ‎block_and_use()‎ إلى أن تُضبَط قيمة راية ‎ready‎ بمساعدة التحميل الذري المتراخي، ثم يُستخدَم سور اكتساب واحد لتوفير ترتيب الذاكرة المطلوب. إدارة الذاكرة (Memory management) التخزين الحرّ (Free Storage) مصطلح "الكومة" (heap) هو مصطلح عام في الحوسبة يشير إلى مساحة من الذاكرة يمكن تخصيص أجزاء منها أو تحريرها بشكل مستقل عن الذاكرة التي يوفرها المكدّس (stack). ويسمي المعيار في ++C هذه المساحة بالتخزين الحر، وهو أكثر دقة من الكومة. وقد تبقى مناطق الذاكرة المخصصة للتخزين الحرّ حتّى بعد الخروج من النطاق الأصلي الذي خُصِّصت فيه، ويمكن تخصيص ذاكرة للبيانات في التخزين الحر إن كانت مساحة البيانات أكبر من أن تُخزَّن في المكدّس. تُستخدم الكلمتان المفتاحيتان new و delete لتخصيص الذاكرة الخام (Raw memory) وتحريرها. float *foo = nullptr; { *foo = new float; // تخصيص ذاكرة لعدد عشري float bar; // تخصيص المكدّس } // ما تزال باقية foo لكنّ bar نهاية delete foo; // وهذا يؤدي إلى جعل المؤشّر غير صالح ،pF حذف ذاكرة العدد العشري الموجودة عند foo = nullptr; // من الممارسات السيئة `nullptr` يُعد ضبط المؤشر عند القيمة. من الممكن أيضًا تخصيص ذاكرة للمصفوفات ذات الحجم الثابت بكلمتيْ new و delete، لكن مع صيغة مختلفة قليلاً، ذلك أن تخصيص ذاكرة المصفوفات يختلف عن تخصيص الذاكرة للكائنات الأخرى، وسيؤدّي خلط الاثنتين إلى عطب في الكومة (heap corruption). ويؤدي تخصيص ذاكرة المصفوفات أيضًا إلى تخصيص ذاكرة مخصّصة لتعقّب حجم المصفوفة، وذلك لأجل استخدامها عند حذف المصفوفة لاحقًا (تتعلق بالتنفيذ). // تخصيص ذاكرة مؤلّفة من 256 عددًا صحيحًا int *foo = new int[256]; // حذف المصفوفة delete[] foo; سيُنفَّذ المنشئ والمدمّر -كما هو حال الكائنات في المكدّس (Stack based objects)- عند استخدام new و delete بدلًا من malloc و free، لهذا فإنّ خيار new و delete خير من malloc و free. انظر المثال التالي حيث نخصص ذاكرة لنوعٍ ComplexType، ونستدعي منشئه، ثم نستدعي مدمر ()ComplexType ونحذف ذاكرة Complextype عند الموضع pC. struct ComplexType { int a = 0; ComplexType() { std::cout << "Ctor" << std::endl; } ~ComplexType() { std::cout << "Dtor" << std::endl; } }; ComplexType *foo = new ComplexType(); delete foo; الإصدار ≥ C++‎ 11 يوصى باستخدام المؤشّرات الذكية منذ الإصدار C++‎ 11 للإشارة إلى المِلكِيّة. الإصدار ≥ C++‎ 14 C++‎ 14 أضافت ‎std::make_unique‎ إلى مكتبة القوالب القياسية STL، مغيرة بذلك الإرشادات لتفضيل ‎std::make_unique‎ أو std::make_shared على استخدام new و delete. new قد لا ترغب في بعض الحالات في الاعتماد على التخزين الحرّ (Free Store) لتخصيص الذاكرة، وتريد تخصيص ذاكرة مخصصة باستخدام ‎new‎. عندئذ يمكنك استخدام ‎Placement New‎، بحيث تخبر المعامل "new" بأن يخصّص الذاكرة من موضع مُخصص مسبقًا. انظر: int a4byteInteger; char *a4byteChar = new (&a4byteInteger) char[4]; في المثال السابق ، الذاكرة المشار إليها عبر ‎a4byteChar‎ هي 4 بايت، مخصصة "للمكدّس" عبر المتغيّر الصحيح ‎a4byteInteger‎. وفائدة هذا النوع من تخصيص الذاكرة أنه سيكون للمبرمجين تحكّم كامل في التخصيص، فبما أن ذاكرة ‎a4byteInteger‎ في المثال أعلاه مُخصّصة في المكدّس فلن تحتاج إلى استدعاء صريح لـ a4byteChar delete. يمكن تحقيق نفس السلوك في حالة تخصيص ذاكرة ديناميكية أيضًا. مثلّا: int *a8byteDynamicInteger = new int[2]; char *a8byteChar = new (a8byteDynamicInteger) char[8]; يشير مؤشّر الذاكرة ‎a8byteChar‎ في هذه الحالة إلى الذاكرة الديناميكية المخصصة عبر ‎a8byteDynamicInteger‎. لكن مع ذلك، سنحتاج في هذه الحالة إلى استدعاء ‎a8byteDynamicInteger‎ صراحةً لتحرير الذاكرة. هذا مثال آخر: #include <complex> #include <iostream> struct ComplexType { int a; ComplexType(): a(0) {} ~ComplexType() {} }; int main() { char* dynArray = new char[256]; نستدعي منشئ ComplexType لتهيئة الذاكرة كـ ComplexType، نتابع … new((void* ) dynArray) ComplexType(); // تنظيف الذاكرة بعد الانتهاء reinterpret_cast<ComplexType*>(dynArray)->~ComplexType(); delete[] dynArray; // placement new يمكن أيضا استخدام ذاكرة المكدّس مع alignas(ComplexType) char localArray[256]; //alignas() available since C++11 new((void* ) localArray) ComplexType(); // لا تحتاج إلى استدعاء المدمّر إلا لذاكرة المكدّس reinterpret_cast<ComplexType*>(localArray)->~ComplexType(); return 0; } المكدّس المكدّس هو منطقة صغيرة من الذاكرة توضع فيها القيم المؤقتة أثناء التنفيذ، ويعدّ تخصيص البيانات فيه سريعًا جدًا مقارنة بتخصيصها في الكومة، إذ أنّ الذاكرة كلها مسنَدة سلفًا لهذا الغرض. int main() { int a = 0; // مُخزّنة على المكدّس return a; } وقد سُمِّي مكدّسًا لأنّ الاستدعاءات المتسلسلة للدوال ستكون لها ذاكرة مؤقتة "مُكدّسة" فوق بعضها البعض، وكل واحدة ستستخدم قسمًا صغيرًا منفصلًا من الذاكرة. في المثال التالي، ستوضع f على المكدس في النهاية بعد كل وضع كل شيء (انظر 1)، وتوضع d كذلك بعد كل شيء في نطاق ()main (انظر 2): float bar() { // (1) float f = 2; return f; } double foo() { // (2) double d = bar(); return d; } int main() { // foo() لا تُخزّن في المكدّس أيّ متغيرات خاصّة بالمستخدم إلى حين استدعاء return (int) foo(); } ستبقى البيانات المخزّنة على المكدّس صالحة ما دام النطاق الذي خَصص المتغيّر نشطًًا. int* pA = nullptr; void foo() { int b = *pA; pA = &b; } int main() { int a = 5; pA = &a; foo(); // خارج النطاق pA سلوك غير معرَّف، أصبحت القيمة التي يشير إليها a = *pA; } هذا الدرس جزء من سلسلة دروس عن C++‎. ترجمة -بتصرّف- للفصل Chapter 115: Memory management و Chapter 116: C++11 Memory Model من كتاب C++ Notes for Professionals
  14. القيم الثنائية مصنفة النوع المُعرّفة من المستخدم (Self-made user-defined literal for binary) رغم إمكانية كتابة عدد ثنائي في C++‎ 14 على النحو التالي: int number =0b0001'0101; // ==21 إلا أننا في الأسطر التالية سنستعرض مثالًا مشهورًا يوفّر طريقة أخرى ذاتية التنفيذ للأعداد الثنائية. لاحظ أن برنامج توسيع القالب التالي يعمل في وقت التصريف. template < char FIRST, char...REST > struct binary { static_assert(FIRST == '0' || FIRST == '1', "invalid binary digit"); enum { value = ((FIRST - '0') << sizeof...(REST)) + binary < REST... > ::value }; }; template < > struct binary < '0' > { enum { value = 0 }; }; template < > struct binary < '1' > { enum { value = 1 }; }; // عامل قيمة خام مصنفة النوع template < char...LITERAL > inline constexpr unsigned int operator "" _b() { return binary < LITERAL... > ::value; } // عاملُ قيمةٍ خامٍ مصنَّفةِ النوع template < char...LITERAL > inline constexpr unsigned int operator "" _B() { return binary < LITERAL... > ::value; } #include <iostream> int main() { std::cout << 10101_B << ", " << 011011000111_b << '\n'; // تطبع 21, 1735 } القيم مصنفة النوع المعيارية المُعرّفة من المستخدم (Standard user-defined literals for duration) الإصدار ≥ C++‎ 14 فيما يلي قيمُ مدةٍ مصنَّفةِ النوع، ومعرَّفة من قِبل المستخدم (duration user literals)، مصرح عنها في فضاء الاسم namespace std::literals::chrono_literals، حيث ‎literals‎ و ‎chrono_literals‎ هما فضاءا اسم ضمنيّان (inline namespaces). يمكن الوصول إلى هذه العوامل باستخدام using namespace std::literals و using namespace std::chrono_literals و using namespace std::literals::chrono_literals. #include <chrono> #include <iostream> int main() { using namespace std::literals::chrono_literals; std::chrono::nanoseconds t1 = 600ns; std::chrono::microseconds t2 = 42us; std::chrono::milliseconds t3 = 51ms; std::chrono::seconds t4 = 61s; std::chrono::minutes t5 = 88min; auto t6 = 2 * 0.5h; auto total = t1 + t2 + t3 + t4 + t5 + t6; std::cout.precision(13); std::cout << total.count() << " nanoseconds" << std::endl; // 8941051042600 nanoseconds std::cout << std::chrono::duration_cast < std::chrono::hours > (total).count() << " hours" << std::endl; // ساعتان } القيم مصنَّفة النوع المُعرّفة من المستخدم، ذات قيَم long double يوضّح المثال التالي كيفية استخدام قيم مصنَّفة النوع، مُعرّفة من المستخدم وذات قيَم long double: #include <iostream> long double operator "" _km(long double val) { return val * 1000.0; } long double operator "" _mi(long double val) { return val * 1609.344; } int main() { std::cout << "3 km = " << 3.0_km << " m\n"; std::cout << "3 mi = " << 3.0_mi << " m\n"; return 0; } خرج هذا البرنامج هو: 3 km = 3000 m 3 mi = 4828.03 m السلاسل النصية المجردة القياسية والمعرّفة من المستخدم (Standard user-defined literals for strings) الإصدار ≥ C++‎ 14 فيما يلي سلاسل نصية مجردةٌ ومُعرّفة من المستخدم (string user literals)، مُصرَّح عنها في namespace std::literals::string_literals، حيث ‎literals‎ و ‎string_literals‎ هما فضاءا اسم مُضمّنان. ويمكن الوصول إلى هذه العوامل باستخدام using namespace std::literals و using namespace std::string_literals و using namespace std::literals::string_literals. #include <codecvt> #include <iostream> #include <locale> #include <string> int main() { using namespace std::literals::string_literals; std::string s = "hello world"s; std::u16string s16 = u"hello world"s; std::u32string s32 = U"hello world"s; std::wstring ws = L"hello world"s; std::cout << s << std::endl; std::wstring_convert < std::codecvt_utf8_utf16 < char16_t > , char16_t > utf16conv; std::cout << utf16conv.to_bytes(s16) << std::endl; std::wstring_convert < std::codecvt_utf8_utf16 < char32_t > , char32_t > utf32conv; std::cout << utf32conv.to_bytes(s32) << std::endl; std::wcout << ws << std::endl; } ملاحظة: قد تحتوي السلاسل النصية المجردة على المحرف ‎\0‎، انظر المثال التالي: // "foo"s النصية سينتج عنها C منشئات سلاسل std::string s1 = "foo\0\0bar"; // '\0' تحتوي هذه السلسلة النصية في وسطها على محرفين std::string s2 = "foo\0\0bar"s; القيم مصنفة النوع المركّبة المعرّفة من المستخدم (Standard user-defined literals for complex) الإصدار ≥ C++‎ 14 فيما يلي، قيم مركّبة مصنفة النوع ومعرّفة من المستخدم، مُصرًّح عنها فيnamespace std::literals::complex_literals، حيث ‎literals‎ و ‎complex_literals‎ فضاءا اسم ضمنيان. يمكن الوصول إلى هذه العوامل باستخدام using namespace std::literals و using namespace std::complex_literals و using namespace std::literals::complex_literals. #include <complex> #include <iostream> int main() { using namespace std::literals::complex_literals; std::complex < double > c = 2.0 + 1i; // {2.0, 1.} std::complex < float > cf = 2.0f + 1if; // {2.0f, 1.f} std::complex < long double > cl = 2.0L + 1il; // {2.0L, 1.L} std::cout << "abs" << c << " = " << abs(c) << std::endl; // abs(2,1) = 2.23607 std::cout << "abs" << cf << " = " << abs(cf) << std::endl; // abs(2,1) = 2.23607 std::cout << "abs" << cl << " = " << abs(cl) << std::endl; // abs(2,1) = 2.23607 } هذا الدرس جزء من سلسلة دروس عن C++‎. ترجمة -بتصرّف- للفصل Chapter 114: User-Defined Literals من كتاب C++ Notes for Professionals
  15. سنستعرض في هذا الدرس بعض أنماط التصميم الشهيرة في C++‎ ثم سنتطرق سريعًا إلى مفهوم إعادة التصميم (Refactoring) والنمط Goto Cleanup المتَّبع. نمط المحوِّل (Adapter Pattern) يتيح نمط المحوِّل للأصناف غير المتوافقة أن تعمل معًا، والسبب الأساسي في استخدامه تكمن في أنّه يمكّن المطوّرين من إعادة استخدام البرامج الموجودة عبر تعديل الواجهة فقط. يعتمد نمط المحول على تركيب الكائنات (object composition). العميل يستدعي العملية على المحوِّل. يستدعي المحوِّل الكائن المحوَّل Adaptee لتنفيذ العملية. تُبنى المكدّسات (stacks) في مكتبة القوالب القياسية STL على المتجهات، فمثلًا: عندما ينفّذ المُكدّس الدّالةَ push()‎، فإنّ المتجه الأساسي (underlying vector) سينفذ التابع vector::push_back()‎. انظر المثال التالي: #include <iostream> // الواجهة المقصودة class Rectangle { public: virtual void draw() = 0; }; // المركّب القديم - المحوَّل class LegacyRectangle { public: LegacyRectangle(int x1, int y1, int x2, int y2) { x1_ = x1; y1_ = y1; x2_ = x2; y2_ = y2; std::cout << "LegacyRectangle(x1,y1,x2,y2)\n"; } void oldDraw() { std::cout << "LegacyRectangle: oldDraw(). \n"; } private: int x1_; int y1_; int x2_; int y2_; }; // Adapter wrapper مغلِّف المحوَّل class RectangleAdapter: public Rectangle, private LegacyRectangle { public: RectangleAdapter(int x, int y, int w, int h): LegacyRectangle(x, y, x + w, y + h) { std::cout << "RectangleAdapter(x,y,x+w,x+h)\n"; } void draw() { std::cout << "RectangleAdapter: draw().\n"; oldDraw(); } }; int main() { int x = 20, y = 50, w = 300, h = 200; Rectangle * r = new RectangleAdapter(x, y, w, h); r -> draw(); } // :الخرج //LegacyRectangle(x1,y1,x2,y2) //RectangleAdapter(x,y,x+w,x+h) شرح الشيفرة أعلاه: يعتقد العميل أنّه يتحدث إلى ‎Rectangle‎ الهدف هو الصنف ‎Rectangle‎، وهو الذي سيستدعي العميلُ التابعَ عليه. Rectangle * r = new RectangleAdapter(x, y, w, h); r -> draw(); لاحظ أنّ صنف المحوِّل (adapter class) يستخدم الوراثة المتعدّدة. class RectangleAdapter: public Rectangle, private LegacyRectangle { ... } يتيح المحوِّل‎RectangleAdapter‎ للصنف ‎LegacyRectangle‎ الاستجابة للطلب (استدعاء ‎draw()‎ على ‎Rectangle‎) عن طريق وراثة الصنفين معًا. لا يملك الصنف ‎LegacyRectangle‎ نفس التوابع (‎draw()‎) التي يملكها ‎Rectangle‎، لكن يمكن أن يأخذ Adapter(RectangleAdapter)‎ استدعاءات التابعِ ‎Rectangle‎ ثمّ يعود لاستدعاء التابع ‎LegacyRectangle‎ على ‎oldDraw()‎. class RectangleAdapter: public Rectangle, private LegacyRectangle { public: RectangleAdapter(int x, int y, int w, int h): LegacyRectangle(x, y, x + w, y + h) { std::cout << "RectangleAdapter(x,y,x+w,x+h)\n"; } void draw() { std::cout << "RectangleAdapter: draw().\n"; oldDraw(); } }; يُترجمُ نمط المحوِّل واجهةَ صنف معيّن إلى واجهة أخرى متوافقة، ولكن مختلفة. لذلك، فهو يشبه نمط الوكيل من حيث أنّه مغلّف أحادي المكوّنات (single-component wrapper)، لكن قد تكون واجهة الصنف المحوَّل والصنف الأصلي مختلفة. ويمكن استخدام نمط المحوِّل لإظهار واجهة برمجية (API) معيّنة للسماح لها بالعمل مع شيفرات أخرى كما رأينا في المثال أعلاه. أيضًا، يمكننا أن نأخذ واجهات غير متجانسة، ونحوّلها لواجهة برمجية موحّدة ومتسقة. لدى نمط الجسر بنية مشابهة للكائنات المحوِّلة، بيْد أنّ للجسور هدفًا مختلفًا، إذ يُرادُ منها فصل الواجهة عن التقديم، حتّى يسهل تعديلها بشكل مستقل. أمّا المحوِّل فيُراد منه تعديل واجهة كائن موجود. نمط المراقب (Observer pattern) الهدف من نمط المراقب هو تعريف اعتمادية واحد-إلى-متعدد (one-to-many) بين الكائنات، بحيث إذا تغيرت حالة كائن تُرسل إشعارات إلى جميع الكائنات المتعلّقة به وتحديثها تلقائيًا. يعرِّف الهدف (subject) والمراقبُون (observers) اعتمادية الواحد-إلى-متعدد، وفي هذه الاعتمادية يعتمد المراقبون على الأهداف، وعندما تتغيّر حالة الهدف يتم إشعار المراقبين تلقائيًا. وبناءً على ذلك يمكن تحديث المراقبين وإعطاؤهم قيمًا جديدة. فيما يلي مثال من كتاب "Design Patterns" من تأليف جاما (Gamma). #include <iostream> #include <vector> class Subject; class Observer { public: virtual ~Observer() = default; virtual void Update(Subject&) = 0; }; class Subject { public: virtual ~Subject() = default; void Attach(Observer& o) { observers.push_back(&o); } void Detach(Observer& o) { observers.erase(std::remove(observers.begin(), observers.end(), &o)); } void Notify() { for (auto* o : observers) { o->Update(*this); } } private: std::vector<Observer*> observers; }; class ClockTimer: public Subject { public: void SetTime(int hour, int minute, int second) { this -> hour = hour; this -> minute = minute; this -> second = second; Notify(); } int GetHour() const { return hour; } int GetMinute() const { return minute; } int GetSecond() const { return second; } private: int hour; int minute; int second; }; class DigitalClock: public Observer { public: explicit DigitalClock(ClockTimer& s) : subject(s) { subject.Attach(*this); } ~DigitalClock() { subject.Detach( *this); } void Update(Subject& theChangedSubject) override { if ( &theChangedSubject == &subject) { Draw(); } } void Draw() { int hour = subject.GetHour(); int minute = subject.GetMinute(); int second = subject.GetSecond(); std::cout << "Digital time is " << hour << ":" << minute << ":" << second << std::endl; } private: ClockTimer& subject; }; class AnalogClock: public Observer { public: explicit AnalogClock(ClockTimer& s): subject(s) { subject.Attach( *this); } ~AnalogClock() { subject.Detach( * this);} void Update(Subject& theChangedSubject) override { if ( &theChangedSubject == &subject) { Draw(); } } void Draw() { int hour = subject.GetHour(); int minute = subject.GetMinute(); int second = subject.GetSecond(); std::cout << "Analog time is " << hour << ":" << minute << ":" << second << std::endl; } private: ClockTimer& subject; }; int main() { ClockTimer timer; DigitalClock digitalClock(timer); AnalogClock analogClock(timer); timer.SetTime(14, 41, 36); } الخرج: Digital time is 14: 41: 36 Analog time is 14: 41: 36 توضّح النقاط التالية ملخّص نمط المراقب: تستخدم الكائنات (‎DigitalClock‎ أو ‎AnalogClock‎) واجهات الموضوع (‎Attach()‎ أو ‎Detach()‎) إمّا لأجل الاشتراك (subscribe) كمراقبين، أو إلغاء الاشتراك (إزالة أنفسهم) من كونهم مراقبين (‎subject.Attach(*this);‎، ‎subject.Detach(*this);‎. يمكن أن يكون لكل موضوع عدّة مراقبين (‎vector<Observer*> observers;‎). يحتاج جميع المراقبين إلى تنفيذ واجهة المراقب (Observer interface). لدى هذه الواجهة تابع واحد فقط، وهو ‎Update()‎، ويُستدعى عند تغيّر حالة الموضوع (‎Update(Subject &)‎) بالإضافة إلى التابعين ‎Attach()‎ و ‎Detach()‎، ينفِّذ الهدف الحقيقي التابعَ ‎Notify()‎ الذي يُستخدم لتحديث جميع المراقبين الحاليين عندما تتغيّر الحالة، لكن تتم جميعها في هذه الحالة في الصنف الأب، Subject (Subject::Attach (Observer&)‎ و ‎void Subject::Detach(Observer&)‎ و void Subject::Notify()‎. قد يحتوي الكائن الحقيقي أيضًا على توابع لضبط قيمة حالته، أو الحصول عليها. يمكن أن تكون المراقبات الحقيقية أيّ صنف ينفذ واجهة المراقب (Observer interface)، ويشترك كل مراقب مع هدف حقيقي ليحصل على التحديثات (‎subject.Attach(*this);‎). كائنا نمط المراقب مترابطان بشكل طفيف، إذ يمكنهما التفاعل مع بعضهما البعض، لكنّ معرفتها ببعضهما محدودة. الإشارات والفتحات (Slots) الإشارات والفتحات (Slots) هي بنية لغوية قُدِّمت في Qt، وتسهّل على المطوّرين تقديم نمط المراقب دون الحاجة لاستخدام الشيفرات المُتداولة (boilerplate code). الفكرة الرئيسية وراء الإشارات والفتحات هي أنّ عناصر التحكم (controls) التي تُعرف أيضًا باسم الودجات widgets) يمكنها إرسال إشارات تحتوي على معلومات حول الحدث، والتي يمكن استقبالها من قبل عناصر تحكم أخرى باستخدام دوال خاصة تُعرف باسم الفتحات (slots)، وهي أعضاء أصناف في Qt. يتوافق نظام الإشارة / الفتحة مع تصميم واجهات المستخدم الرسومية، كما يمكن استخدام نظام الإشارة-الفتحة للدخل / الخرج غير المتزامن (asynchronous I/O) بما في ذلك المقابس sockets، والأنابيب pipes، والأجهزة التسلسلية serial devices، وغيرها مما يختص بإشعارات الأحداث أو لربط أزمنة الأحداث (timeout events) مع نُسخ الكائن والتوابع أو الدوالّ المناسبة. لا يلزم كتابة شيفرة خاصة بالتسجيل/إلغاء التسجيل/الاستدعاء، لأنّ الكائن الوصفي للمصرّف (Meta Object Compiler أو اختصارًا MOC) الخاصّ بـ Qt يولّد البنية الأساسية اللازمة تلقائيًا . تدعم لغة C#‎ أيضًا إنشاءات مشابهة، لكنها تستخدم مصطلحات وصيغة مختلفة: فالإشارات تسمّى أحداثًا، والفتحات تسمّى مفوِّضَات (delegates). إضافة إلى ذلك يمكن أن يكون المفوّض متغيّرًا محليًا، مثل مؤشّرات الدوال، بينما يجب أن تكون الفتحة في Qt عضوًا في صنف. نمط المصنع (Factory Pattern) يقسّم نمط المصنع (Factory pattern) عمليّة إنشاء الكائنات، ويتيح الإنشاء بالاسم باستخدام واجهة مشتركة: class Animal { public: virtual std::shared_ptr < Animal > clone() const = 0; virtual std::string getname() const = 0; }; class Bear: public Animal { public: virtual std::shared_ptr < Animal > clone() const override { return std::make_shared < Bear > ( * this); } virtual std::string getname() const override { return "bear"; } }; class Cat: public Animal { public: virtual std::shared_ptr < Animal > clone() const override { return std::make_shared < Cat > ( *this); } virtual std::string getname() const override { return "cat"; } }; class AnimalFactory { public: static std::shared_ptr < Animal > getAnimal(const std::string& name) { if (name == "bear") return std::make_shared < Bear > (); if (name == "cat") return std::shared_ptr < Cat > (); return nullptr; } }; نمط الباني يفصل نمط الباني (Builder Pattern) عملية إنشاء الكائن عن الكائن نفسه، والفكرة الرئيسية وراء ذلك هي أنّ الكائن ليس عليه مسؤولية إنشائه، وقد تكون عمليّة تصريف الكائنات المعقّدة مهمّة معقدة في حدّ ذاتها، لذا يمكن تفويض هذه المهمة إلى صنف آخر. سأنشئ فيما يلي بانيًا بريديًا Email Builder بلغة C++‎، وهو مستوحى من فكرة مشابهة في في C#‎، كائن البريد الإلكتروني ليس بالضرورة كائنًا معقدًا، ولكنّه مثال جيد لتوضيح كيفية عمل هذا النمط. #include <iostream> #include <sstream> #include <string> using namespace std; // التصريح اللاحق للباني class EmailBuilder; class Email { public: friend class EmailBuilder; // Email يمكن للباني الوصول إلى الأعضاء الخاصة في static EmailBuilder make(); string to_string() const { stringstream stream; stream << "from: " << m_from << "\nto: " << m_to << "\nsubject: " << m_subject << "\nbody: " << m_body; return stream.str(); } private: Email() = default; // قصر الإنشاء على الباني string m_from; string m_to; string m_subject; string m_body; }; class EmailBuilder { public: EmailBuilder& from(const string &from) { m_email.m_from = from; return *this; } EmailBuilder& to(const string &to) { m_email.m_to = to; return *this; } EmailBuilder& subject(const string &subject) { m_email.m_subject = subject; return *this; } EmailBuilder& body(const string &body) { m_email.m_body = body; return *this; } operator Email&& () { return std::move(m_email); // لاحظ عمليّة النقل } private: Email m_email; }; EmailBuilder Email::make() { return EmailBuilder(); } // مثال إضافي std::ostream& operator << (std::ostream& stream, const Email& email) { stream << email.to_string(); return stream; } int main() { Email mail = Email::make().from("me@mail.com") .to("you@mail.com") .subject("C++ builders") .body("I like this API, don't you?"); cout << mail << endl; } بالنسبة للإصدارات الأقدم من C++‎، يمكن تجاهل عملية ‎std::move‎ وإزالة && من عامل التحويل لكنّ هذا سيؤدّي إلى إنشاء نسخة مؤقتة. ينهي المنشئ عمله عندما يُرسَل البريد الإلكتروني بواسطة ‎operator Email&&()‎. يكون المنشئ في هذا المثال كائنًا مؤقتًا، ويعيد البريدَ الإلكتروني قبل تدميره. يمكنك أيضًا استخدام عملية صريحة مثل ‎Email EmailBuilder::build() {...}‎ بدلًا من عامل التحويل. تمرير الباني من الميزات الرائعة التي يوفّرها "نمط الباني" هي القدرةُ على استخدام عدّة عوامل (actors) لإنشاء كائن معيّن، ويمكن ذلك عن طريق تمرير المنشئ إلى العوامل الأخرى، والتي سيعطي كل منها بعض المعلومات الإضافية للكائن المبنِيّ. هذا مفيد بشكل خاص عندما تريد بناء الاستعلامات query، أو إضافة المُرشِّحات، وغيرها من المواصفات. void add_addresses(EmailBuilder& builder) { builder.from("me@mail.com") .to("you@mail.com"); } void compose_mail(EmailBuilder& builder) { builder.subject("I know the subject") .body("And the body. Someone else knows the addresses."); } int main() { EmailBuilder builder; add_addresses(builder); compose_mail(builder); Email mail = builder; cout << mail << endl; } الكائنات القابلة للتغيير يمكنك تغيير تصميم نمط الباني بما يناسب احتياجاتك، سنوضّح هذا الأمر في هذه الفقرة: كائن البريد الإلكتروني في المثال السابق كان غير قابل للتغيير (immutable)، أي أنّه لا يمكن تعديل خاصّياته لأنّه لا يمكن الوصول إليها، وقد كانت هذه الميزة مطلوبة، لكن ماذا لو كنت بحاجة إلى تعديل الكائن بعد إنشائه، سيكون عليك أن توفّر له بعض الضوابط (setters). ولمّا كانت تلك الضوابط تتكرّر في المنشئ، فقد تفكر في جمعها جميعًا في صنف واحد -لن تكون هناك حاجة للصنف الباني إذن-. لكن قد يكون الأفضل جعل الكائن المبنِيّ قابلاً للتغيير. نمط تصميم المفردة (Singleton Design Pattern) التهيئة المُرجأة (Lazy Initialization) عثرت على هذا المثال في قسم ‎Q & A‎ في هذا الرابط. انظر أيضًا هذه المقالة للحصول على تصميم بسيط لتقييم مُرجأ مع مفردة مضمونة التدمير. انظر المثال التالي عن مفردة تقليدية ذات تقييم مُرجأ ومُدمَّرة بشكل صحيح. class S { public: static S& getInstance() { static S instance; // تدميرها مضمون // تُستنسخ عند أوّل استخدام return instance; } private: S() {}; // القوسان المعقوصان ضروريان هنا // C++ 03 // ======== // لا تنس التصريح عن هذين الاثنين، احرص على أن يكونا غير مقبولين // وإلّا فقد تُنسخ المفردة S(S const&); // لا تنفذها void operator=(S const&); // لا تنفذها // C++ 11 // ======= // بإمكاننا أيضا استخدام طريقة حذف التوابع، لكنّنا لن نفعل public: S(S const& ) = delete; void operator = (S const& ) = delete; }; ملاحظة: ذكر سكوت مايرز (Scott Meyers) في كتابه Effective Modern C++ أن التوابع المحذوفة يجب أن تكون عامة، فذلك يسهل اكتشاف الأخطاء لأن رسائل الخطأ تكون أفضل حينها، فالمصرِّفات تتحقق من قابلية الوصول (accessibility) قبل الحالة المحذوفة. يمكنك معرفة المزيد عن المتفرّدات من الروابط التالية: توضّح هذه الصفحة متى يجب استخدام نمط المفردة: نمط المفردة Singleton في موسوعة حسوب راجع هاتين المقالتين الأجنبيتين لمزيد من المعلومات حول ترتيب التهيئة وكيفية التعامل معها: Static variables initialisation order Finding C++ static initialization order problems تصف هذه المقالة الأجنبية دورة الحياة لمتغير ساكن في دالة ++C: What is the lifetime of a static variable in a C++ function?‎ تناقش المقالة الأجنبية التالية بعض تأثيرات الخيوط على المفردات: Singleton instance declared as static variable of GetInstance method توضّح هذه المقالة الأجنبية لماذا لن يعمل قفل التحقق المزدوج (double checked locking) في C++‎: ‏What are all the common undefined behaviours that a C++ programmer should know about?‎ المفردات الساكنة الآمنة من إلغاء التهيئة (Static deinitialization-safe singleton) قد تعتمد بعض الكائنات الساكنة (static objects) في بعض الحالات على المفردة، وقد ترغب في ضمان منع تدميرها إلا عند عدم الحاجة إليها. لأجل ذلك يمكن استخدام ‎std::shared_ptr‎ لمنع تدمير المُتفرّدات وإبقائها متاحة لجميع من يستخدمها حتى عندما تُستدعى المدمّرات الساكنة في نهاية البرنامج: class Singleton { public: Singleton(Singleton const&) = delete; Singleton& operator=(Singleton const&) = delete; static std::shared_ptr < Singleton > instance() { static std::shared_ptr < Singleton > s { new Singleton }; return s; } private: Singleton() {} }; ملاحظة: يظهر هذا المثال كإجابة في قسم الأسئلة والأجوبة في موقع SO. المفردات الآمنة خيطيًا (Thread-safe Singeton) الإصدار ≥ C++‎ 11 يضمن معيار C++‎ 11 أنّ كائنات نطاق الدوالّ (function scope objects) تُهيَّأ بطريقة متزامنة، ويمكن استخدام هذا لتقديم مفردة آمنة خيطيًا مع تهيئة مُرجأة. class Foo { public: static Foo& instance() { static Foo inst; return inst; } private: Foo() {} Foo(const Foo&) = delete; Foo& operator =(const Foo&) = delete; }; الأصناف الفرعية (Subclasses) انظر المثال التالي: class API { public: static API& instance(); virtual~API() {} virtual const char* func1() = 0; virtual void func2() = 0; protected: API() {} API(const API& ) = delete; API& operator = (const API& ) = delete; }; class WindowsAPI: public API { public: virtual const char* func1() override { /* شيفرة ويندوز */ } virtual void func2() override { /* شيفرة ويندوز */ } }; class LinuxAPI: public API { public: virtual const char* func1() override { /* شيفرة لينكس */ } virtual void func2() override { /* شيفرة لينكس */ } }; API& API::instance() { #if PLATFORM == WIN32 static WindowsAPI instance; #elif PLATFORM = LINUX static LinuxAPI instance; #endif return instance; } المُصرّف في هذا المثال يربط الصنف ‎API‎ بالصنف الفرعي المناسب، من أجل الوصول إلى ‎API‎ حتّى لو لم يكن مربوطًا بشيفرة مخصوصة بمنصّة معينة. تقنيات إعادة التصميم (Refactoring Techniques) يشير مفهوم إعادة البناء (Refactoring) إلى تعديل الشيفرة واستبدال نسخة مُحسّنة بها، ورغم أنّ إعادة البناء تُجرى غالبًا أثناء تغيير الشيفرة بُغية إضافة بعض الميزات أو تصحيح الأخطاء، إلّا أنّ هذا المصطلح مخصوص أساسًا بعمليات تحسين الشيفرة بدون إضافة ميزات أو تصحيح الأخطاء. Goto Cleanup يُستخدم أحيانًا نمط التصميم ‎goto cleanup‎ في شيفرات C++‎ التي بُنِيت على شيفرات مكتوبة بلغة C، ونظرًا لأنّ الأمر ‎goto‎ يصعِّب فهم سير عمل الدوال، فغالبًا ما يُوصى بتجنّبه. ويمكن استبدال تعليمة return أو الحلقات أو الدوال بالأمر ‎goto‎. بالمقابل، يتيح استخدام ‎goto cleanup‎ التخلُّص من منطق التنظيف (cleanup logic). short calculate(VectorStr **data) { short result = FALSE; VectorStr *vec = NULL; if (!data) goto cleanup; //< return false يمكن أن يُستعاض عنها بـ result = TRUE; cleanup: delete[] vec; return result; } في C++‎، يمكنك استخدام تقنية RAII لحلّ هذه المشكلة: struct VectorRAII final { VectorStr *data { nullptr }; VectorRAII() = default; ~VectorRAII() { delete[] data; } VectorRAII(const VectorRAII & ) = delete; }; short calculate(VectorStr **data) { VectorRAII vec {}; if (!data) return FALSE; //< return false يمكن الاستعاضة عنها بـ return TRUE; } بعد هذا، يمكنك الاستمرار في إعادة بناء الشيفرة. مثلًا، عن طريق استبدال ‎VectorRAII‎ بمؤشّر فريد std::unique_ptr أو متّجه std::vector. هذا الدرس جزء من سلسلة دروس عن C++‎. ترجمة -بتصرّف- للفصل Chapter 112: Design pattern implementation in C++‎ والفصل Chapter 113: Singleton Design Pattern من كتاب C++ Notes for Professionals