محمد بغات
الأعضاء-
المساهمات
177 -
تاريخ الانضمام
-
تاريخ آخر زيارة
-
عدد الأيام التي تصدر بها
6
نوع المحتوى
ريادة الأعمال
البرمجة
التصميم
DevOps
التسويق والمبيعات
العمل الحر
البرامج والتطبيقات
آخر التحديثات
قصص نجاح
أسئلة وأجوبة
كتب
دورات
كل منشورات العضو محمد بغات
-
معالج C الأولي هو محلّل/مبدّل نصوص يُشغَّل قبل التصريف الفعلي للشيفرة، ويُستخدم لتوسيع وتيسير استخدام لغة C (وكذلك C++ لاحقًا)، ويمكن استخدامه من أجل: تضمين ملفّات أخرى باستخدام #include. تعريف شيفرة جامعة (macro)، لاستبدال النص باستخدام #define التصريف الشرطي باستخدام #if #ifdef. توجيه شيفرة معيّنة لمنصّة أو مٌصرّف معيّن (امتداد للتصريف الشرطي) قيود التضمين قد تُضمّن ترويسة من قبل ترويسة أخرى. لذلك، فالملفّات المصدرية التي تتضمّن عدّة ترويسات قد تتضمّن بعض الترويسات أكثر من مرّة بشكل غير مباشر. وإن كانت إحدى الترويسات المُضمّنة أكثر من مرّة تحتوي على تعريفات، فإنّّ المٌصرّف -بعد المعالجة الأوّلية- سيرصد انتهاكًا لقاعدة التعريف الواحد (الفقرة 3.2 من المعيار C++ لعام 2003) ومن ثم يحدث خطأ في التصريف. يمكن منع التضمين المتعدّد باستخدام دروع التضمين "include guards"، وتُعرَف أيضًا بدروع الترويسة، أو دروع الشيفرة الجامعة (macro guards). وتُنفَّذ هذه الدروع باستخدام مُوجِّهات المُعالج الأوّلي #define و #ifndef و #endif. انظر: // Foo.h #ifndef FOO_H_INCLUDED #define FOO_H_INCLUDED class Foo // تعريف صنف { }; #endif الميزة الرئيسية لاستخدام دروع التضمين أنّها تعمل مع جميع المُصرّفات المتوافقة مع المعايير والمُعالجات الأوّلية. من ناحية أخرى، قد تخلق دروع التضمين بعض المشاكل للمطورين، لوجوب التأكد أنّ وحدات الشيفرة الجامعة فريدة في جميع الترويسات المُستخدمة في المشروع، خاصة إذا استَخدَمت ترويستين (أو أكثر) FOO_H_INCLUDED كدروع تضمين، فإنّّ أولى تلك الترويستين المُضمّنتين في وحدة تصريف ستمنع تضمين الترويسات الأخرى. هذه التحديات تظهر خاصّة إذا كان المشروع يستخدم مكتبات الطرف الثالث، والتي تحتوي على ترويسات تشترك في استخدام دروع تضمين. من الضروري أيضًا التأكد من أنّ وحدات الشيفرة الجامعة المستخدمة في دروع التضمين لا تتعارض مع وحدات الشيفرة الجامعة الأخرى المُعرَّفة في الترويسة. معظم تنفيذات C++ تدعم المُوجِّه #pragma once الذي يحرص على عدم تضمين الملفّ إلّا مرّة واحدة فقط في كل عمليّة تصريف، وهو مُوجِّه قياسي، لكنه ليس جزءًا من أيّ معيار من معايير ISO C++. مثلا: // Foo.h #pragma once class Foo {}; في الشيفرة السابقة: بينما تُجنِّب #pragma once بعض المشاكل المرتبطة بدروع التضمين، فإنّ #pragma -حسب المعيار- هي بطبيعتها خُطَّاف خاص بالمٌصرّف (compiler-specific hook)، وستُتجاهل بصمت من قبل المٌصرّفات التي لا تدعمها. حمل المشاريع (أي porting) التي تستخدم #pragma once إلى المٌصرّفات التي لا تدعمها ليس بالأمر الهيّن. هناك عدد من إرشادات التشفير ومعايير C++ التي توصي بعدم استخدام أيّ مُعالج أوّلي إلا من أجل تضمين ملفات الترويسة #include، أو لأجل وضع دروع التضمين في الترويسات. المنطق الشرطي والتعامل مع تعدد المنصات باختصار، يتمحور منطق المعالجة الأوّلية الشرطية حول التحكم في جعل منطق الشيفرة متاحًا للتصريف أو غير متاح باستخدام تعاريف الشيفرات الجامعة. هذه ثلاث حالات استخدام أساسية: عدّة إصدارات من تطبيق (مثلًا: إصدار للتنقيح، وآخر للإطلاق، وآخر للاختبار، وآخر للتحسين) التصريفات متعدّدة المنصات (cross-platform compiles) - نفس الشيفرة المصدرية، مع تصريفات لعدة منصات استخدام نفس الشيفرة لأجل عدة إصدارات من التطبيق (مثلًا: إصدار Basic و Premium و Pro من البرنامج) - مع ميزات مختلفة قليلاً. مثال أ: مقاربة متعددة المنصات لإزالة الملفّات: #ifdef _WIN32 #include <windows.h > // وبقية ملفات نظام ويندوز #endif #include <cstdio> bool remove_file(const std::string &path) { #ifdef _WIN32 return DeleteFile(path.c_str()); #elif defined(_POSIX_VERSION) || defined(__unix__) return (0 == remove(path.c_str())); #elif defined(__APPLE__) //TODO: دالة مُخصّصة مع نافذة حوار للترخيص NSAPI التحقق مما إذا كان لـ return (0 == remove(path.c_str())); #else #error "This platform is not supported" #endif } مثال ب: إتاحة إمكانية التسجيل الإضافي لأجل التنقيح: void s_PrintAppStateOnUserPrompt() { std::cout << "--------BEGIN-DUMP---------------\n" << AppState::Instance()->Settings().ToString() << "\n" #if ( 1 == TESTING_MODE ) // الخصوصية: لا نريد المعلومات الشخصية للمستخدم إلا عند الاختبار << ListToString(AppState::UndoStack()->GetActionNames()) << AppState::Instance()->CrntDocument().Name() << AppState::Instance()->CrntDocument().SignatureSHA() << "\n" #endif << "--------END-DUMP---------------\n" } مثال ج: توفير ميزة مدفوعة (premium feature) في منتج منفصل (ملاحظة: هذا المثال توضيحي. من الأفضل إتاحة الميزة دون الحاجة إلى إعادة تثبيت التطبيق) void MainWindow::OnProcessButtonClick() { #ifndef _PREMIUM CreatePurchaseDialog("Buy App Premium", "This feature is available for our App Premium users. Click the Buy button to purchase the Premium version at our website"); return; #endif //… الميزات الفعلية هنا } بعض الطرق الشائعة: تعريف رمز في وقت الاستدعاء (invocation): يمكن استدعاء المُعالج الأوّلي برموز مسبقة (مع تهيئة اختيارية). على سبيل المثال (gcc -E يعمل فقط على المُعالج الأوّلي): gcc - E - DOPTIMISE_FOR_OS_X - DTESTING_MODE = 1 Sample.cpp يُعالَج Sample.cpp كما لو أنّ #define OPTIMISE_FOR_OS_X و #define TESTING_MODE 1 مُضافان إلى أعلى Sample.cpp. التأكد أن شيفرة جامعة ما معرَّفة: إذا لم تكن الشيفرة الجامعة مُعرّفة وقورنت قيمتها أو تم التحقق منها، فسيفترض المعالج الأوّلي دائمًا أنّ القيمة ستساوي 0. وهناك عدّة طرق للتعامل مع هذا، إحداها هي افتراض أنّ الإعدادات الافتراضية تُمثَّل بالعدد 0، ويجب إجراء أيّ تغييرات لازمة بشكل صريح (على سبيل المثال ENABLEEXTRADEBUGGING = 0 افتراضيًا، مع الإسناد -DENABLEEXTRADEBUGGING = 1 لإعادة التعريف). هناك طريقة أخرى، وهي جعل جميع التعاريف والقيم الافتراضية صريحة، ويمكن تحقيق ذلك بدمج المُوجِّهين #ifndef و #error: #ifndef (ENABLE_EXTRA_DEBUGGING) // إن لم تكن مضافة سلفا DefaultDefines.h برجاء إضافة # error "ENABLE_EXTRA_DEBUGGING is not defined" #else # if ( 1 == ENABLE_EXTRA_DEBUGGING ) //code # endif #endif وحدات الشيفرة الجامعة التوليدية X-macros هي تقنية اصطلاحية لتكرار توليد بنيات برمجية في وقت التصريف، وتتكون الشيفرات الجامعة التوليدية من جزأين: القائمة، وتنفيذ القائمة. هذا مثال توضيحي: #define LIST\ X(dog)\ X(cat)\ X(racoon) // class Animal { // public: // void say(); // }; #define X(name) Animal name; LIST #undef X int main() {#define X(name) name.say(); LIST #undef X return 0; } هذا المثال سيُوسَّع من قبل المعالج الأوّلي إلى ما يلي: Animal dog; Animal cat; Animal racoon; int main() { dog.say(); cat.say(); racoon.say(); return 0; } عندما تصبح القوائم كبيرة (أكثر من 100 عنصر)، فإنّ هذه التقنية تساعد على تجنّب الإكثار من النسخ واللصق. وإذا أردت تجنّب التعريف غير الضروري لـ X قبل استخدام LIST، فيمكنك تمرير اسم شيفرة جامعة كوسيط أيضًا: #define LIST(MACRO)\ MACRO(dog)\ MACRO(cat)\ MACRO(racoon) الآن، يمكنك تعريف الشيفرة الجامعة الذي يجب استخدامها عند توسيع القائمة، كما يلي: #define FORWARD_DECLARE_ANIMAL(name) Animal name; LIST(FORWARD_DECLARE_ANIMAL) إذا وجب على كل استدعاء لـ MACRO أن يأخذ معاملات إضافية ثابتة بالنسبة للقائمة، فيمكن استخدام وحدات شيفرات جامعة متغيّرة (variadic macros): // Visual studio #define EXPAND(x) x #define LIST(MACRO, ...)\ EXPAND(MACRO(dog, __VA_ARGS__))\ EXPAND(MACRO(cat, __VA_ARGS__))\ EXPAND(MACRO(racoon, __VA_ARGS__)) يُمرَّر الوسيط الأولى عبر LIST، بينما تُوفّر الوسائط الأخرى عبر المستخدم عند استدعاء LIST. مثلا، الشيفرة التالية: #define FORWARD_DECLARE(name, type, prefix) type prefix## name; LIST(FORWARD_DECLARE, Animal, anim_) LIST(FORWARD_DECLARE, Object, obj_) سوف تتوسع على النحو التالي: Animal anim_dog; Animal anim_cat; Animal anim_racoon; Object obj_dog; Object obj_cat; Object obj_racoon; الشيفرات الجامعة Macros تُصنّف وحدات الشيفرة الجامعة إلى مجموعتين رئيسيتين: وحدات الشيفرة الجامعة المشابهة للكائنات (object-like macros)، ووحدات الشيفرة الجامعة المشابهة للدوالّ (function-like macros). تُعامَل وحدات الشيفرة الجامعة كأداة لتعويض مقطع (token) معيّن من الشيفرة أثناء عملية التصريف، هذا يعني أنّه يمكن تجريد المقاطع الكبيرة (أو المكرّرة) من الشيفرة عبر شيفرة جامعة. انظر: // هذه شيفرة جامعة تشبه الكائنات #define PI 3.14159265358979 هذه شيفرة جامعة تشبه الدوال، لاحظ أنه يُمكننا استخدام الشيفرَات الجامعة المُعرّفة مسبقًا في تعريفات شيفرة جامعة أخرى. ولا يعرف المصرِّف أيّ نوع يتعامل معه، لذلك يفضل استخدام الدوال المضمّنة: #define AREA(r) (PI *(r) *(r)) // يمكن استخدامها على النحو التالي double pi_macro = PI; double area_macro = AREA(4.6); تستخدم مكتبة Qt هذه التقنية لإنشاء نظام وصفي للكائنات (meta-object system) عن طريق مطالبة المستخدم بتعريف الشيفرة الجامعة Q_OBJECT في رأس الصنف الذي يوسّع QObject. وتُكتب أسماء وحدات الشيفرة الجامعة في العادة بأحرف كبيرة لتسهيل تمييزها عن الشيفرات العادية. هذا ليس شرطًا لكنّ يستحسنه كثير من المبرمجين. عند مصادفة شيفرة جامعة مشابهة للكائنات فإنها تُوسّع كعملية لصق-نسخ بسيطة، مع استبدال اسم الشيفرَة الجامعة بتعريفها. أمّا الشيفرات الجامعة المشابهة للدوال، فيُوسّع كلّ من اسمها ومعاملاتها. double pi_squared = PI * PI; double pi_squared = 3.14159265358979 * 3.14159265358979; double area = AREA(5); double area = (3.14159265358979 *(5) *(5)) وغالبًا ما تُوضع معاملات الشيفرة الجامعة الشبيهة بالدالّة (function-like macro parameters) بسبب هذا ضمن الأقواس، كما في AREA() أعلاه، وذلك لمنع أيّ أخطاء قد تحدث أثناء توسيع الشيفرة الجامعة، خاصة الأخطاء التي قد تنتج عن مُعاملات الشيفرة الجامعة التي تتألّف من عدة قيم. #define BAD_AREA(r) PI *r *r double bad_area = BAD_AREA(5 + 1.6); // ما يراه المصرف: double bad_area = 3.14159265358979 * 5 + 1.6 * 5 + 1.6; double good_area = AREA(5 + 1.6); // ما يراه المصرف: double good_area = (3.14159265358979 *(5 + 1.6) *(5 + 1.6)); لاحظ أيضًا أنّه يجب توخي الحذر بخصوص المعاملات المُمرّرة إلى وحدات الشيفرة الجامعة بسبب هذا التوسيع البسيط، وذلك لمنع الآثار الجانبية غير المتوقعة. وإذا عُدِّل المُعامل أثناء التقييم فسيُعُدَّل في كل مرّة يُستخدَم في الشيفرة الجامعة الموسَّعة، ونحن لا نريد ذلك عادة. يبقى هذا صحيحًا حتى لو كانت الشيفرة الجامعة تُحيط المعاملات بالأقواس لمنع كسر الشيفرة بسبب التوسيع. int oops = 5; double incremental_damage = AREA(oops++); // ما يراه المصرف: double incremental_damage = (3.14159265358979*(oops++)*(oops++)); بالإضافة إلى ذلك، لا توفّر وحدات الشيفرة الجامعة أيّ حماية من أخطاء الأنواع، ممّا يصعّب فهم الأخطاء الناتجة عن عدم تطابق الأنواع. ولمّا كان المبرمجون ينهون السطر بفاصلة منقوطة في العادة، فغالبًا ما تُصمَّم وحدات الشيفرة الجامعة المراد استخدامها كسطور مستقلة "لابتلاع" الفاصلة المنقوطة، هذا يمنع أيّ أخطاء غير مقصودة ناتجة عن وجود فاصلة منقوطة إضافية. #define IF_BREAKER(Func) Func(); if (some_condition) // Oops. IF_BREAKER(some_func); else std::cout << "I am accidentally an orphan." << std::endl; في هذا المثال، كسرت الفاصلة المنقوطة المزدوجة غير المقصودة كتلة if...else، ومنعت المٌصرّف من مطابقة else بـ if. ولكي نمنع هذا، تُحذَف الفاصلة المنقوطة من تعريف الشيفرة الجامعة، ممّا يتسبب في "ابتلاع" الفاصلة المنقوطة مباشرةً بعد أيّ استخدام لها. #define IF_FIXER(Func) Func() if (some_condition) IF_FIXER(some_func); else std::cout << "Hooray! I work again!" << std::endl; يسمح ترك الفاصلة المنقوطة الزائدة أيضًا باستخدام الشيفرة الجامعة دون إنهاء التعليمة الحالية، وذلك يمكن أن يكون مفيدًا. #define DO_SOMETHING(Func, Param) Func(Param, 2) // ... some_function(DO_SOMETHING(some_func, 3), DO_SOMETHING(some_func, 42)); عادة، ينتهي تعريف الشيفرة الجامعة في نهاية السطر، فإذا احتاجت الشيفرة الجامعة إلى أن تمتدّ على عدّة أسطر، فيمكن استخدام شرطة عكسية \ في نهاية السطر للإشارة إلى ذلك. ويجب أن تكون هذه المشروطة العكسية هي الحرف الأخير من السطر، فذلك يُخطر المُعالج الأوّلي بوجوب ضمّ السطر التالي إلى السطر الحالي، ومعاملتهُما كسطرٍ واحد. يمكن استخدام هذا عدّة مرات على التوالي. #define TEXT "I \ am \ many \ lines." // ... std::cout << TEXT << std::endl; // I am many lines. هذا مفيد بشكل خاص في وحدات الشيفرة الجامعة المعقّدة الشبيهة بالدوالّ، والتي قد تحتاج إلى الامتداد على عدّة أسطر. #define CREATE_OUTPUT_AND_DELETE(Str) \ std::string* tmp = new std::string(Str); \ std::cout << *tmp << std::endl; \ delete tmp; // ... CREATE_OUTPUT_AND_DELETE("There's no real need for this to use 'new'.") أمّا بخصوص وحدات الشيفرة الجامعة المعقدة الشبيهة بالدوالّ، فقد يكون من المفيد منحها نطاقًا خاصًّا بها لمنع التداخلات في الأسماء أو التسبب في تدمير الكائنات في نهاية الشيفرة الجامعة، وذلك على غرار الدوالّ الفعلية. المقاربة الشائعة لهذا هي استخدام الحلقة do while 0، حيث تُوضع الشيفرة الجامعة في كتلة التكرار. لا تُتبع هذه الكتلة عادة بفاصلة منقوطة، ممّا يسمح لها بابتلاع واحدة. #define DO_STUFF(Type, Param, ReturnVar) do {\ Type temp(some_setup_values);\ ReturnVar = temp.process(Param);\ } while (0) int x; DO_STUFF(MyClass, 41153.7, x); int x; do { MyClass temp(some_setup_values); x = temp.process(41153.7); } while (0); هناك أيضًا وحدات شيفرة جامعة متغايرة (variadic)، والتي تأخذ عددًا متغيّرا من الوسائط، ثم توسّعها جميعًا بدلاً من المُعامل الخاص __VA_ARGS__. #define VARIADIC(Param, ...) Param(__VA_ARGS__) VARIADIC(printf, "%d", 8); // ما يراه المصرِّف: printf("%d", 8); لاحظ أنّه يمكن وضع __VA_ARGS__ أثناء التوسيع في أيّ مكان في التعريف، وسيُوسّع بشكل صحيح. #define VARIADIC2(POne, PTwo, PThree, ...) POne(PThree, __VA_ARGS__, PTwo) VARIADIC2(some_func, 3, 8, 6, 9); some_func(8, 6, 9, 3); في حالة المعامِل المتغير الذي ليس له وسائط، فإنّ المٌصرّفات تتعامل مع الفاصلة الزائدة بشكل مختلف، حيث تبتلع بعض المُصرّفات -مثل Visual Studio- الفاصلة بصمت بدون أيّ صيغة خاصّة، فيما ستطلّب منك مُصرّفات أخرى -مثل GCC- وضع ## مباشرة قبل __VA_ARGS__. ويكون من الحكمة بسبب هذا أن تضع تعريف وحدات الشيفرة الجامعة التي تأخذ عددًا متغيّرا من الوسائط في عبارة شرطية إذا كنت حريصًا على قابلية الشيفرة للعمل في أكثر من مكان. في المثال التالي: COMPILER هو شيفرة جامعة تحدد المصرِّف المستخدم. #if COMPILER == "VS" #define VARIADIC3(Name, Param, ...) Name(Param, __VA_ARGS__) #elif COMPILER == "GCC" #define VARIADIC3(Name, Param, ...) Name(Param, ##__VA_ARGS__) #endif /* COMPILER */ الشيفرات الجامعة المُعرّفة مسبقًا (Predefined macros) الشيفرة الجامعة المُعرّفة مسبقًا هي تلك التي يعرّفها المٌصرّف -على النقيض من تلك التي يعرّفها المستخدمون في الملف المصدري-، ويجب ألّا يُعاد تعريف تلك الشيفرات الجامعة أو إلغاء تعريفها من قِبل المستخدم. الشيفرات الجامعة التالية مُعرّفة مسبقًا وفقًا لمعيار C++: __LINE__ - تحتوي على رقم السطر الذي تُسُتخدم فيه هذه الشيفرة الجامعة، ويمكن تغييرها عبر المُوجِّه #line. __FILE__ - تحتوي على اسم الملفّ الذي تُستخدم فيه هذه الشيفرة الجامعة في، ويمكن تغييرها عبر المُوجِّه #line. __DATE__ - تحتوي تاريخ تصريف الملف (بتنسيق "Mmm dd yyyy")، ويتم تنسيق Mmm كما لو كانت مُعادة من قبل std::asctime(). __TIME__ - تحتوي زمن تصريف الملف وفق التنسيق "HH: MM: SS". __cplusplus - تُعرَّف من قبل مصرّف C++ أثناء تصريف ملفات C++، وقيمتها هي الإصدار القياسي الذي يتوافق مع المٌصرّف، أي 199711L لـ C++ 98 و C++ 03، و 201103L لـ C++ 11، و 201402L لمعيار C++ 14. الإصدار ≥ C++ 11 __STDC_HOSTED__ - تُعطى القيمة 1 إذا كان التطبيق مُستضافًا (hosted)، أو 0 إذا كان قائما بذاته (freestanding). الإصدار ≥ C++ 17 __STDCPP_DEFAULT_NEW_ALIGNMENT__ - تحتوي على size_t حرفية، والتي تمثّل المحاذاة المستخدمة لإجراء استدعاء للعامل operator new غير المدرِك للمحاذاة. بالإضافة إلى ذلك، يُسمح للشيفرة الجامعة التالية أن تُعرَّف مسبقًا من قبل التنفيذات، لكنّها غير إلزامية: __STDC__ - يعتمد معناها على التنفيذ، ولا تٌعرَّف عادة إلّا عند تصريف ملف في لغة C للدلالة على الامتثال الكامل مع معيار C القياسي، أو قد لا تُنفّد إذا قرّر المٌصرّف عدم دعم هذه الشيفرة الجامعة. الإصدار ≥ C++ 11 __STDC_VERSION__ - يعتمد معناها على التنفيذ، وعادة ما تساوي قيمتها إصدارَ لغة C، على نحو مشابه لـ __cplusplus في إصدار C++، أو قد لا تُعرَّف، إذا قرر المٌصرّف عدم دعم هذه الشيفرة الجامعة. __STDC_MB_MIGHT_NEQ_WC__ - تُعطى القيمة 1، إذا كان ممكنًا لقيم الترميز الضيق لمجموعة المحارف الأساسية، ألا تساوي قيم نظرائها الواسعة، مثلًا في حال كان (uintmax_t)'x' != (uintmax_t)L'x'. __STDC_ISO_10646__ - تُعرّف في حال كان wchar_t مُرمّزًا بترميز اليونيكود (Unicode)، وتُوسّع إلى عدد صحيح على شكل yyyymmL إشارةً إلى دعم آخر مُراجَعة لليونيكود. __STDCPP_STRICT_POINTER_SAFETY__ - تُعطى القيمة 1 إذا كان للتنفيذ نظام أمان صارم للمؤشّرات (strict pointer safety) __STDCPP_THREADS__ - تُعطى القيمة 1 إذا أمكن احتواء البرنامج على أكثر من خيط (thread) واحد للتنفيذ، ينطبق هذا على التنفيذ المستقل (freestanding implementation) أما التنفيذات المستضافة فيمكن أن يكون لها أكثر من خيط واحد. ربما تجب الإشارة إلى __func__، وهي ليست شيفرة جامعة ولكنّها متغيِّر محلّي لدالّة مُعرّفة مُسبقا (predefined function-local variable). يحتوي هذا المتغير على اسم الدالّة التي تُستخدَم فيها على شكل مصفوفة حروف ساكنة وفق تنسيق مُعرّف من قِبل التنفيذ. ويمكن أن يكون للمٌصرّفات مجموعة خاصّة بها من وحدات الشيفرة الجامعة المسبقة، وذلك إضافة إلى وحدات الشيفرة الجامعة القياسية المُعرّفة مسبقًا، ارجع إن شئت إلى توثيق المٌصرّف للاطلاع عليها. على سبيل المثال: gcc Microsoft Visual C++ clang Intel C++ Compiler توجد بعض تلك الشيفرات الجامعة للاستعلام عن دعم بعض الميزات فقط: #ifdef __cplusplus // C++ في حال صُرِّفت عبر مصرّف extern "C" { //C يجب أن تُزخرَف شيفرة // هنا C ترويسة مكتبة } #endif البعض الآخر مفيدة جدًا للتنقيح: الإصدار ≥ C++ 11 bool success = doSomething( /*some arguments*/ ); if( !success ){ std::cerr << "ERROR: doSomething() failed on line " << __LINE__ - 2 << " in function " << __func__ << "()" << " in file " << __FILE__ << std::endl; } والبعض الآخر للتحكم في الإصدار: int main(int argc, char *argv[]) { if( argc == 2 && std::string( argv[1] ) == "-v" ){ std::cout << "Hello World program\n" << "v 1.1\n" // عليك تحديث هذا يدويا << "compiled: " << __DATE__ << ' ' << __TIME__ // هذا يُحدّث تلقائيا << std::endl; } else { std::cout << "Hello World!\n"; } } عمليات المعالجة الأولية يُستخدم المُعامل # أو مُعامل التنصيص (stringizing operator) لتحويل مُعامل شيفرة جامعة إلى سلسلة نصية، ولا يمكن استخدامه إلّا مع شيفرة جامعة ذات وسائط. انظر المثال التالي حيث يحول المعالج الأولي المعامِلَ x إلى السلسلة النصية المجردة x: #define PRINT(x) printf(#x "\n") PRINT(This line will be converted to string by preprocessor); printf("This line will be converted to string by preprocessor" "\n"); يضمّ المُصرّف سلسلتين نصّيتين إلى بعضهما، وسيكون الوسيط printf() النهائي عبارة عن سلسلة نصية مجردة تنتهي بمحرف السطر الجديد. وسيتجاهل المُعالج الأوّلي المسافات البيضاء الموجودة قبل أو بعد وسيط الشيفرة الجامعة، لذلك ستطبع الشيفرة التالية نفس النتيجة. PRINT(This line will be converted to string by preprocessor); إذا تطلب مُعامل السلسلة النصية المجردة تسلسل تهريب (escape sequence)، كما هو الحال قبل علامة الاقتباس المزدوجة، فسيُدرَج تلقائيًا عبر المعالج الأوّلي. PRINT(This "line" will be converted to "string" by preprocessor); printf("This \"line\" will be converted to \"string\" by preprocessor""\n"); يُستخدم العامل ## أو عامل لصق القِطع (Token pasting operator) لضمّ سلسلتين نصّيتين أو مقطعَين خاصين بشيفرة جامعة. انظر المثال التالي حيث يدمج المعالجُ الأولي المتغيرَ مع x: #define PRINT(x) printf("variable" #x " = %d", variable##x) int variableY = 15; PRINT(Y); printf("variable""Y"" = %d", variableY); وسنحصل على الناتج التالي: variableY = 15 #pragma once تدعم معظم تطبيقات C++ المُوجِّه #pragma once الذي يتحقّق من أنّ الملفّ لن يُضمَّن إلّا مرّة واحدة في كل عملية تصريف، وهو ليس جزءًا من أيّ معيار من معايير ISO C++. على سبيل المثال: // Foo.h #pragma once class Foo {}; بينما تتجنب #pragma once بعض المشاكل المرتبطة بدروع التضمين (include guards) التي ذكرناها آنفًا، فإنّ #pragma -حسب تعريفها في المعايير- هي خطّاف خاصّ بالمٌصرّف (compiler-specific hook)، وسيُتجاهَل بصمت من قبل المٌصرّفات التي لا تدعمه. ويجب تعديل المشاريع التي تستخدم #pragma once لتكون متوافقة مع المعايير، وقد يؤدّي #pragma once إلى تسريع كبير لعملية التصريف في بعض المُصرِّفات -خاصّة تلك التي تستخدم الترويسات المُصرّفة مسبقًا-. وبالمثل، تسرّع بعض المُعالجات الأوّلية عملية التصريف من خلال التحقق من الترويسات المُتضمّنة التي تستخدم الدروع. قد تزيد الفائدة من استخدام كل من #pragma once والدروع معًا أو تنقص بحسب التنفيذ ووقت التصريف. وقد كان يوصى بدمج #pragma once مع دُروع التضمين عند كتابة تطبيقات MFC على ويندوز، وكانت تُنشأ بواسطة الأدوات add class و add dialog و add windows الخاصة بـ Visual Studio، لذا من الشائع أن تجدها في تطبيقات C++ الموجّهة لويندوز. رسائل خطأ المُعالج الأوّلي يمكن توليد أخطاء التصريف باستخدام المعالج الأوّلي، هذا مفيد لعدد من الأسباب: إخطار المستخدم في حال كان على منصّة أو مٌصرّف غير مدعوم. إعادة خطأ في حال كان الإصدار أقل من gcc 3.0.0. #if __GNUC__ < 3 #error "This code requires gcc > 3.0.0" #endif إعادة خطأ إن كان التصريف على حاسوب Apple. #ifdef __APPLE__ #error "Apple products are not supported in this release" #endif هذا الدرس جزء من سلسلة مقالات عن C++. ترجمة -بتصرّف- للفصل Chapter 75: Preprocessor من كتاب C++ Notes for Professionals
-
تُسنَد فئات القيمة إلى تعبيرات C++ بناءً على نتائج تلك التعبيرات، ويمكن لهذه الفئات أن تؤثّر على تحليل التحميل الزائد (overload resolution) للدوالّ في C++، كما تحدّد خاصّيتين مهمَّتين ومنفصلتين حول التعابير، تحدد الأول منهما إذا كان للتعبير هوية (identity)، أي إذا كان يشير إلى كائن له اسمُ متغيّرٍ (variable name)، حتى لو لم يكن اسم المتغيّر مُضمَّنًا في التعبير. والثانية هي إذا كان يجوز النقل ضمنيًا (implicitly move) من قيمة التعبير، أو بشكل أدقّ، إذا كان التعبير سيرتبط بأنواع المعاملات ذات القيمة اليمينية (r-value parameter types) عند استخدامه كمُعامل دالة أم لا. تُعرّف C++ ثلاث فئات تمثّل مجموعة من هذه الخصائص: lvalue - تعبير يساري (تعبيرات ذات هوّية، ولكن لا يمكن النقل منها). xvalue - تعبير مرتحل (تعبيرات ذات هوّية ويمكن النقل منها). prvalue - تعبير يميني خالص (تعبيرات بدون هوية ويمكن النقل منها). لا تحتوي C++ على تعبيرات ليس لها هوية ولا يمكن النقل منها. من ناحية أخرى، تعرّف C++ فئتي قيمَة أخريين، كل منهما تعتمد حصرًا على إحدى هذه الخصائص: glvalue - تعبير يساري مُعمّم النوع (تعبيرات ذات هوية) rvalue - تعبير يميني (تعبيرات يمكن النقل منها). ويمكن استخدام هذه كمجموعات لتصنيف الفئات السابقة. انظر هذا الرسم للتوضيح: القيم اليمينيّة rvalue التعبير اليمينيّ rvalue هو أيّ تعبير يمكن نقله ضمنيًا بغض النظر عما إذا كانت له هوية. وبتعبير أدق، يمكن استخدام التعبيرات اليمينيّة كوسائط للدوال التي تأخذ مُعاملات من النوع T && ( يمثّل T نوع التعبير expr)، والتعبيرات اليمينية هي وحدها التي يمكن تمريرها كوسائط لمعاملات مثل هذه الدوالّ. أما في حال استخدام تعبير غير يميني فإنّّ تحليل التحميل الزائد سيختار أيّ دالّة لا تستخدم مرجع يمينيًا (rvalue reference)، وسيطلق خطأً في حال تعذّر العثور عليها. تشمل فئة التعبيرات اليمينية حصرًا جميع تعبيرات xvalue و prvalue، وتحوّل دالّة المكتبة القياسية std::move تعبيرًا غير يمينيّ بشكل صريح إلى تعبير يميني، إذ تحوّل التعبير إلى تعبير من الفئة xvalue، فحتى لو كان التعبير يمينيا خالصًا ناقص الهوية (identity-less prvalue) من قبل، فإنّه سيكتسب هويةً -اسم معامِل الدالة- عبر تمريره كمُعامل إلى std::move، ويصبح من الفئة xvalue. انظر المثال التالي: std::string str("init"); //1 std::string test1(str); //2 std::string test2(std::move(str)); //3 str = std::string("new value"); //4 std::string &&str_ref = std::move(str); //5 std::string test3(str_ref); //6 يأخذ منشئ السلاسل النصية مُعاملًا واحدًا من النوع std::string&&، ويُطلق على هذا المنشئ عادة "منشئ النقل" (move constructor)، لكن فئة القيمة الخاصة بـ str ليست يمينيّة (بل يساريّة)، لذا لا يمكن استدعاء التحميل الزائد للمنشئ. وبدلاً من ذلك يُستدعي التحميل الزائد لـ const std::string&، أي مُنشئ النسخ. تتغير الأمور في السطر الثالث حيث تكون القيمة المُعادة من std::move هي T&&، إذ يمثّل T النوع الأساسي للمُعامِل المُمرّر، لذا فإنّّ std::move(str) تُعيد std::string&&. واستدعاء الدالة الذي تكون قيمته المعادة مرجعًا يمينيًا (rvalue reference) يُعدُّ تعبيرًا يمينيًا، وتحديدًا من فئة xvalue، لذا قد تستدعي منشئَ النقلِ للسلسلة النصية std::string. بعد ذلك، أي بعد السطر الثالث، يكون النقل من str قد تم، وصارت محتويات str غير محددة. يمرّر السطر 4 عنصرًا مؤقّتًا إلى مُعامل الإسناد الخاص بـ std::string، الذي له تحميل زائد يأخذ std::string&&، ويكون التعبير std::string("new value") تعبيرًا يمينيًا (على وجه التحديد من الفئة prvalue)، لذا قد يستدعي التحميل الزائد. وعليه سيُنقل العنصر المؤقّت إلى str مع استبدال المحتويات غير المُحدّدة بمحتويات محدّدة. ينشئ السطر 5 مرجعًا يمينيًا مُسمّى (named rvalue reference) يحمل الاسم str_ref ويشير إلى str، وهنا تصبح فئات القيمة مُربكة. فرغم أن str_ref ستكون مرجعًا يمينيًّا يشير إلى std::string، إلا أن فئة قيمة التعبير str_ref ليست يمينيّة بل يسارية، ولهذا لا يمكن للمرء استدعاء مُنشئ النقل الخاص بـ std::string مع التعبير str_ref. ولنفس السبب فإن السطر 6 ينسخ قيمة str إلى test3. وإذا أردنا نقله، سيتعيّن علينا توظيف std::move مرّة أخرى. القيمة المرتحلة xvalue التعابير من فئة القيمة المرتحلة xvalue (أو eXpiring value) هي تعابير ذات هوية تمثّل كائنات يمكن النقل منها ضمنيًا. وفكرتها بشكل عام هي أنّ الكائنات التي تمثّلها هذه التعبيرات هي على وشك أن تُدمَّر، وبالتالي فإنّ النقل منها ضمنيًا سيكون مقبولًا. انظر الأمثلة التالية على ذلك: struct X { int n; }; extern X x; 4; // prvalue: ليس لديها هوية x; // lvalue x.n; // lvalue std::move(x); // xvalue std::forward<X&>(x); // lvalue X { 4 }; // prvalue: ليس لها هوية X { 4 }.n; // xvalue: ليس لها هوية وتشير إلى موارد يمكن إعادة استخدامها تعبيرات القيمة اليمينية الخالصة prvalue تعبير القيمة اليمينيّة الخالصة prvalue (أو pure-rvalue) هي تعبيرات تفتقر إلى الهوية، ويُستخدم تقييمها عادة لتهيئة كائن يمكن نقله ضمنيًا. هذه بعض الأمثلة على ذلك: التعبيرات التي تمثّل كائنات مؤقّتة، مثل std::string("123"). استدعاء دالة لا تعيد مرجعًا. كائن حرفي (باستثناء السلاسل النصية الحرفية - إذ أنها يسارية)، مثل 1 أو true أو 0.5f أو 'a'. تعبيرات لامدا. لا يمكن تطبيق معامل العنونة (addressof operator) المُضمّن (&) على هذه التعبيرات. تعبيرات القيمة اليسارية lvalue التعبيرات اليسارية lvalue هي تعبيرات ذات هويّة لكن لا يمكن النقل منها ضمنيًا، وتشمل التعبيرات التي تتألّف من اسم متغيّر واسم دالّة والتعبيرات التي تستخدمها مُعاملات التحصيل (dereference) المُضمّنة والتعبيرات التي تشير إلى مراجع يسارية. تكون القيمة اليسارية عادة مجرّد اسم، لكن يمكن أن تأتي بأشكال أخرى أيضًا: struct X { … }; X x; // قيمة يسارية x X* px = &x; // px is an lvalue *px = X {}; // قيمة يمينيّة خالصة X{} هي قيمة يسارية، و *px X* foo_ptr(); // قيمة يمينيّة خالصة foo_ptr() X &foo_ref(); // قيمة يسارية foo_ref() في حين أنّ معظم الكائنات الحرفية (على سبيل المثال 4، 'x') هي تعبيرات يمينية خالصة، إلا أن السلاسل النصية الحرفية يسارية. تعبيرات القيم اليسارية المُعمّمة glvalue تعابير القيم اليسارية المُعمّمة glvalue (أو "generalized lvalue") هي التعابير التي ليس لها هوية بغض النظر عما إذا كان يمكن النقل منها أم لا. وتشمل هذه الفئة تعبيرات القيم اليسارية (التعبيرات التي لها هوية ولكن لا يمكن النقل منها)، وتعبيرات القيمة المرتحلة xvalues (التعبيرات التي لها هوية، ويمكن النقل منها). بالمقابل، فهي لا تشمل القيم اليمينيةّ الخالصة prvalues (تعبيرات بدون هوية). وإذا كان لتعبيرٍ ما اسم، فإنّّه يساريّ معمَّم glvalue: struct X { int n; }; X foo(); X x; x; std::move(x); foo(); X {}; X {}.n; في الشيفرة السابقة: x له اسم، وعليه يكون يساريًا معمَّمًا. (std::move(x له اسم بما أننا ننقل من x، وعليه يكون قيمة يسارية معمَّمة glvalue لكن بما أننا نستطيع النقل منها فتكون قيمة مرتحلة وليست يسارية. ()foo ليس له اسم، فهو يميني خالص، وليس يساريا معمَّمًا. {} X قيمة مؤقتة ليس لها اسم، لذا فالتعبير يميني خالص، وليس يساريا معمَّمًا. X {}.n له اسم، لذا فهو يساري معمم، كما يمكن النقل منه، لذا فهو مرتحل وليس يساريًا معمَّمًا. هذا الدرس جزء من سلسلة دروس عن C++. ترجمة -بتصرّف- للفصل Chapter 74: Value Categories من كتاب C++ Notes for Professionals
-
ما هي تعابير لامدا؟ توفّر دوال لامدا طريقة موجزة لإنشاء كائنات دوالّ بسيطة، وتعبير لامدا هو قيمة يمينية خالصة (prvalue) تُنتجُ كائنَ تغليف (closure)، والذي يتصرف ككائن دالة. نشأ الاسم "تعبير لامدا" (lambda expression) عن علم حسابيات اللامدا (lambda calculus)، وهو شكل رياضي اختُرِع في الثلاثينيات من قِبل Alonzo Church للإجابة عن بعض الأسئلة المتعلقة بالمنطق والحوسبيّات (computability)، وقد شكّلت حسابيات لامدا أساس لغة LISP، وهي لغة برمجة وظيفية. مقارنة بحسابيات لامدا و LISP، فإنّّ تعبيرات لامدا في C++ تشترك معها في أنّها غير مسمّاة، وأنّها تلتقط المتغيّرات الموجودة في السياق المحيط، لكنّها تفتقر إلى القدرة على العمل على الدوال أو إعادتها. وغالبًا ما تُستخدَم دوال لامدا كوسائط للدوالّ التي تأخذ كائنًا قابلاً للاستدعاء، وذلك غالبًا أبسط من إنشاء دالة مُسمّاة، والتي لن تُستخدَم إلا عند تمريرها كوسيط، وتعبيرات لامدا مفضّلة في مثل هذه الحالات، لأنّها تسمح بتعريف الدوّال ضمنيًا. يتألف تعبير لامدا عادة من ثلاثة أجزاء: قائمة الالتقاط []، وقائمة المعاملات الاختيارية ()، والمتن {}، وكلها يمكن أن تكون فارغة: []() {} // تعبير لامدا فارغ لا يعيد أي شيء. قائمة الالتقاط (Capture list) تمثّل [] قائمة الالتقاط. لا يمكن الوصول إلى متغيّرات النطاق المحيط عبر تعبير لامدا افتراضيًا، ويكون المتغير متاحًا بمجرد التقاطه داخل تعبير لامدا سواء كنسخة أو مرجع، وتصبح المتغيرات الملتقطة جزءًا من لامدا، ولا يلزم تمريرها عند استدعاء لامدا، على عكس وسائط الدوال. int a = 0; // تعريف متغير عددي صحيح auto f =[]() { return a * 9; }; // Error: 'a' cannot be accessed auto f =[a]() { return a * 9; }; // بالقيمة 'a' تم التقاط auto f =[& a]() { return a++; }; // بالمرجع 'a' تم التقاط // a ملاحظة: المبرمج ليس مسؤولا عن ضمان ألا تُدمَّر // قبل استدعاء لامدا auto b = f(); // من دالة الالتقاط ولم تُمرّر هنا a استدعاء دالة لامدا، تم أخذ قائمة المعاملات تمثّل () قائمة المعاملات، وهي تماثل قائمة المعاملات في الدوال العادية، ويمكن حذف الأقواس إذا لم يأخذ تعبير لامدا أيّ وسائط، إلا إذا كنت بحاجة إلى جعل تعبير لامدا قابلًا للتغيير mutable. يُعدُّ تعبيرا لامدا التاليان متكافئين: auto call_foo =[x]() { x.foo(); }; auto call_foo2 =[x] { x.foo(); }; الإصدار ≥ C++ 14 تستطيع قائمةُ المعاملات أن تستخدم النوعَ النائب (placeholder type) auto بدلاً من الأنواع الفعلية، وهذا يجعل الوسيط يتصرّف كمُعامل قالب لقالب دالة. تعبيرات لامدا التالية متكافئة: auto sort_cpp11 =[](std::vector<T>::const_reference lhs, std::vector<T>::const_reference rhs) { return lhs < rhs; }; auto sort_cpp14 =[](const auto &lhs, const auto &rhs) { return lhs < rhs; }; متن الدالة القوسان المعقُوصان {} يحتويان على متن تعبير لامدا، ويُماثل متن الدوال العادية. استدعاء دالة لامدا يعيد تعبير لامدا مغلِّفًا أو غلافًا (closure)، ويمكن استدعاؤه باستخدام الصيغة operator() (كما هو الحال مع الدوال العادية): int multiplier = 5; auto timesFive =[multiplier](int a) { return a * multiplier; }; std::out << timesFive(2); // 10 multiplier = 15; std::out << timesFive(2); // 2 *5 == 10 نوع القيمة المعادة افتراضيًا، يُستنبَط نوع القيمة المُعادة لتعبير لامدا تلقائيًا. []() { return true; }; في هذه الحالة، نوع القيمة المُعادة هو bool، يمكنك أيضًا تعريف نوع القيمة المُعادة يدويًا باستخدام الصياغة التالية: []()->bool { return true; }; دوال لامدا القابلة للتغيير (Mutable Lambda) الكائنات المُلتقطة بالقيمة في دوال لامدا تكون افتراضيًا غير قابلة للتغيير (immutable)، وذلك لأنّ المُعامل operator() الخاصّ بالكائن المغلّف المُنشأ ثابت (const) افتراضيًا. في المثال التالي، سيفشل التصريف لأن ++C ستحاول محاكاة حالة تعبير لامدا. auto func = [c = 0](){++c; std::cout << c;}; يمكن السماح بالتعديل باستخدام الكلمة المفتاحية mutable، والتي تجعل المُعامل operator() غير ثابت const: auto func =[c = 0]() mutable { ++c; std::cout << c; }; إذا اسُتخدِم مع نوع القيمة المُعادة، فينبغي وضع mutable قبله. auto func =[c = 0]() mutable->int { ++c; std::cout << c; return c; }; انظر المثال التالي لتوضيح فائدة لامدا: الإصدار < C++ 11 // كائن دالي عام لأجل المقارنة struct islessthan { islessthan(int threshold): _threshold(threshold) {} bool operator()(int value) const { return value < _threshold; } private: int _threshold; }; // التصريح عن متجهة const int arr[] = { 1, 2, 3, 4, 5 }; std::vector<int> vec(arr, arr + 5); // إيجاد عدد أصغر من مُدخَل مُعطى - على افتراض أنه سيكون مُدخلا إلى دالة int threshold = 10; std::vector<int>::iterator it = std::find_if(vec.begin(), vec.end(), islessthan(threshold)); الإصدار ≥ C++ 11 // التصريح عن متجهة std::vector<int> vec { 1, 2, 3, 4, 5 }; // إيجاد عدد أصغر من مُدخَل مُعطى - على افتراض أنه سيكون مُدخلا إلى دالة int threshold = 10; auto it = std::find_if(vec.begin(), vec.end(), [threshold](int value) { return value < threshold; }); يمكننا تلخيص ما سبق بالجدول التالي: table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; } المعامل التفاصيل default-capture الالتقاط الافتراضي يحدّد كيفيّة التقاط جميع المتغيّرات غير المدرجة، قد يكون عبر = (التقاطًا بالقيمة) أو عبر & (التقاطا بالمرجع). وفي حال حذفها، سيتعذّر الوصول إلى المتغيّرات غير المدرجة ضمن متن لامدا. كما يجب أن يسبق default- capture المعاملُ capture-list. capture-list لائحة الالتقاط يحدّد كيف يمكن الوصول إلى المتغيّرات المحلية داخل متن لامدا، تُلتقط المتغيّرات التي ليست لها سابقة (prefix) بالقيمة (by value) بينما تُلتقط المتغيّرات المسبوقة بـ & بالمرجع. يمكن استخدام this في توابع الأصناف لجعل جميع أعضاء الكائن الذي يشير إليه في المتناول عبر المرجع. ولا يمكن الوصول إلى المتغيّرات غير المدرجة في القائمة إلا إذا كانت القائمة مسبوقة بـ default-capture. argument-list قائمة الوسائط يحدّد وسائط دالّة لامدا. mutable (اختياري) عادة ما تكون المتغيّرات الملتقطة بالقيمة ثابتة (const). وستكون غير ثابتة عند تحديد mutable. وسيُحتفظ بالتغييرات على تلك المتغيّرات بين الاستدعاءات. throw-specification رفع الاعتراضات (اختياري) يحدّد سلوك رفع الاعتراضات في دالّة لامدا، مثل: noexcept أو throw (std::exception). attributes السمات (اختياري) سمات دالّة لامدا. على سبيل المثال، إذا كان متن لامدا يلقي دائمًا اعتراضًا، فيمكن استخدام [[noreturn]]. نوع القيمة المعادة (اختياري) يحدّد نوع القيمة المُعادة من دالّة لامدا. وهي ضرورية في الحالات التي لا يكون بمقدور المٌصرّف فيها تخمين نوع القيمة المعادة. lambda-body متن لامدا الكتلة التي تحتوي تنفيذ لامدا. تحديد نوع القيمة المُعادة بالنسبة لدوال لامدا التي تحتوي عبارة return واحدة فقط، أو عدّة عبارات return من نفس النوع، فيمكن للمصرّف أن يستنتج نوع القيمة المُعادة. انظر المثال التالي الذي يعيد قيمة بوليانية لأن "10 < value" هي قيمة بوليانية. auto l =[](int value) { return value > 10; } بالنسبة لدوال لامدا التي لها عدّة عبارات return من أنواع مختلفة، فلا يمكن للمصرّف أن يستنتج نوع القيمة المُعادة: // error: return types must match if lambda has unspecified return type auto l =[](int value) { if (value < 10) { return 1; } else { return 1.5; } }; في هذه الحالة، يجب عليك تحديد نوع القيمة المُعادة بشكل صريح: // 'double' حُدِّد نوع القيمة المعادة بـ auto l =[](int value)->double { if (value < 10) { return 1; } else { return 1.5; } }; تطابق هذه القواعدُ قواعدَ استنتاج نوع auto، ولا تعيد دوال لامدا التي لم يُصرّح بنوع قيمتها المُعادة صراحة مراجعًا أبدًا، لذا إذا كنت تودّ إعادة نوع مرجعي (reference type)، فيجب تحديده بشكل صريح: تعيد copy في المثال التالي قيمة من نوع &X لذا تنسخ المدخلات: auto copy = [](X& x) { return x; }; لا يحدث نسخ لأن ref في المثال التالي ستعيد قيمة من نوع &X: auto ref = [](X& x) -> X& { return x; }; الالتقاط بالقيمة Capture by value إن حدّدتَ اسم المتغيّر في قائمة الالتقاط فإنّّ تعبير لامدا سيلتقطُه بالقيمة، هذا يعني أنّ نوع المغلِّف المُنشأ لتعبير لامدا سيخزّن نسخة من المتغيّر، وهذا يتطّلب أيضًا أن يكون نوع المتغيّر قابلاً للنسخ: int a = 0; [a]() { return a; // بالقيمة 'a' تم التقاط }; الإصدار < C++ 14 auto p = std::unique_ptr<T>(...); [p]() { // Compile error; `unique_ptr` is not copy-constructible return p->createWidget(); }; من C++ 14 وصاعدًا، من الممكن تهيئة المتغيّرات على الفور، وهذا يسمح بالتقاط "أنواع النقل فقط" في لامدا. الإصدار ≥ C++ 14 auto p = std::make_unique<T> (...); [p = std::move(p)]() { return p->createWidget(); }; رغم أنّ دوال لامدا يمكن أن تلتقط المتغيّرات بالقيمة عندما تُمرّر باسمائها، إلّا أنّ تلك المتغيّرات لا يمكن تعديلها في متن لامدا افتراضيًا، وذلك لأنّ نوع المغلّف يضع متن لامدا في تصريح من الشكل operator() const. وتُطبَّق const على محاولات الوصول إلى المتغيّرات العضوية (member variables) في نوع المغلّف، والمتغيّرات المُلتقطة التي هي أعضاء في المغلّف (على خلاف المتوقّع): int a = 0; [a]() { a = 2; // غير جائز decltype(a) a1 = 1; a1 = 2; // جائز }; لإزالة const، يجب عليك وضع الكلمة المفتاحية mutable في تعبير لامدا: int a = 0; [a]() mutable { a = 2; // 'a' يمكن الآن تعديل return a; }; نظرًا لأنّ a التُقِطت بالقيمة، فإنّّ أيّ تعديلات تُجرى عن طريق استدعاء تعبير لامدا لن تؤثر على a، ولقد نُسخِت قيمة a في تعبير لامدا عند إنشائه، لذلك فإنّ نسخة a في تعبير لامدا مختلفة عن المتغيّر a الخارجي. int a = 5 ; auto plus5Val = [a] (void) { return a + 5 ; } ; auto plus5Ref = [&a] (void) {return a + 5 ; } ; a = 7 ; std::cout << a << ", value " << plus5Val() << ", reference " << plus5Ref() ; // "7, value 10, reference 12" تعبيرات لامدا الذاتية الاستداعاء Recursive lambdas لنفترض أنّنا نرغب في كتابة خوارزمية اقليدس للقاسم المشترك الأكبر gcd() على شكل تعبير لامدا. أولا، سنكتبها على شكل دالة: int gcd(int a, int b) { return b == 0 ? a : gcd(b, a%b); } تعبيرات لامدا لا يمكن أن تكون ذاتية الاستدعاء، لأنها غير قادرة على استدعاء نفسها، فلامدا ليس لها اسم، واستخدام this داخل متن لامدا إنّما يشير إلى this المُلتقَط (بافتراض أنّ تعبير لامدا مُنشأ في متن دالة تابعة، وإلا فسيُطلق خطأ)، إذن كيف نحلّ هذه المشكلة؟ استخدام std::function يمكننا جعل تعبير لامدا يحصل على مرجع إلى كائن std::function لم يُنشأ بعد: std::function<int(int, int)> gcd = [&](int a, int b){ return b == 0 ? a : gcd(b, a%b); }; هذا سيعمل بدون مشاكل، ولكن ينبغي ألّا تستخدم هذه الطريقة إلّا لُمامًا، إذ أنّها بطيئة (لأنّنا نستخدم محو الأنواع - type erasure - بدلاً من استدعاء الدالّة المباشرة)، كما أنّها هشّة (نسخ gcd أو إعادة gcd سيكسر الشيفرة لأنّ تعبير لامدا يشير إلى الكائن الأصلي)، ولن تعمل مع دوال لامدا العامة. استخدام مؤشرين ذكيين auto gcd_self = std::make_shared<std::unique_ptr< std::function<int(int, int)> >>(); *gcd_self = std::make_unique<std::function<int(int, int)>>( [gcd_self](int a, int b){ return b == 0 ? a : (**gcd_self)(b, a%b); }; }; هذا يضيف الكثير من الأعمال غير المباشرة (ما يؤدّي إلى زيادة الحِمل)، ولكن يمكن نسخه/إعادته، كما أنّ جميع النسخ تشترك في الحالة، وهذا يتيح لك إعادة تعبير لامدا، وهذا الحل أفضل من الحل المذكور أعلاه. استخدام Y-combinator باستخدام بِنية صغيرة مُساعِدة، يمكننا حل جميع هذه المشاكل: template < class F> struct y_combinator { F f; // لامدا ستُخزَّن هنا // مسبق operator() عامل template < class...Args > decltype(auto) operator()(Args && ...args) const { ستمرر نفسها هنا إلى f ثم تمرر الوسائط، ويجب أن تأخذ لامدا الوسيط الأول كـ auto&& recurse أو نحوه، نتابع المثال: return f(*this, std::forward<Args> (args)...); } }; // :دالة مساعِدة تستنبط نوع لامدا template < class F> y_combinator<std::decay_t < F>> make_y_combinator(F && f) { return { std::forward<F> (f) }; } // ( `make_` هناك حلول أفضل من C++17 تذكر أنّ في ) يمكننا تنفيذ gcd على النحو التالي: auto gcd = make_y_combinator( [](auto&& gcd, int a, int b){ return b == 0 ? a : gcd(b, a%b); } ); y_combinator هو أحد مفاهيم حسابيّات لامدا، ويتيح لك العمل بالذاتية دون الحاجة إلى تسمية الدالة، وهذا هو بالضبط ما ينقصنا في دوال لامدا. تستطيع إنشاء تعبير لامدا يأخذ "recurse" كوسيط أوّل، وعندما تريد استخدام الذاتية، يمكنك تمرير الوسائط إلى recurse. ويعيد y_combinator بعد ذلك كائن دالة يستدعي تلك الدالّة مع وسائطها، ولكن بكائن "recurse" المناسب (أي y_combinator نفسه) كوسيط أوّل، ويوجِّه بقية الوسائط التي مرّرتها إلى y_combinator إلى تعبير لامدا على النحو التالي: auto foo = make_y_combinator( [&](auto&& recurse, some arguments) { { // اكتب شيفرة تعالج بعض المعاملات }); استدع recurse مع بعض الوسائط الأخرى عند الحاجة لتنفيذ الذاتية (Recursion)، وكذلك نحصل على الذاتية في لامدا دون أيّ قيود أو حِمل كبير. الالتقاط الافتراضي Default capture افتراضيًا، لا يمكن الوصول إلى المتغيّرات المحلية التي لم تُحدَّد بشكل صريح في قائمة الالتقاط من داخل متن تعبير لامدا، بيْد أنّه من الممكن ضمنيًا التقاط المتغيّرات المُسمّاة من قبل متن لامدا: int a = 1; int b = 2; // التقاط افتراضي بالقيمة [=]() { return a + b; }; // بالقيمة a و b تم التقاط // الالتقاط الافتراضي بالمرجع [&]() { return a + b; }; // بالمرجع a و b ما يزال بالإمكان القيام بالتقاط صريحٍ بجانب الالتقاط الافتراضي الضِّمني، سيعيد الالتقاط الصريح تعريف الالتقاط الافتراضي: int a = 0; int b = 1; [=, &b]() { a = 2; b = 2; }; في الشيفرة السابقة: لا تجوز a = 2 لأن a ملتقطَة بالقيمة، وتعبير لامدا لا يقبل التغيير، أي ليس mutable، بينما تجوز b = 2 لأن b ملتقَطة بالمرجع. دوال لامدا في الأصناف والتقاط this تُعدُّ تعبيرات لامدا الذي تم تقييمها في تابع ما صديقة للصّنف الذي ينتمي إليه ذلك التابع ضمنيًا: class Foo { private: int i; public: Foo(int val): i(val) {} // تعريف دالة تابعة void Test() { auto lamb = [](Foo &foo, int val) { // (private) تعديل متغير عضو خاص foo.i = val; }; يُسمح لـ lamb أن تصل إلى عضو خاص لأنها صديقة لـ Foo، نتابع المثال: lamb(*this, 30); } }; مثل هذه التعبيرات ليست صديقة لهذا الصنف فحسب، بل لها نفس إمكانيات الوصول التي يتمتّع بها الصنف الذي صُرِّح عنها فيه. كذلك يمكن لدَوال لامدا التقاط المؤشّر this، والذي يمثّل نُسخة الكائن الذي استُدعِيت الدالّة الخارجية عليه، عن طريق إضافة this إلى قائمة الالتقاط، انظر المثال التالي: class Foo { private: int i; public: Foo(int val): i(val) {} void Test() { // بالقيمة this التقاط المؤشر auto lamb =[this](int val) { i = val; }; lamb(30); } }; عند التقاط this، يمكن لتعبير لامدا استخدام أسماء أعضاء الصنف الذي يحتويه كما لو كان في صنفه الحاوي، لذلك تُطبّق this-> ضمنيًا على أولئك الأعضاء. يجب الانتباه إلى أنّ this تُلتقط بالقيمة، ولكن ليس بقيمة النوع، بل تُلتقَط بقيمة this، وهو مؤشّر، لهذا فإنّ تعبير لامدا لا يملك this، وإذا تجاوز عُمرُ تعبير لامدا عمرَ الكائن الذي أنشأه، فقد يصبح غير صالح. هذا يعني أيضًا أنّ لامدا يمكنها تعديل this حتى لو لم تكن قابلة للتغيير (mutable)، ذلك أنّ المؤشّر هو الذي يكون ثابتًا const وليس الكائن الذي يؤشّر إليه، إلا إن كان التابع الخارجي دالة ثابتة const. أيضًا، تذكّر أنّ كتل الالتقاط الافتراضية [=] و [&] ستلتقط this ضمنيًا، وكلاهما سيلتقطَانه بقيمة المؤشّر، وفي الحقيقة، فمن الخطأ تحديد this في قائمة الالتقاط عند إعطاء قيمة افتراضية. الإصدار ≥ C++ 17 وتستطيع دوال لامدا التقاط نسخة من كائن this المُنشأ في وقت إنشاء تعبير لامدا، وذلك عن طريق إضافة *this إلى قائمة الالتقاط: class Foo { private: int i; public: Foo(int val): i(val) {} void Test() { // التقاط نسخة من الكائن المُعطى من قبل المؤشر auto lamb =[*this](int val) mutable { i = val; }; lamb(30); // this->i لا تغيّر } }; الالتقاط بالمرجع Capture by reference إذا وضعت السابقة & قبل اسم متغيّر محلي، فسيُلتقط المتغيّر بالمرجع، هذا يعني نظريًا أنّ نوع مُغلِّفِ لامدا سيكون له متغيّر مرجعي يُهيَّأُ كمرجع، وسيشير ذلك المرجع إلى المتغيّر المقابل الموجود خارج نطاق تعبير لامدا، وأيّ استخدام للمتغيّر في متن لامدا سوف يشير إلى المتغيّر الأصلي: // 'a' التصريح عن المتغير int a = 0; // بالمرجع 'a' التصريح عن لامدا تلتقط auto set =[& a]() { a = 1; }; set(); assert(a == 1); الكلمة المفتاحية mutable ليست مطلوبة لأنّ a نفسها ليست ثابتة. والالتقاط بالمرجع يعني أنّ لامدا يجب ألا تخرج عن نطاق المتغيّرات التي تلتقطها، لذلك يمكنك استدعاء الدوال التي تأخذ دالّة، ولكن لا تستدع دالّة تخزّن لامدا خارج نطاق مراجعك، وكذلك لا تُعِد تعبير لامدا. تعابير لامدا العامة النوع Generic lambdas الإصدار ≥ C++ 14 يمكن أن تأخذ دوال لامدا وسائط من أيّ نوع، هذا يسمح لهذه الدوال أن تكون أكثر عمومية: auto twice =[](auto x) { return x + x; }; int i = twice(2); // i == 4 std::string s = twice("hello"); // s == "hellohello" طُبِّق هذا في C++ عبر جعل operator() تزيد تحميل دالّة قالب (template function)، انظر النوع التالي الذي له سلوك مكافئ لسلوك مُغلِّف لامدا أعلاه: struct _unique_lambda_type { template < typename T> auto operator()(T x) const { return x + x; } }; ليس بالضرورة أن تكون كل المعاملات عامة في تعبير لامدا غير المحدد أو العام (Generic lambda): [](auto x, int y) { return x + y; } هنا، تُستنبَط x بناءً على وسيط الدالّة الأوّل، بينما سيكون y دائمًا عددًا صحيحًا (int)، وقد تأخذ دوال لامدا العامَّة الوسائط بالمرجع أيضًا، وذلك باستخدام القواعد المعتادة لـ auto و &، أما إن أُخذ وسيط عام كـ auto&&، فسيكون مرجعًا أماميًا (forwarding reference) يشير إلى الوسيط المُمرّر، وليس مرجعًا يمينِيّا (rvalue reference): auto lamb1 = [](int &&x) {return x + 5;}; auto lamb2 = [](auto &&x) {return x + 5;}; int x = 10; lamb1(x); lamb2(x); في الشيفرة السابقة، لا تجوز (lamb1(x لوجوب استخدام std::move(x) لأجل معامِلات &&int، بينما تجوز (lamb1(x لأن نوع x يُستنتج على أنه &int. كذلك يمكن أن تكون دوالّ لامدا متغايرة (variadic)، وأن تعيد توجيه وسائطها: auto lam = [](auto&&... args){return f(std::forward<decltype(args)>(args)...);}; أو: auto lam = [](auto&&... args){return f(decltype(args)(args)...);}; والتي لن تعمل "بالشكل الصحيح" إلّا مع المتغيّرات من نوع auto&&. أيضًا، أحد الأسباب القوية لاستخدام دوال لامدا العامّة هو البنية اللغوية للزيارة (visiting syntax)، انظر: boost::variant<int, double> value; apply_visitor(value, [&](auto&& e){ std::cout << e; }); هنا، قمنا هنا بالزيارة بأسلوب متعدد الأشكال، لكن في السياقات الأخرى فلا تهم أسماء النوع المُمرّرة: mutex_wrapped<std::ostream&> os = std::cout; os.write([&](auto&& os){ os << "hello world\n"; }); لا فائدة من تكرار النوع std::ostream& هنا؛ كأنك تذكر نوع المتغيّر في كل مرّة تستخدمه، ولقد أنشأنا هنا زائرًا غير متعدد الأشكال، كما استخدمنا auto هنا لنفس سبب استخدام auto في حلقة for(:). استخدام دوال لامدا لفك حزم المعاملات المضمنة الإصدار ≥ C++ 14 كان فكّ حزم المعامِلات (Parameter pack unpacking) يتطّلب كتابة دالّة مساعدة في كل مرّة تريد القيام بذلك. مثلًا: template<std::size_t...Is > void print_indexes(std::index_sequence < Is... >) { using discard = int[]; (void) discard { 0, ((void)( std::cout << Is << '\n' // هي ثابتة في وقت التصريف Is هنا ), 0)... }; } template<std::size_t I> void print_indexes_upto() { return print_indexes(std::make_index_sequence < I> {}); } يريد print_indexes_upto إنشاء وفكّ حزمة معاملات من الفهارس، ولفعل ذلك، يجب استدعاء دالّة مساعدة. يجب أن تنشئ دالة مساعدة مخصصة في كل مرة تريد فك حزمة معامِلات أنشأتها، ويمكن تجنب ذلك هنا باستخدام دوال لامدا، إذ يمكنك فكّ الحِزَم باستخدام دالة لامدا على النحو التالي: template<std::size_t I> using index_t = std::integral_constant<std::size_t, I> ; template<std::size_t I> constexpr index_t<I> index {}; template < class = void, std::size_t...Is > auto index_over(std::index_sequence < Is... >) { return[](auto && f) { using discard = int[]; (void) discard { 0, (void( f(index < Is>) ), 0)... }; }; } template<std::size_t N> auto index_over(index_t<N> = {}) { return index_over(std::make_index_sequence < N> {}); } الإصدار ≥ C++ 17 يمكن تبسيط index_over() باستخدام التعبيرات المطوية على النحو التالي: template<class=void, std::size_t...Is> auto index_over( std::index_sequence<Is...> ) { return [](auto&& f){ ((void)(f(index<Is>)), ...); }; } بعد ذلك، يمكنك استخدام هذا لتجنّب الحاجة إلى فكّ حزم المعاملات يدويًا عبر تحميل زائد ثانٍ في الشيفرات الأخرى، فذلك سيتيح لك فكّ حزم المعاملات بشكل مضمّن (inline): template < class Tup, class F> void for_each_tuple_element(Tup&& tup, F&& f) { using T = std::remove_reference_t<Tup> ; using std::tuple_size; auto from_zero_to_N = index_over<tuple_size < T> {} > (); from_zero_to_N( [&](auto i) { using std::get; f(get<i> (std::forward<Tup> (tup))); } ); } نوع auto i المُمرّر إلى دالة لامدا عبر index_over هو std::integral_constant<std::size_t, ???> الذي يحتوي على تحويل constexpr إلى std::size_t لا يعتمد على حالة this، وعليه نستطيع استخدامه كثابت في وقت التصريف، مثلا عندما نمرّره إلى std::get<i> أعلاه. سنعيد الآن كتابة المثال أعلاه: template<std::size_t I> void print_indexes_upto() { index_over(index < I>)([](auto i) { std::cout << i << '\n'; // هي ثابتة في وقت التصريف i هنا }); } صار المثال أقصر بكثير. انظر إن شئت هذا المثال الحيّ على ذلك. الالتقاطات المعممة النوع Generalized capture الإصدار ≥ C++ 14 تستطيع دوال لامدا التقاط التعبيرات وليس المتغيّرات فقط، هذا يسمح لدوال لامدا بتخزين أنواع النقل فقط (move-only types): auto p = std::make_unique<T> (...); auto lamb =[p = std::move(p)]() { p->SomeFunc(); }; هذا ينقل المتغيّر p الخارجي إلى متغيّر لامدا المُلتقط ، ويسمى أيضًا p، وتمتلك lamb الآن الذاكرة المخصّصة لـ make_unique. وبما أن التغليف (closure) يحتوي على نوع غير قابل للنسخ، فذلك يعني أنّ lamb ستكون غير قابلة للنسخ، لكن ستكون قابلة للنقل رغم هذا: auto lamb_copy = lamb; // غير جائز auto lamb_move = std::move(lamb); // جائز الآن أصبحت lamb_move تملك الذاكرة، لاحظ أنّ std::function<> تتطّلب أن تكون القيم المخزّنة قابلة للنسخ، يمكنك كتابة دالة خاصّة بك تتطّلب النقل فقط أو وضع لامدا في غلاف مؤشّر مشترك shared_ptr: auto shared_lambda = [](auto&& f){ return [spf = std::make_shared<std::decay_t<decltype(f)>>(decltype(f)(f))] (auto&&...args)->decltype(auto) { return (*spf)(decltype(args)(args)...); }; }; auto lamb_shared = shared_lambda(std::move(lamb_move)); هنا أخذنا دالة لامدا للنقل فقط ووضعنا حالتها في مؤشّر مشترك، ثم ستُعاد دالة لامدا قابلة للنسخ، ثم التخزين في std::function أو نحو ذلك. يستخدم الالتقاط المُعمّم استنباط النوع auto لاستنباط نوع المتغيّر، وسيصرّح عن هذه الالتقاطات على أنّها قيم افتراضيًا، لكن يمكن أن تكون مراجع أيضًا: int a = 0; auto lamb = [&v = a](int add) // مختلفان `a` و `v` تذكّر أنّ اسمَي { v += add; // `a` تعدي }; lamb(20); // ستصبح 20 `a` يستطيع الالتقاط المعمَّم أن يلتقط تعبيرًا عشوائيًا لكن لا يلزمه التقاط متغيرات خارجية: auto lamb =[p = std::make_unique<T> (...)]() { p->SomeFunc(); } هذا مفيد في إعطاء دوال لامدا قيمًا عشوائية يمكنها الاحتفاظ بها وربّما تعديلها دون الحاجة إلى التصريح عنها خارجيًا، بالطبع، هذا لا يكون مفيدًا إلّا إذا كنت تنوي الوصول إلى تلك المتغيّرات بعد أن تكمل لامدا عملها. التحويل إلى مؤشر دالة إذا كانت قائمة الالتقاط الخاصّة بلَامدا فارغة، فستُحوَّل لامدا ضمنيا إلى مؤشّر دالة يأخذ نفس الوسائط ويعيد نوع القيمة المُعادة نفسه: auto sorter =[](int lhs, int rhs)->bool { return lhs < rhs; }; using func_ptr = bool(*)(int, int); func_ptr sorter_func = sorter; // تحويل ضمني يمكن أيضًا فرض هذا التحويل باستخدام عامل "+" الأحادي: func_ptr sorter_func2 = +sorter; // فرض التحويل الضمني سيكون سلوك استدعاء مؤشّر الدالّة هذا مكافئًا لاستدعاء operator() على لامدا، فلا يعتمد مؤشّر الدالّة على وجود مغلّف لامدا، لذلك قد يستمرّ حتى بعد انتهاء مغلّف لامدا. هذه الميزة مفيدة عند استخدام دوال لامدا مع الواجهات البرمجية التي تتعامل مع مؤشّرات الدوال، بدلاً من كائنات دوال C++. الإصدار ≥ C++ 14 يمكن أيضًا تحويل دالة لامدا عامة لها قائمة التقاط فارغة إلى مؤشّر دالّة، وإذا لزم الأمر سيُستخدَم استنباط وسيط القالب (template argument deduction) لاختيار التخصيص الصحيح. auto sorter =[](auto lhs, auto rhs) { return lhs < rhs; }; using func_ptr = bool(*)(int, int); func_ptr sorter_func = sorter; // int استنتاج // لكن السطر التالي غامض // func_ptr sorter_func2 = +sorter; ترقية دوال لامدا إلى C++ 03 باستخدام الكائنات الدالية (functor) دوال لامدا في C++ هي اختصارات مفيدة توفّر صياغة مختصرة لكتابة الدوال، ويمكن الحصول على وظيفة مشابهة في C++ 03 (وإن كانت مُطولًة) عن طريق تحويل دالّة لامدا إلى كائن دالّي: // هذه بعض الأنواع struct T1 { int dummy; }; struct T2 { int dummy; }; struct R { int dummy; }; هذه الشيفرة تستخدم دالة لامدا،هذا يعني أنها تستلزم C++11، نتابع المثال: R use_lambda(T1 val, T2 ref) { هنا، استعمل auto لأن النوع المُعاد من دالة لامدا مجهول، نتابع: auto lambda =[val, &ref](int arg1, int arg2)->R { /* متن لامدا */ return R(); }; return lambda(12, 27); } // C++03 صنف الكائن الدالي - صالح في class Functor { // قائمة الالتقاط T1 val; T2 &ref; public: // المنشئ inline Functor(T1 val, T2 &ref): val(val), ref(ref) {} // متن الكائن الدالي R operator()(int arg1, int arg2) const { /* متن لامدا */ return R(); } }; هذا يكافئ use_lambda لكنه يستخدم كائنًا دالّيًا، وهو صالح في C++03: R use_functor(T1 val, T2 ref) { Functor functor(val, ref); return functor(12, 27); } int main() { T1 t1; T2 t2; use_functor(t1, t2); use_lambda(t1, t2); return 0; } إذا كانت دالّة لامدا قابلة للتغيير (mutable)، فاجعل مُعامل الاستدعاء الخاص بالكائن الدالّي غير ثابت، أي: R operator()(int arg1, int arg2) /*non-const*/ { /* متن لامدا */ return R(); } هذا الدرس جزء من سلسلة دروس عن C++. ترجمة -بتصرّف- للفصل Chapter 73: Lambdas من كتاب C++ Notes for Professionals
-
إمساك الاستثناءات Catching exceptions تُستخدَم الكتلة try/catch لإمساك الاستثناءات إذ توضع في القسم try الشيفراتُ التي يُشتبه في أنّها قد ترفع استثناءً، فيما تتكفّل الشيفرة الموضوعة في الكتلة catch بمعالجة الاستثناء حال رفعه. #include <iostream> #include <string> #include <stdexcept> int main() { std::string str("foo"); try { str.at(10); // std::out_of_range محاولة الدخول إلى العنصر قد تؤدي إلى رفع } catch (const std::out_of_range &e) { // وتحتوي رسالة توضيحية std::exception موروثة من what() std::cout << e.what(); } } يمكن استخدام عدة كتل catch للتعامل مع أكثر من نوع ٍمن الاستثناءات، وفي حال استخدام عدة عبارات catch فإنّّ آلية معالجة الاستثناءات ستحاول مطابقتها بحسب ترتيب ظهورها في الشيفرة: std::string str("foo"); try { str.reserve(2); // std::length_error محاولة تخصيص سعة زائدة قد تؤدي إلى رفع str.at(10); // std::out_of_range محاولة الدخول إلى هذا العنصر سترفع } catch (const std::length_error &e) { std::cout << e.what(); } catch (const std::out_of_range &e) { std::cout << e.what(); } يمكن إمساك أصناف الاستثناءات المشتقّة من صنف أساسي باستخدام عبارة catch واحدة مخصّصة للصنف الأساسي. ويمكن استبدال العبارتين catch الخاصتين بالاستثنائَين std::length_error و std::out_of_range في المثال أعلاه بعبارة catch واحدة موجّهة للاستثناء std:exception: std::string str("foo"); try { str.reserve(2); // std::length_error محاولة تخصيص سِعة زائدة قد تؤدي إلى رفع str.at(10); // std::out_of_range محاولة الدخول إلى هذا العنصر سترفع } catch (const std::exception &e) { std::cout << e.what(); } ونظرًا لأنّ عبارات catch تُختبَر بالترتيب، فتأكد من كتابة عبارات catch الأكثر تخصيصًا أولاً، وإلا فقد لا تُستدعى شيفرة الاستثناء المخصوصة أبدًا: try { /* شيفرة ترفع استثناء */ } catch (const std::exception &e) { /* std::exception معالجة كل الاستثناءات من النوع */ } catch (const std::runtime_error &e) { } لن تُنفَّذ الكتلة الأخيرة في الشيفرة السابقة لأن std::runtime_error ترث من std::exception، وقد أُمسكت كل استثناءات std::exception من قِبل تعليمة catch التي سبقتها. هناك حلّ آخر، وهو استخدام عبارة catch تعالج كل الاستثناءات، على النحو التالي: try { throw 10; } catch (...) { std::cout << "caught an exception"; } إعادة رفع استثناء قد ترغب أحيانًا أن تفعل شيئًا بالاستثناء الذي أمسكته، مثل كتابته في سجل الأخطاء، أو طباعة تحذير، ثمّ إعادة رفعه إلى النطاق الأعلى لكي يُعالج هناك. وهذا ممكن، إذ يمكنك إعادة رفع أيّ استثناء تمسكه، انظر: try { ... // شيفرة هنا } catch (const SomeException &e) { std::cout << "caught an exception"; throw; } يؤدّي استخدام throw; بدون وسائط إلى إعادة رفع الاستثناء الممسوك حاليًا. الإصدار ≥ C++ 11 لإعادة رفع استثناء std::exception_ptr سبقت إدارته، توفّر مكتبة C++ القياسية دالّة rethrow_exception يمكن استخدامها عبر تضمين ترويسة في البرنامج. #include <iostream> #include <string> #include <exception> #include <stdexcept> void handle_eptr(std::exception_ptr eptr) // لا حرج في التمرير بالقيمة { try { if (eptr) { std::rethrow_exception(eptr); } } catch (const std::exception &e) { std::cout << "Caught exception \"" << e.what() << "\"\n"; } } int main() { std::exception_ptr eptr; try { std::string().at(1); // std::out_of_range ًسيولّد هذا استثناء } catch (...) { eptr = std::current_exception(); // إمساك } handle_eptr(eptr); } // eptr هنا عند تدمير std::out_of_range سيُستدعى مدمّر أفضل تطبيق: رفع الاستثناءات بالقيمة وإمساكها بالمراجع الثابتة بشكل عام، يُوصى برفع الاستثناء بالقيمة (بدلاً من رفعه بالمؤشّر)، ولكن يوصى بإمساكه بالمراجع الثابتة. try { // throw new std::runtime_error("Error!"); // لا تفعل هذا // سينشئ هذا كائن استثناء في الكومة، وسيتطلّب منك أن تمسك المؤشر وتدير الذاكرة بنفسك // وهذا قد يُسبِّب تسرّب الذاكرة throw std::runtime_error("Error!"); } catch (const std::runtime_error &e) { std::cout << e.what() << std::endl; } أحد الأسباب التي تجعل الإمساك بالمرجع أفضل، هو أنّه يلغي الحاجة إلى إعادة بناء الكائن عند نقله إلى كتلة catch (أو عند نشره إلى كتل catch الأخرى)، كما يتيح الإمساك بالمرجع معالجة الاستثناءات بأسلوب تعدد الأشكال، ويتجنّب تشريح slicing الكائنات، لكن إن كنت تريد إعادة رفع استثناء مثل throw e;، انظر المثال أدناه، فلا يزال بإمكانك تشريح الكائن لأنّ تعليمة throw e; تنشئ نسخة من الاستثناء من النّوع المُصرّح عنه: #include <iostream> struct BaseException { virtual const char *what() const { return "BaseException"; } }; struct DerivedException: BaseException { // اختيارية هنا "virtual" الكلمة المفتاحية virtual const char *what() const { return "DerivedException"; } }; int main(int argc, char **argv) { try { try { throw DerivedException(); } catch (const BaseException &e) { std::cout << "First catch block: " << e.what() << std::endl; // ==> First catch block: DerivedException throw e; سيغير ذلك الاستثناء من DerivedException إلى BaseException، تابع: } } catch (const BaseException &e) { std::cout << "Second catch block: " << e.what() << std::endl; // ==> Second catch block: BaseException } return 0; } إذا كنت متأكدًا من أنك لن تفعل أيّ شيء يؤدي إلى تغيير الاستثناء (مثل إضافة معلومات أو تعديل رسالة الخطأ المرفقة بالاستثناء)، فإنّّ إمساك الاستثناء بمرجع ثابت سيسمح للمٌصرّف بإجراء بعض التحسينات، ولكن هذا لن يلغي بالضرورة الحاجة إلى تشريح الكائن (كما هو مُوضّح في المثال أعلاه). الاستثناءات المخصصة ينبغي ألّا ترفع قيمًا خامًا raw values مثل استثناءات، بل استخدم أحد أصناف الاستثناءات القياسية أو اصنع واحدًا خاصًّا بك، ويُعدّ إنشاء صنف استثناء موروث من std::exception أسلوبًا مستحسنًا. في المثال التالي، انظر صنف استثناء مخصّص يرث مباشرةً من std::exception: #include <exception> class Except: virtual public std::exception { protected: int error_number; ///< Error number int error_offset; ///< Error offset std::string error_message; ///< Error message public: /**Constructor (C++ STL string, int, int). * @param msg The error message * @param err_num Error number * @param err_off Error offset */ explicit Except(const std::string &msg, int err_num, int err_off): error_number(err_num), error_offset(err_off), error_message(msg) {} المدمر: يكون وهميًا Virtual من أجل السماح بالتصنيف الفرعي، تابع: virtual~Except() throw () {} نعيد مؤشرًا يشير إلى وصف الخطأ الثابت، ومؤشرًا يشير إلى *const char، وتحتوي الذاكرة الأساسية على الكائن Except، ويجب ألا يحاول المستدعون أن يحرروا الذاكرة. انظر: virtual const char *what() const throw () { return error_message.c_str(); } /**Returns error number. * @return #error_number */ virtual int getErrorNumber() const throw () { return error_number; } /**Returns error offset. *@return #error_offset */ virtual int getErrorOffset() const throw () { return error_offset; } }; هذا مثال تطبيقي: try { throw (Except("Couldn't do what you were expecting", -12, -34)); } catch (const Except &e) { std::cout << e.what() << "\nError number: " << e.getErrorNumber() << "\nError offset: " << e.getErrorOffset(); } بهذه الطريقة فإنّك لا ترفع رسالة خطأ وحسب، ولكن ترفع أيضًا بعض القيم الأخرى التي توضّح طبيعة الخطأ بالضبط، وهكذا يصبح التعامل مع الأخطاء أسهل وأيسر. يوجد صنف استثناء يتيح لك معالجة رسائل الخطأ بسهولة، وهو std::runtime_error، تستطيع الاشتقاق من هذا الصنف على النحو التالي: #include <stdexcept> class Except: virtual public std::runtime_error { protected: int error_number; ///< Error number int error_offset; ///< Error offset public: /**Constructor (C++ STL string, int, int). * @param msg The error message * @param err_num Error number * @param err_off Error offset */ explicit Except(const std::string &msg, int err_num, int err_off): std::runtime_error(msg) { error_number = err_num; error_offset = err_off; } /** المدمر * للسماح بالاشتقاق Virtual الكلمة المفتاحية */ virtual~Except() throw () {} /**Returns error number. * @return #error_number */ virtual int getErrorNumber() const throw () { return error_number; } /**Returns error offset. *@return #error_offset */ virtual int getErrorOffset() const throw () { return error_offset; } }; لاحظ أنّك لم تُعِدْ تعريف الدالّة what() من الصنف الأساسي (std::runtime_error)، أي أنّنا سنستخدم إصدار الصنف الأساسي من what()، لكن لا شيء يمنعك من إعادة تعريفها إن أردت. std::uncaught_exceptions الإصدار ≥ C++ 17 تقدّم C++ 17 النوع int std::uncaught_exceptions() (ليحلّ محلّ النوع bool std::uncaught_exception() المحدود) لمعرّفة عدد الاستثناءات غير الممسوكة حاليًا، يتيح هذا للأصناف معرفة ما إذا كان الاستثناء قد دُمِّر أثناء فكّ المكدّس stack unwinding أم لا. #include <exception> #include <string> #include <iostream> // تطبيق التغيير عند التدمير: // في حال رفع استثناء: Rollback. // غير ذلك: Commit. class Transaction { public: Transaction(const std::string &s): message(s) {} Transaction(const Transaction &) = delete; Transaction &operator=(const Transaction &) = delete; void Commit() { std::cout << message << ": Commit\n"; } void RollBack() noexcept(true) { std::cout << message << ": Rollback\n"; } // ... ~Transaction() { if (uncaughtExceptionCount == std::uncaught_exceptions()) { Commit(); // قد يُطرَح استثناء } else { // فك المكدّس الحالي RollBack(); } } private: std::string message; int uncaughtExceptionCount = std::uncaught_exceptions(); }; class Foo { public: ~Foo() { try { Transaction transaction("In ~Foo"); // حتى لو كان هناك استثناء غير ممسوك commit الاعتماد //... } catch (const std::exception &e) { std::cerr << "exception/~Foo:" << e.what() << std::endl; } } }; int main() { try { Transaction transaction("In main"); // RollBack (تراجع) Foo foo; // يعتمد المعاملة ~Foo //... throw std::runtime_error("Error"); } catch (const std::exception &e) { std::cerr << "exception/main:" << e.what() << std::endl; } } الناتج: In~Foo: Commit In main: Rollback exception / main: Error استخدام كتلة try في الدوال العادية إليك المثال التوضيحي التالي: void function_with_try_block() try { // شيفرة هنا } catch (...) { // شيفرة هنا } والذي يكافئ: void function_with_try_block() { try { // شيفرة هنا } catch (...) { // شيفرة هنا } } لاحظ أنّه بالنسبة للمنشئات والمدمّرات فإنّّ السلوك يكون مختلفًا لأنّ كتلة catch تعيد رفع استثناء على أيّ حال (الاستثناء الممسوك في حال لم يكن هناك رفع الاستثناء آخر في جسم كتلة catch). ويُسمح للدّالّة main أن تحتوي على كتلة try مثل أيّ دالّة أخرى، بيْد أنّ كتلة try في الدالة main لن تمسك الاستثناءات التي تحدث أثناء إنشاء المتغيّرات الساكنة غير المحلية، أو عند تدمير أيّ متغيّر ساكن، لكن بدلاً من ذلك تُستدعى std::terminate. الاستثناء المتداخل الإصدار ≥ C++ 11 أثناء معالجة الاستثناء، يمكن إمساك استثناء عام من دالّة ذات مستوى منخفض (مثل خطأ في نظام الملفّات أو خطأ في نقل البيانات) ورفع استثناء عالي المستوى أكثر تخصيصًا يشير إلى أنّه قد تعذّر تقديم بعض العمليات عالية المستوى (مثل عدم القدرة على نشر صورة على الويب). يسمح ذلك بالاستجابة للمشاكل الخاصّة بالعمليات عالية المستوى، كما يسمح للمبرمج بإيجاد مكان وقوع الاستثناء في التطبيق، الجانب السلبي في هذا الحل هو أنّ مكدّس الاستدعاءات callstack الخاص بالاستثناء سيُقتطَع، وسيُفقد الاستثناء الأصلي، ويفرض هذا على المطوّرين تضمين نصّ الاستثناء الأصلي يدويًا في آخر استثناء مُنشأ. تهدف الاستثناء المتداخلة std::nested_exception إلى حل هذه المشكلة عن طريق إرفاق استثناء منخفض المستوى، والذي يصف سبب رفع الاستثناء، باستثناء عالي المستوى، والذي يصف ما يعنيه الاستثناء في هذه الحالة بالذات. يسمح الاستثناء المتداخل بتداخل الاستثناءات بفضل std::throw_with_nested: #include <stdexcept> #include <exception> #include <string> #include <fstream> #include <iostream> struct MyException { MyException(const std::string &message): message(message) {} std::string message; }; void print_current_exception(int level) { try { throw; } catch (const std::exception &e) { std::cerr << std::string(level, ' ') << "exception: " << e.what() << '\n'; } catch (const MyException &e) { std::cerr << std::string(level, ' ') << "MyException: " << e.message << '\n'; } catch (...) { std::cerr << "Unkown exception\n"; } } void print_current_exception_with_nested(int level = 0) { try { throw; } catch (...) { print_current_exception(level); } try { throw; } catch (const std::nested_exception &nested) { try { nested.rethrow_nested(); } catch (...) { print_current_exception_with_nested(level + 1); // (Recursion) ذاتية } } catch (...) { //فارغ // (Recursion) إنهاء الذاتية } } // دالة بسيطة تمسك استثناء وتغلّفه في استثناء متداخل void open_file(const std::string &s) { try { std::ifstream file(s); file.exceptions(std::ios_base::failbit); } catch (...) { std::throw_with_nested(MyException { "Couldn't open " + s }); } } // دالة بسيطة تمسك استثناء وتغلّفه في اسستثناء متداخل void run() { try { open_file("nonexistent.file"); } catch (...) { std::throw_with_nested(std::runtime_error("run() failed")); } } // تشغيل الدالة أعلاه وطباعة الاستثناء الممسوك int main() { try { run(); } catch (...) { print_current_exception_with_nested(); } } الخرج المحتمل: exception: run() failed MyException: Couldn 't open nonexistent.file exception: basic_ios::clear إذا كنت تعمل حصرًا مع الاستثناءات الموروثة من std::exception، فهناك إمكانية لتبسيط الشيفرة أكثر. استخدام كتلة Try في المنشئات الطريقة التالية هي الوحيدة لإمساك استثناء في قائمة مهيئ: struct A: public B { A() try: B(), foo(1), bar(2) { // جسم المنشئ } catch (...) { تُمسَك الاستثناءات الناجمة عن قائمة المهيئ والمنشئ هنا، وسيعاد رفع الاستثناء الممسوك عند عدم رفع أي استثناء، نتابع المثال: } private: Foo foo; Bar bar; }; استخدام كتلة Try في المدمرات انظر المثال التوضيحي التالي: struct A { ~A() noexcept(false) try { // جسم المدمّر } catch (...) { // الاستثناءات الناجمة عن جسم المدمّر تُمسَك هنا // في حال عدم رفع أيّ استثناء هنا، فسيُعاد رفع الاستثناء الممسوك } }; رغم أنّه يمكن رفع استثناء من المدمّر، إلّا أنّه ينبغي التزام الحذر، ذلك أنّه في حال رفع استثناء في مدمّر مُستدعًى أثناء فكّ المكدّس، فإنّّ ذلك سيتسبّب في استدعاء std::terminate. هذا الدرس جزء من سلسلة دروس عن C++. ترجمة -بتصرّف- للفصل Chapter 72: Exceptions من كتاب C++ Notes for Professionals
-
حجم الأنواع العددية الصحيحة الأنواع التالية هي أنواع عددية صحيحة: char الأنواع العددية الصحيحة المُؤشّرة Signed integer types الأنواع العددية الصحيحة غير المُؤشّرة Unsigned integer types char16_t و char32_t bool wchar_t باستثناء sizeof(char) و sizeof(signed char) و sizeof(unsigned char)، الموجودة بين الفقرة 3.9.1.1 [basic.fundamental/1] والفقرة 5.3.3.1 [expr.sizeof] و sizeof(bool)، والتي تتوقف على التنفيذ بالكامل وليس لها حدّ أدنى، فإنّّ متطلّبات الحد الأدنى لأحجام هذه الأنواع موجود في القسم 3.9.1 [basic.fundamental] من المعيار، وسنوضّحه أدناه. حجم char تحدّد جميع إصدارات معيار C++، في الفقرة 5.3.3.1، أنّ sizeof تعيد القيمة 1 لكلّ من unsigned char و signed char و char (مسألة ما إذا كان النوع char مؤشّرا - signed - أو غير مؤشّر - unsigned - تتعلّق بالتنفيذ). الإصدار ≥ C++ 14 النوع char كبير بما يكفي لتمثيل 256 قيمة مختلفة، وهو مناسب لتخزين وحدات رموز UTF-8. حجم الأنواع العددية الصحيحة المُؤشّرة وغير المُؤشّرة تنصّ المواصفات القياسية، في الفقرة 3.9.1.2، أنّه في قائمة أنواع الأعداد الصحيحة القياسية المُؤشّرة، التي تتكون من signed char و short int و int و long int و long long int، فإنّّ كل نوع سيوفّر مساحة تخزينية تكافئ على الأقل المساحة التخزينيّة للنّوع السابق في القائمة. إضافة لذلك، وكما هو مُوضّح في الفقرة 3.9.1.3، كل نوع من هذه الأنواع يقابله نوع صحيح قياسي غير مُؤشّر، هذه الأنواع هي: unsigned char unsigned short int unsigned int unsigned long int unsigned long long int هذه الأنواع في النهاية لها نفس حجم ومُحاذاة النوع المؤشّر المقابل بالإضافة إلى ذلك، وكما هو مُوضّح في الفقرة 3.9.1.1، النوع char له نفس متطلّبات signed char و unsigned char فيما يخصّ الحجم والمحاذاة. الإصدار < C++ 11 قبل الإصدار C++ 11، لم يكن long long و unsigned long long جزءًا من معيار C++. لكن بعد إدخالهما في لغة C في معيار C99، دعمت العديدُ من المصرّفات النوع long long كنوع عددي صحيح مؤشّر، و unsigned long long كنوع عددي صحيح غير مؤشّر موسّع، له نفس قواعد أنواع C. يضمن المعيار ما يلي: 1 == sizeof(char) == sizeof(signed char) == sizeof(unsigned char) <= sizeof(short) == sizeof(unsigned short) <= sizeof(int) == sizeof(unsigned int) <= sizeof(long) == sizeof(unsigned long) الإصدار ≥ C++ 11 <= sizeof(long long) == sizeof(unsigned long long) لا ينصّ المعيار على حد أدنى للأحجام لكل نوع على حدة. بدلاً من ذلك، لكلّ نوعٍ من الأنواع مجالًا من الحدود الدنيا التي يمكن أن يدعمها، والتي تكون موروثة على النحو المُوضّح في الفقرة 3.9.1.3 من معيار C، في الفقرة 1.2.4.2.1. ويمكن استنتاج الحد الأدنى لحجم نوع ما من ذلك المجال من خلال تحديد الحد الأدنى لعدد البتّات المطلوبة، لاحظ أنّه في معظم الأنظمة قد يكون المجال المدعوم الفعلي لأيّ نوع أكبر من الحد الأدنى، وأنه في الأنواع المُؤشّرة تتوافق المجالات مع مكمّل (complement) واحد، وليس مع مكمّلين اثنين كما هو شائع؛ وذلك لتسهيل توافق مجموعة واسعة من المنصات مع المعايير. النوع النطاق الأدنى العدد الأدنى المطلوب للبِتَّات signed char -127 إلى 127 8 unsigned char 0 إلى 255 8 signed short -32,767 إلى 32,767 16 unsigned short 0 إلى 65,535 16 signed int -32,767 إلى 32,767 16 unsigned int 0 إلى 65,535 16 signed long -2,147,483,647 إلى 2,147,483,647 32 unsigned long 0 إلى 4,294,967,295 32 table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; } الإصدار ≥ C++ 11 النوع النطاق الأدنى العدد الأدنى المطلوب للبِتَّات signed long long -9,223,372,036,854,775,807 إلى 9,223,372,036,854,775,807 64 unsigned long long 0 إلى 18,446,744,073,709,551,615 64 قد تختلف أحجام الأنواع من تنفيذ لآخر بما أنه يُسمح للأنواع أن تكون أكبر من الحد الأدنى لمتطلّبات الحجم، وأبرز مثال على ذلك تجده في نموذَجي البيانات المخزّنة على 64 بتّة: LP64 و LLP64، إذ أنّه في أنظمة LLP64 (مثل Windows 64-bit)، فإنّ الأنواع ints و long تُخزّن على 32 بتّة (32-bit)، وفي LP64 (مثل 64-bit Linux)، فإنّ int مخزّنة على 32 بتّة، أمّا long فمُخزّنة على 64 بتّة. لهذا من الخطأ افتراض أنّ أنواع الأعداد الصحيحة لها نفس الحجم في جميع الأنظمة. الإصدار ≥ C++ 11 إذا كانت هناك حاجة إلى أنواع عددية صحيحة ذات أحجام ثابتة فاستخدم أنواعًا من الترويسة ، لكن لاحظ أنّ المعيار لا يجبر التنفيذ (implementations) على دعم الأنواع ذات الحجم المضبوط: int8_t int16_t int32_t int64_t intptr_t uint8_t uint16_t uint32_t uint64_t uintptr_t الإصدار ≥ C++ 11 حجم char16_t و char32_t يتعلق حجم char16_t و char32_t بالتنفيذ كما ينصّ على ذلك المعيار في الفقرة 5.3.3.1، مع الشروط الواردة في الفقرة 3.9.1.5: char16_t كبير بما يكفي لتمثيل أيّ وحدة رمز من UTF-16، كما أنّ له نفس الحجم والإشارة والمحاذاة التي للنوع uint_least16_t، وعليه فحجمه يساوي 16 بتّة على الأقل. char32_t كبير بما يكفي لتمثيل أي وحدة رمز في UTF-32، كما أنّ له نفس الحجم والإشارة والمحاذاة التي للنوع uint_least32_t، وعليه فيجب أن يساوي حجمه 32 بتّة على الأقل. حجم bool يتعلق حجم bool بالتنفيذ، ولا يساوي بالضرورة 1. حجم wchar_t wchar_t، كما هو مُوضّح في الفقرة 3.9.1.5 هو نوع متميّز إذ يمكن أن يمثّل مجال قيمه كل وحدات الرموز (code unit) في أكبر مجموعة محارف موسّعة من بين اللغات المدعومة، وله نفس الحجم والإشارة والمحاذاة لأحد الأنواع العددية الصحيحة الأخرى، والذي يمثّل نوعه الأساسي (underlying type). يتعلق حجم هذا النوع بالتنفيذ كما هو مُعرَّف في الفقرة 5.3.3.1، وقد يساوي على سبيل المثال 8 أو 16 أو 32 بتّة على الأقل؛ وإذا كان النظام يدعم اليونيكود فينبغي أن يُخزّن النوع wchar_t على 32 بتّة على الأقل (باستثناء Windows، إذ يُخزّن النّوع wchar_t على 16 بتّة لأغراض التوافق). وهو موروث من معيار C90، في ISO 9899: 1990 الفقرة 4.1.5، لكن مع بعض التعديلات البسيطة. ويساوي حجم wchar_t غالبًا 8 أو 16 أو 32 بتّة، وذلك حسب التنفيذ. مثلًا: في أنظمة يونكس والأنظمة المشابهة لها، تُخزّن wchar_t على 32 بتّة، وعادة ما تستخدم في UTF-32. في ويندوز، تُخزّن wchar_t على 16 بتّة، وتُستخدم مع UTF-16. على الأنظمة التي لا تدعم إلّا 8 بتّات فقط، تُخزّن wchar_t على 8 بتّات. الإصدار ≥ C++ 11 إذا كنت تريد دعم اليونيكود، فإنّه يُوصى باستخدام char لأجل الترميز UTF-8، و char16_t لأجل الترميز UTF-16، و char32_t لأجل الترميز UTF-32، بدلاً من استخدام wchar_t. نماذج البيانات يمكن أن تختلف أحجام أنواع الأعداد الصحيحة بين المنصات كما ذكرنا أعلاه، وما يلي هي أكثر النماذج شيوعًا (الأحجام محسوبة بالبتّات): النموذج int long مؤشر LP32 (2/4/4) 16 32 32 ILP32 (4/4/4) 32 32 32 LLP64 (4/4/8) 32 32 64 LP64 (4/8/8) 32 64 64 من بين هذه النماذج: نظام ويندوز 16-بت استخدم LP32. الأنظمة الشبيهة بيونكس مثل يونكس ولينكس ونظام ماك 10 (Mac OSX) وغيرها، ذات معمارية 32 منها، وكذلك نظام ويندوز 32-بت، تستخدم جميعها ILP32. ويندوز 64-bit يستخدم LLP64. اليونكسات ذات معمارية 64-bit تستخدم LP64. لاحظ أنّ هذه النماذج غير مذكورة في المعيار. النوع Char قد يكون مُؤشَّرًا أو لا لا ينصّ المعيار على وجوب أن يكون النوع char مؤشَّرًا من عدمه، لذا فإنّّ كل مصرّف ينفذه بشكل مختلف، أو قد يسمح بتعديله باستخدام سطر الأوامر. مجالات الأنواع العددية تتعلّق مجالات أنواع الأعداد الصحيحة بالتنفيذ، كما توفّر الترويسة قالب std::numeric_limits<T> الذي يوفّر الحدّين الأدنى والأقصى لقيم جميع الأنواع الأساسية. تفي القيم بالضمانات التي يحدّدها معيار C عبر الترويسات و C++ 11 <= ) <cinttypes) std::numeric_limits<signed char>::min() - تساوي SCHAR_MIN، وهي أصغر من أو تساوي -127. std::numeric_limits::max() تساوي SCHAR_MAX، وهي أكبر من أو تساوي 127. std::numeric_limits<unsigned char>::max() تساوي UCHAR_MAX، وهي أكبر من أو تساوي 255. std::numeric_limits<short>::min() تساوي SHRT_MIN، وهي أصغر من -32767 أو تساويها. std::numeric_limits<short>::max() تساوي SHRT_MAX، وهي أكبر من أو تساوي 32767. std::numeric_limits<unsigned short>::max() تساويUSHRT_MAX، وهي أكبر من أو تساوي 65535. std::numeric_limits<int>::min() تساوي INT_MIN، وهي أصغر من -32767 أو تساويها. std::numeric_limits<int>::max() تساوي INT_MAX، وهي أكبر من أو تساوي 32767. std::numeric_limits<unsigned int>::max() تساوي UINT_MAX، وهي أكبر من أو تساوي 65535. std::numeric_limits<long>::min() تساوي LONG_MIN، وهي أصغر من أو تساوي -2147483647. std::numeric_limits<long>::max() تساوي LONG_MAX، وهي أكبر من أو تساوي 2147483647. std::numeric_limits<unsigned long>::max() تساوي ULONG_MAX، وهي أكبر من أو تساوي 4294967295. الإصدار ≥ C++11 std::numeric_limits<long long>::min() تساوي LLONG_MIN، وهي أكبر من أو تساوي -9223372036854775807. std::numeric_limits<long long>::max() تساوي LLONG_MAX، وهي أكبر من أو تساوي 9223372036854775807. std::numeric_limits<unsigned long long>::max() تساوي ULLONG_MAX، وهي أكبر من أو تساوي 18446744073709551615. بالنسبة للنوع العشري T، فإنّّ max() تمثّل القيمة المنتهية (finite) القصوى، بينما تمثّل min() الحدّ الأدنى للقيمة المُوحّدة الموجبة. تمّ توفير بعض الأعضاء الإضافيين للأنواع العشرية، وهي متعلّقة بالتنفيذ أيضًا، ولكنّها تلبّي بعض الضمانات التي يحدّدها المعيار C عبر الترويسة . يعيد digits10 عدد الأرقام العشرية الخاصّة بالدقة. std::numeric_limits<float>::digits10 تساوي FLT_DIG، والتي لا تقلّ عن 6. std::numeric_limits<double>::digits10 تساوي DBL_DIG، والتي لا تقلّ عن 10. std::numeric_limits<long double>::digits10 تساوي LDBL_DIG، والتي لا تقلّ عن 10. العضو min_exponent10 هو الحد الأدنى السلبي E بحيث يكون 10 أسّ E طبيعيًّا. std::numeric_limits<float>::min_exponent10 تساوي FLT_MIN_10_EXP، والتي يساوي على الأكثر -37. std::numeric_limits<double>::min_exponent10 تساوي DBL_MIN_10_EXP، والتي تساوي على الأكثر -37. std::numeric_limits<long double>::min_exponent10 تساوي LDBL_MIN_10_EXP، والتي تساوي على الأكثر -37. العضو max_exponent10 هو الحد الأقصى E بحيث يكون 10 أسّ E منتهيًا (finite). std::numeric_limits<float>::max_exponent10 يساوي FLT_MIN_10_EXP، ولا يقل عن 37. std::numeric_limits<double>::max_exponent10 يساوي DBL_MIN_10_EXP، ولا يقل عن 37. std::numeric_limits<long double>::max_exponent10 تساوي LDBL_MIN_10_EXP، ولا يقل عن37. إذا كان العضو is_iec559 صحيحًا، فإنّّ النوع سيكون مطابقًا للمواصفات IEC 559 / IEEE 754، وبالتالي سيُحدَّد مجاله من قبل المعيار. تمثيل قيم الأنواع العشرية ينصّ المعيار أن لا تقلّ دقة النوع long double عن دقة النوع double، والذي ينبغي ألّا تقل دقّته عن دقّة النوع float؛ وأنّ النوع long double ينبغي أن يكون قادرًا على تمثيل أيّ قيمة يمثّلها النوع double، وأن يمثّل double أيّ قيمة يمكن أن يمثّلها النوع float، أما تفاصيل التمثيل فتتعلّق بالتنفيذ. وبالنسبة لنوع عشري T، فإنّّ std::numeric_limits<T>::radix تحدّد الجذر المُستخدم في تمثيل T، وإذا كانت std::numeric_limits<T>::is_iec559 صحيحة، فإنّّ تمثيل T يطابق أحد التنسيقات المُعرَّفة من قبل معيار IEC 559 / IEEE 754. التدفق الزائد عند التحويل من عدد صحيح إلى غد صحيح مُؤشّر عند تحويل عدد صحيح مؤشّر أو غير مؤشّر إلى نوع عددي صحيح مؤشّر ولا تكون قيمته قابلة للتمثيل في النوع المقصود، فإنّّ القيمة المُنتجة تتعلّق بالتنفيذ. مثلًا، لنفرض أن مجال النوع signed char في هذا التنفيذ يكون [-128,127]، ومجال النوع unsigned char من 0 حتى 255: int x = 12345; signed char sc = x; // معرفة بالتنفيذ sc قيمة unsigned char uc = x; // مهيأة عند القيمة 57 uc النوع الأساسي وحجم التعدادات إذا لم يكن النوع الأساسي (underlying type) لنوع تعدادي مُحدّدا بشكل صريح، فإنّّه سيُعرَّف من قبل التنفيذ. enum E { RED, GREEN, BLUE, }; using T = std::underlying_type<E>::type; // يعرِّفه التنفيذ ومع ذلك، فإنّّ المعيار ينصّ على ألّا يكون نوع التعداد الأساسي أكبر من int إلّا إن لم يكن النوعان int و unsigned int قادريْن على تمثيل جميع قيم التعداد. لذلك في الشيفرة أعلاه، النوع T يمكن أن يكون int أو unsigned int أو short ولكن ليس long long مثلًا. لاحظ أنّ التعداد له نفس حجم نوعه الأساسي (كما يعيده التابع sizeof). القيمة العددية لمؤشر نتيجة تحويل مؤشّر إلى عدد صحيح باستخدام reinterpret_cast تتعلّق بالتنفيذ، لكن "… يُهدف إلى ألا تكون النتيجة مفاجئة للمطوّرين الذين يعرفون نظام العنونة في الجهاز." int x = 42; int *p = &x; long addr = reinterpret_cast<long> (p); std::cout << addr << "\n"; // طبع رقم عنوان وبالمثل، فإنّّ المُؤشر الناتج عن تحويل عدد صحيح يكون أيضًا متعلّقًا بالتنفيذ، والطريقة الصحيحة لتخزين مؤشّر كعدد صحيح هي استخدام النوعين uintptr_t أو intptr_t، انظر المثال التالي حيث لم يكن uintptr_t في C++03 وإنما في C99، كنوع اختياري في الترويسة : #include <stdint.h> uintptr_t uip; الإصدار ≥ C++ 11 يوجد std::uintptr_t اختياري في C++11: #include <cstdint> std::uintptr_t uip; تُحيل C++ 11 إلى C99 لتعريف uintptr_t (المعيار C99، 6.3.2.3): بالنسبة لغالبية المنصات الحديثة، يمكنك أن تفترض أنّ مساحة العنونة (address space) مسطحة وأنّ الحسابيات على uintptr_t تكافئ الحسابيات على char *، ومن الممكن أن يجري التنفيذ أيّ تعديل عند تحويل void * إلى uintptr_t طالما أنّه يمكن عكس التعديل عند التحويل من uintptr_t إلى void *. مسائل تقنية في الأنظمة المتوافقة مع XSI (X/Open System Interfaces)، فإنّّ النوعين intptr_t و uintptr_t إلزاميان، أمّا في الأنظمة الأخرى فهما اختياريان. الدوال ليست كائنات ضمن معيار C، ولا يضمن معيار C أنّ uintptr_t يستطيع تخزين مؤشّر دالة. كذلك يتطّلب التوافق مع POSIX (2.12.3) أنّ: معيار C99 الفقرة 7.18.1: قد يكون uintptr_t مناسبًا إذا كنت تريد العمل على بتّات المؤشّر بشكل قد يتعذّر في حال استخدمت عددًا صحيحًا مؤشّرًا. عدد البتات في البايت البايت (byte) في C++ هو المساحة التي يشغلها كائن char، وعدد البتات في البايت مُحدّد في CHAR_BIT، ومُعرّف في climits ولا يقل عن 8. وفي حين أن عدد بتّات البايت في معظم الأنظمة الحديثة هو 8 وأن أنظمة POSIX تتطلّب أن يساوي CHAR_BIT القيمة 8 بالضبط، إلا أنّ هناك بعض الأنظمة التي يكون فيها CHAR_BIT أكبر من 8، فقد يساوي مثلًا 8 أو 16 أو 32 أو 64 بتّة. هذا الدرس جزء من سلسلة دروس عن C++. ترجمة -بتصرّف- للفصل Chapter 71: Implementation-defined behavior من كتاب C++ Notes for Professionals
-
التعبيرات النمطية (تُسمّى أحيانًا regexs أو regexps) هي صِيغ نصّية تمثّل الأنماط التي يمكن مطابقتها في السلاسل النصّية، وقد تدعم التعبيرات النمطيّة التي قُدِّمت في C++ 11 -اختياريًا- إعادة مصفوفة من السلاسل النصّية المطابِقة، أو صيغة نصّية أخرى تحدّد كيفيّة استبدال الأنماط المتطابقة في السلاسل النصية. الصياغة الأولى للدالة regex_match: bool regex_match(BidirectionalIterator first, BidirectionalIterator last, smatch& sm, const regex& re, regex_constraints::match_flag_type flags) يمثّل BidirectionalIterator أيَّ مُكرّر محارف يوفّر عامليْ الزيادة (increment) والإنقَاص (decrement). والوسيط smatch يمكن أن يكون كائنًا cmatch أو أيّ متغيّر آخر من الصنف match_results يقبل النوع BidirectionalIterator، ويمكن حذف هذا الوسيط إذا لم تكن بحاجة إلى إعادة نتائج التعبير النمطي. وتعيد ما إذا كان التعبير re يطابق كامل تسلسل المحارف المُعُرّف بواسطة first و last. الصياغة الثانية للدالة regex_match: bool regex_match(const string& str, smatch& sm, const regex re&, regex_constraints::match_flag_type flags) قد تكون string من النّوع const char* أو قيمة نصّية يسارية، وتُحذَف الدوالّ التي تقبل سلسلة نصيّة يمينية R- بشكل صريح. الوسيط smatch يمكن أن يكون كائنًا cmatch أو أيّ متغيّر آخر من الصنف match_results يقبل سلسلة نصية. ويمكن حذف الوسيط smatch إذا لم تكن بحاجة إلى إعادة نتائج التعبير النمطي .تعيد الدالة regex_match ما إذا كان التعبير re قد طابق كامل السلسلة النصية str. أمثلة عن regex_match و regex_search const auto input = "Some people, when confronted with a problem, think \"I know, I'll use regular expressions.\""s; smatch sm; cout << input << endl; إن انتهى input بعلامة تنصيص تحتوي كلمة تبدأ بالكلمة "reg" وكلمة أخرى تبدأ بـ "ex" فالتقط الجزء السابق من input. if (regex_match(input, sm, regex("(.*)\".*\\breg.*\\bex.*\"\\s*$"))) { const auto capture = sm[1].str(); cout << '\t' << capture << endl; // الخرج: "\tSome people, when confronted with a problem, think\ n "; ابحث في الجزء الملتقط عن "a problem" أو "problems #". if (regex_search(capture, sm, regex("(a|d+)\\s+problems?"))) { const auto count = sm[1] == "a"s ? 1 : stoi(sm[1]); cout << '\t' << count << (count > 1 ? " problems\n" : " problem\n"); // الخرج: --> "\t1 problem\ n " cout << "Now they have " << count + 1 << " problems.\n"; // الخرج: "Now they have 2 problems\ n " } } هذا مثال حيّ على ذلك. مثال عن مُكرّر التعبيرات النمطية regex_iterator تُعد regex_iterator خيارًا ممتازًا عند معالجة الخرج الملتقَط بشكل متكرر، وسيعيد تحصيل regex_iterator كائن match_result، وهذا يفيد في الالتقاطات الشرطية (conditional captures) أو الالتقاطات المترابطة. لنقل أنّنا نريد تقطيع (tokenize) مقتطف من شيفرة C++: enum TOKENS { NUMBER, ADDITION, SUBTRACTION, MULTIPLICATION, DIVISION, EQUALITY, OPEN_PARENTHESIS, CLOSE_PARENTHESIS }; يمكننا تقطيع هذه السلسلة النصّية: const auto input = "42/2 + -8\t=\n(2 + 2) * 2 * 2 -3"s باستخدام مكرّر تعبيرات نمطية regex_iterator على النحو التالي: vector<TOKENS> tokens; const regex re { "\\s*(\\(?)\\s*(-?\\s*\\d+)\\s*(\\)?)\\s*(?:(\\+)|(-)|(\\*)|(/)|(=))" }; for_each(sregex_iterator(cbegin(input), cend(input), re), sregex_iterator(), [& ](const auto &i) { if (i[1].length() > 0) { tokens.push_back(OPEN_PARENTHESIS); } tokens.push_back(i[2].str().front() == '-' ? NEGATIVE_NUMBER : NON_NEGATIVE_NUMBER); if (i[3].length() > 0) { tokens.push_back(CLOSE_PARENTHESIS); } auto it = next(cbegin(i), 4); for (int result = ADDITION; it != cend(i); ++result, ++it) { if (it->length() > 0 U) { tokens.push_back(static_cast<TOKENS> (result)); break; } } }); match_results<string::const_reverse_iterator > sm; if (regex_search(crbegin(input), crend(input), sm, regex { tokens.back() == SUBTRACTION ? "^\\s*\\d+\\s*-\\s*(-?)" : "^\\s*\\d+\\s*(-?)" })) { tokens.push_back(sm[1].length() == 0 ? NON_NEGATIVE_NUMBER : NEGATIVE_NUMBER); } هذا مثال حيّ. ينبغي أن يكون وسيط regex قيمة يسارية (L-value)، إذ أنّ القيم اليمينيّة لن تعمل. المراسي (Anchors) توفّر C++ أربع مراسي فقط: ^ - تمثّل بداية السلسلة النصّية $ - تمثّل نهاية السلسلة النصّية \b - تمثّل محرف \W أو بداية أو نهاية السلسلة النصّية. \B - تمثّل محرف \w. في المثال التالي سنحاول التقاط عددٍ مع إشارَتِه: auto input = "+1--12 * 123/+1234"s; smatch sm; if (regex_search(input, sm, regex { "(?:^|\\b\\W)([+-]?\\d+)" })) { do { cout << sm[1] << endl; input = sm.suffix().str(); } while (regex_search(input, sm, regex { "(?:^\\W|\\b\\W)([+-]?\\d+)" })); } هذا مثال حيّ لاحظ أنّ المرساة لا تستهلك أيّ محرف. مثال على استخدام regex_replace تأخذ هذه الشيفرة عدّة أنماط من الأقواس، وتعيد تنسيقها إلى نمط K&R أو كيرنيجان وريتشي (إشارة إلى أسلوب الأقواس المستخدم في نواة يونكس الأولى). const auto input = "if (KnR)\n\tfoo();\nif (spaces) {\n foo();\n}\nif (allman)\n{\n\tfoo();\n}\nif (horstmann)\n{\tfoo();\n}\nif (pico)\n{\tfoo(); }\nif (whitesmiths)\n\t{\n\tfoo();\n\t}\n"s; cout << input << regex_replace(input, regex("(.+?)\\s*\\{?\\s*(.+?;)\\s*\\}?\\s*"), "$1{\n\t$2\n}\n") << endl; مثال حيّ. مثال على استخدام regex_token_iterator std::regex_token_iterator هي أداة مفيدة للغاية لاستخراج عناصر من ملف يحتوي قيمًا مفصولة بفواصل، كما أنّها قادرة أيضًا على التقاط الفواصل، على خلاف الطرق الأخرى التي تجد صعوبة في ذلك: const auto input = "please split,this,csv, ,line,\\,\n"s; const regex re{ "((?:[^\\\\,]|\\\\.)+)(?:,|$)" }; const vector<string> m_vecFields{ sregex_token_iterator(cbegin(input), cend(input), re, 1), sregex_token_iterator() }; cout << input << endl; copy(cbegin(m_vecFields), cend(m_vecFields), ostream_iterator<string>(cout, "\n")); مثال حي. ينبغي أن يكون الوسيط regex قيمة يسارية (L-value)، فالقِيم اليمينيّة (R-value) لن تعمل. المحدِّدات الكمية لنفترض أنّ لدينا سلسلة نصية ثابتة (const string input) تحتوي رقم هاتف وعلينا أن نتحقّق من صحّته. يمكن أن نبدأ بطلب مدخلات رقمية مع أيّ عدد من المحدِّدات الكمية regex_match(input, regex("\\d*"))، أو مع محدِّد كمي regex_match(input, regex("\\d+")) واحد أو أكثر، بيْد أنّ كليهما سيفشلان إذا كانت المدخلات input تحتوي على سلسلة نصّية رقمية غير صالحة مثل: "123". سنستخدم n أو أكثر من المحدِّدات الكمية للتحقّق من أنّنا حصلنا على 7 أرقام على الأقل: regex_match(input, regex("\\d{7,}")) سيضمن هذا أنّنا سنحصل على العدد الصحيح من أرقام الهاتف، لكن يمكن أن تحتوي input أيضًا على سلسلة رقمية أطول ممّا ينبغي مثل: "123456789012"، لذلك فالحلّ هو استخدام محدِّد كمي بين n و m بحيث يكون عدد أحرف input محصورًا بين 7 و 11 رقمًا regex_match(input, regex("\\d{7,11}")); هذا أفضل، لكن ما تزال هنا مشكلة، إذ ينبغي أن ننتبه إلى السلاسل الرقمية غير القانونية التي تقع في النطاق [7، 11]، مثل: "123456789"، لذا دعنا نجعل رمز البلد (country code) اختياريًا عبر استخدام محدِّد كمي كسول(lazy quantifier): regex_match(input, regex("\\d?\\d{7,10}")) من المهمّ أن تعلم أنّ المحدِّد الكمي الكسول يحاول مطابقة أقل عدد ممكن من المحارف، وعليه فإنّّ الطريقة الوحيدة للمطابقة هي إذا كانت هناك فعليًا 10 محارف متطابقة مع \d{7,10}. (لمطابقة الحرف الأول بطمع (greedy)، سيكون علينا استخدام: \d{0,1}}.) يمكن ضمّ المحدِّد الكمي الكسول (lazy quantifier) إلى أيّ محدِّد كمي آخر. الآن، كيف يمكننا جعل رمز المنطقة اختياريًا، وعدم قبول رمز الدولة إلّا في حال كان رمز المنطقة موجودًا؟ regex_match(input, regex("(?:\\d{3,4})?\\d{7}")) تتطّلب \d{7} في هذا التعبير النمطي النهائي سبعة أرقام، ومسبوقة -اختياريًا- إما بثلاثة أو أربعة أرقام. لاحظ أنّنا لم نضم المحدِّد الكسول : \d{3,4}?\d{7}، إذ أنّ \d{3,4}? يمكن أن يطابق إمّا 3 أو 4 محارف، بيْد أنّه يُفضّل الاكتفاء بـ 3 محارف، ( لهذا يُسمّونه كسولًا). بدلاً من ذلك، جعلنا المجموعة غير الملتقِطة (non-capturing group) لا تنجح في المُطابقة إلّا مرّة واحدة على الأكثر، مع تفضيل عدم التطابق. وهذا يتسبّب في منع التطابق إذا لم تتضمن input رمز المنطقة، كما في: "1234567". أود أن أشير في ختام موضوع المحدِّدات الكمية، إلى محدِّد آخر يمكنك استخدامه، وهو المحدِّد الكمي المُتملِّك (possessive quantifier). كلا المحدِّديْن سواءً المكمّم القنوع أو المكمّم المتملّك، يمكن ضمّهما إلى أيّ مكمّم آخر. وظيفة المكمّم المتملّك الوحيدة هي مساعدة محرّك التعبير النمطي عبر إخباره بأخذ ما أمكن من الأحرف المطابقة وعدم التخلي عنها حتى لو تسبّب ذلك في فشل التعبير النمطي. على سبيل المثال، التعبير التالي لا معنى له: regex_match(input, regex("\\d{3,4}+\\d{7})) لأنّ مُدخلًا input مثل:" 1234567890 " لن يُطابَق بالتعبير \d{3,4}+، وسيُطابق دائمًا بأربعة محارف، حتى لو كانت مطابقة 3 محارف كافية لإنجاح التعبير النمطي. يُستخدم المحدِّد المتملّك عادة عندما تحدّ الوحدة المحدَّدة كميًا عددَ المحارف القابلة للمطابقة. على سبيل المثال: regex_match(input, regex("(?:.*\\d{3,4}+){3}")) يمكن استخدامها إذا كانت input تحتوي أيًّا ممّا يلي: 123 456 7890 123-456-7890 (123)456-7890 (123) 456 - 7890 بيْد أنّ فائدة هذا التعبير النمطي تظهر عندما تتضمّن input مُدخلاً غير صالح، مثل: 12345 - 67890 بدون استخدام المحدِّد الكمي المتملّك، سيتعيّن على محرّك التعبير النمطي الرجوع واختبار كل توليفات .*، سواء مع 3 أو 4 محارف للتحقّق ممّا إن كان يستطيع العثور على تركيبة مطابقة. سيبدأ التعبير النمطي، باستخدام المحدِّد المتملّك، من حيث توقّف المحدِّد المتملّك الثاني، أي المحرف "0"، ثمّ سيحاول محرّك التعبير النمطي ضبط .* للسماح بمطابقة \d{3,4}؛ وفي حال تعذّر ذلك سيفشل التعبير النمطي، ولن يرجِع للخلف للتحقّق ممّا إذا كان من الممكن إنجاح المطابقة عبر إعادة ضبط .* في مرحلة أبكر. تقطيع سلسلة نصية Splitting a string هذا مثال توضيحيّ على كيفية تقسيم سلسلة نصّية: std::vector<std::string> split(const std::string &str, std::string regex) { std::regex r{ regex }; std::sregex_token_iterator start{ str.begin(), str.end(), r, -1 }, end; return std::vector<std::string>(start, end); } split("Some string\t with whitespace ", "\\s+"); // "Some", "string", "with", "whitespace" هذا الدرس جزء من سلسلة دروس عن C++. ترجمة -بتصرّف- للفصل Chapter 70: Regular expressions من كتاب C++ Notes for Professionals
-
التكرار Iteration do تقدِّم تعليمة do حلقة do-while، انظر المثال التالي حيث نحصل على المحرف التالي غير الفارغ من مجرى الدخل القياسي: char read_char() { char c; do { c = getchar(); } while (isspace(c)); return c; } while تقدّم تعليمة while حلقة while. int i = 0; // اطبع عشر نجمات while (i < 10) { putchar('*'); i++; } حلقة for النطاقية range-based std::vector primes = {2, 3, 5, 7, 11, 13}; for (auto prime: primes) { std::cout << prime << std::endl; } for تقدّم تعليمة for حلقة for، يمكن استخدامها في C++ 11 والإصدارات الأحدث لتقديم حلقة for النطاقية (range-based for loop). // اطبع عشر نجمات for (int i = 0; i < 10; i++) { putchar('*'); } التكرار على تعداد ليس هناك حلٌّ مضمَّن مسبقًا للتكرار (iterate) على كائن تعداد (enumeration)، لكن هناك عدة طرق أخرى لذلك: بالنسبة للتعدادات (enum) ذات القيم المتتالية: enum E { Begin, E1 = Begin, E2, // .. En, End }; for (E e = E::Begin; e != E::End; ++e) { // e افعل شيئا ما بـ } الإصدار ≥ C++ 11 يجب تنفيذ العامل operator ++ مع enum class: E& operator ++ (E& e) if (e == E::End) { throw std::out_of_range("for E& operator ++ (E&)"); } e = E(static_cast < std::underlying_type < E > ::type > (e) + 1); return e; } استخدام حاوية كمتّجه (std::vector): enum E { E1 = 4, E2 = 8, // .. En }; std::vector<E> build_all_E() { const E all[] = {E1, E2, /*..*/ En}; return std::vector<E>(all, all + sizeof(all) / sizeof(E)); } std::vector < E > all_E = build_all_E(); ثمّ: for (std::vector<E>::const_iterator it = all_E.begin(); it != all_E.end(); ++it) { E e = *it; // e افعل شيئا ما بـ } الإصدار ≥ C++ 11 أو يمكن استخدام std::initializer_list: enum E { E1 = 4, E2 = 8, // .. En }; constexpr std::initializer_list < E > all_E = { ثم بعد ذلك: for (auto e: all_E) { // e افعل شيئا ما بـ } التعدادات النطاقية (Scoped enums) قدّمت C++ 11 ما يعرف باسم التعداد النطاقي، وهي تعدادات يلزَم تأهيل أعضائها عبر enumname::membername. ويُصرَّح عن التعدادات النطاقية باستخدام الصيغة enum class، فمثلًا لتخزين ألوان قوس قزح، نكتب ما يلي: enum class rainbow { RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, VIOLET }; وللوصول إلى لون معين: rainbow r = rainbow::INDIGO; لا يمكن تحويل أصناف التعدادات (enum class) ضمنيًا إلى أعداد صحيحة (int) بدون تحويل النوع (cast). لذلك فإنّ التعبير int x = rainbow::RED غير صالح. تتيح لك التعدادات النطاقية أيضًا تحديد النوع الأساسي (underlying type)، أي النوع المستخدم لتمثيل العضو. والذي يساوي افتراضيًا النوع int. مثلًا، في لعبة تيك تاك تو (Tic-Tac-Toe)، يمكنك تخزين القطع كما يلي: enum class piece: char { EMPTY = '\0', X = 'X', O = 'O', }; لعلك انتبهت إلى أن التعداد enum يمكن أن يحتوي فاصلة زائدة بعد العضو الأخير. التصريح المسبق عن التعدادات في C++ 11 يمكن التصريح المسبق (forward declaration) عن التعدادات النطاقية على النحو التالي: ... enum class Status; // تصريح مسبق Status doWork(); // استخدام التصريح المسبق ... enum class Status { Invalid, Success, Fail }; Status doWork() // يلزم تصريح كامل لأجل التقديم { return Status::Success; } أمّا التعدادات غير النطاقية، فيمكن التصريح عنها مُسبقًا عبر: ... enum Status: int; // تصريح مسبق، يلزم نوع صريح Status doWork(); // استخدام التصريح المسبق ... enum Status: int { Invalid = 0, Success, Fail }; // يلزم مطابقة نوع التصريح المسبق static_assert(Success == 1); انظر إن شئت مثال "تاجر الفاكهة الأعمى" الأكثر تعقيدًا. التصريح بالتعداد Enumeration Declaration تسمح التعدادات القياسية للمستخدمين بالتصريح عن اسم يمثّل مجموعة من الأعداد الصحيحة، وتسمى هذه الأسماء مجتمعةً عدّادات (enumerators)، وتُعرّف التِّعدادات والعَدّادات المرتبطة بها على النحو التالي: enum myEnum { enumName1, enumName2, }; يعدّ التعداد نوعًا متميزًا عن جميع الأنواع الأخرى، ويكون اسم هذا النوع في المثال أعلاه هو myEnum، ويُتوقّع أن تُخمِّن كائنات هذا النوع قيمة العدّاد داخل التعداد. العدّادات المُصرّح عنها ضمن التعداد تكون قيمًا ثابتة من نوع ذلك التعداد، ورغم أنّ العدّادات تُعلَن ضمن النوع، إلا أنّ عامل النطاق - scope operator - :: ليس ضروريًا للوصول إلى الاسم. لذلك، فاسم العدّاد الأول سيكون enumName1. الإصدار ≥ C++ 11 يمكن استخدام عامل النطاق -اختياري- للوصول إلى عدّاد داخل التعداد. لذلك، يمكن كتابة enumName1 على النحو التالي myEnum::enumName1. وتُسند إلى العدّادات قيم عددية صحيحة تبدأ من 0 وتزداد بـ 1 لكل عدّاد إضافي في التعداد. لذلك في المثال أعلاه، فإنّ قيمة enumName1 تساوي 0، وقيمة enumName2 تساوي 1. كذلك يمكن إسناد قيمة معيّنة إلى العدّادات من قبل المستخدم؛ وعندئذ يجب أن تكون تلك القيمة تعبيرًا عدديًا صحيحًا ثابتًا. أيضًا، سيُسند إلى العدّادات التي لم تُوفّر قيمها صراحة "قيمة العداد السابق + 1". enum myEnum { enumName1 = 1, // 1 القيمة ستكون enumName2 = 2, // 2 القيمة ستكون enumName3, // القيمة ستكون 5، أي القيمة السابقة + 1 enumName4 = 7, // 7 القيمة ستكون enumName5, // 8 القيمة ستكون enumName6 = 5, // القيمة ستكون 5، يجوز العودة إلى الوراء enumName7 = 3, // القيمة ستكون 3، يجوز إعادة استخدام الأعداد enumName8 = enumName4 + 2, // القيمة ستكون 9، يجوز أخذ العدادات السابقة وتعديلها }; التعدادات في تعليمات switch يشيع استخدام العدادات مع عبارات switch، ومن ثمّ فهي تظهر عادة في أجهزة الحالة (state machines). من الميزات المفيدة التي تقدّمها التعدادات في عبارات switch هو أنّه في حال لم تُضمَّن أيّ تعليمة افتراضية في switch، ولم تُستخدم جميع قيم التعداد، فسيُطلِق المصرّف تحذيرًا. enum State { start, middle, end }; ... switch (myState) { case start: ... case middle: ... } // warning: enumeration value 'end' not handled in switch [-Wswitch] هذا الدرس جزء من سلسلة دروس عن C++. ترجمة -وبتصرّف- للفصل Chapter 68: Enumeration من الكتاب C++ Notes for Professionals
-
الترتيب وحاويات التسلسلات std::sort هي خوارزمية لتَرتيب مجموعة من القي، وتوجد في ترويسة المكتبة القياسية algorithm، وهي مُعرّفة بواسطة زوج من المُكرّرات. وتأخذ std::sort كائنًا داليًّا كمعامل أخير للموازنة بين قيمتين، ثم تحدد الترتيب بناءً على ذلك. لاحظ أنّ std::sort ليست مستقرة. يجب أن تفرض دالّة الموازنة ترتيبًا صارمًا وضعيفًا على العناصر. وعمومًا فإن عامل الموازنة => و =< كافيان. وتُستخدم خوارزمية std::sort لترتيب حاوية ذات مُكرّر وصول عشوائي (random-access iterators): الإصدار ≥ C++ 11 #include <vector> #include <algorithm> std::vector<int> MyVector = {3, 1, 2} // < المقارنة الافتراضية لــ std::sort(MyVector.begin(), MyVector.end()); تتطلّب std::sort أن تكون المكرِّرات عشوائية الوصول، ولا توفّر الحاويَتان std::list و std::forward_list (منذ C++ 11) مُكرِّرات وصول عشوائي، لذا لا يمكن استخدامهما مع std::sort. لكن لديهما دالة sort التابعة، والتي تقدّم خوارزميةَ ترتيبٍ تعمل مع أنواع المكرّرات الخاصّة بها. الإصدار ≥ C++ 11 #include <list> #include <algorithm> std::list<int> MyList = {3, 1, 2} // < المقارنة الافتراضية لــ // ترتيب القائمة كاملة MyList.sort(); ترتب الدالة sort القائمة بأكملها دائمًا، لذا لا يمكنها ترتيب مجموعة فرعية (sub-range) من العناصر، لكن بأي حال بما أن لكلٍّ من القوائم (list) والقوائم الأمامية forward_list عمليات ربط سريعة (fast splicing operations)، فتستطيع استخراج العناصر التي تريد ترتيبها من القائمة ثم ترتِّبها، ثم تعيدها حيث كانت على النحو التالي: void sort_sublist(std::list < int > & mylist, std::list < int > ::const_iterator start, std::list < int > ::const_iterator end) { // استخراج وترتيب المجال الفرعي المحدد std::list < int > tmp; tmp.splice(tmp.begin(), list, start, end); tmp.sort(); // إعادة المجال حيث كان list.splice(end, tmp); } الترتيب باستخدام std::map تصاعديًا وتنازليًا يرتّب هذا المثال عناصر قاموس تصاعديًا بحسب المفتاح، وتستطيع استخدام أي نوع بما في ذلك الصنف بدلًا من std::string، انظر المثال التالي: #include <iostream> #include <utility> #include <map> int main() { std::map < double, std::string > sorted_map; // ترتيب أسماء الكواكب بحسب حجمها sorted_map.insert(std::make_pair(0.3829, "عطارد")); sorted_map.insert(std::make_pair(0.9499, "الزهرة")); sorted_map.insert(std::make_pair(1, "الأرض")); sorted_map.insert(std::make_pair(0.532, "المريخ")); sorted_map.insert(std::make_pair(10.97, "المشتري")); sorted_map.insert(std::make_pair(9.14, "زحل")); sorted_map.insert(std::make_pair(3.981, "أورانوس")); sorted_map.insert(std::make_pair(3.865, "نبتون")); for (auto const & entry: sorted_map) { std::cout << entry.second << " (" << entry.first << " من قطر الأرض)" << '\n'; } } الناتج سيكون: (عطارد (0.3829 من قطر الأرض (المريخ (0.532 من قطر الأرض (الزهرة (0.9499 من قطر الأرض (الأرض (1 من قطر الأرض (نبتون (3.865 من قطر الأرض (أورانوس (3.981 من قطر الأرض (زحل (9.14 من قطر الأرض (المشتري (10.97من قطر الأرض إذا كانت هناك مُدخلات ذات مفاتيح متساوية، فاستخدم القاموس المتعدّد multimap بدلًا من القاموس map (كما في المثال أدناه). لترتيب العناصر تنازليا، أعلِن عن القاموس باستخدام عامل موازنة مناسب (std::greater<>): #include <iostream> #include <utility> #include <map> int main() { std::multimap < int, std::string, std::greater < int >> sorted_map; // ترتيب أسماء الحيوانات تنازليا بحسب عدد الأرجل sorted_map.insert(std::make_pair(6, "حشرة")); sorted_map.insert(std::make_pair(4, "قطة")); sorted_map.insert(std::make_pair(100, "الحريشية")); sorted_map.insert(std::make_pair(2, "دجاجة")); sorted_map.insert(std::make_pair(0, "سمكة")); sorted_map.insert(std::make_pair(4, "فرس")); sorted_map.insert(std::make_pair(8, "عنكبوت")); for (auto const & entry: sorted_map) { std::cout << entry.second << " لها " << entry.first << " أرجل" << '\n'; } } الناتج سيكون: الحريشية لها 100 أرجل العنكبوت لها 8 أرجل الحشرة لها 6 أرجل القطة لها 4 أرجل الفرس لها 4 أرجل الدجاجة لها 2 أرجل السمكة لها 0 أرجل ترتيب حاويات التسلسل باستخدام عامل "أصغر من" سيرتِّب std::sort العناصر، إذا لم تُمرَّر أيّ دالة ترتيب، من خلال استدعاء العامل operator< على أزواج من العناصر، ويجب أن تُعيد نوعًا يمكن تحويله إلى قيمة بوليانية bool، وتحتوي الأنواع الأساسية (مثل الأعداد الصحيحة، والعشرية، والمؤشرات…) على عوامل موازنة. نستطيع زيادة تحميل هذا العامل لجعل استدعاء sort الافتراضي يعمل على الأنواع المُعرّفة من قبل المستخدم. انظر: // تضمين حاويات التسلسل #include <vector> #include <deque> #include <list> // تضمن خوارزمية الترتيب #include <algorithm> class Base { public: // v على القيمة variable منشئ يضبط Base(int v): variable(v) {} وهنا استخدم variable لتتيح عامل الترتيب الكامل "أصغر من" أو less، وستمثل this الجانب الأيسر من الموازنة دومًا، نتابع: bool operator < (const Base & b) const { return this -> variable < b.variable; } int variable; }; int main() { std::vector < Base > vector; std::deque < Base > deque; std::list < Base > list; أنشئ هنا عنصرين لترتيبهما: Base a(10); Base b(5); والآن أدرجهما في نهاية الحاوية: vector.push_back(a); vector.push_back(b); deque.push_back(a); deque.push_back(b); list.push_back(a); list.push_back(b); والآن، رتب البيانات باستخدام دالة (operator<(const Base &b: std::sort(vector.begin(), vector.end()); std::sort(deque.begin(), deque.end()); // بشكل مختلف List ينبغي ترتيب القائمة list.sort(); return 0; } ترتيب التسلسلات باستخدام دوال الموازنة إليك المثال التوضيحي التالي: // تضمين التسلسلات #include <vector> #include <deque> #include <list> // أدخل خوارزمية الترتيب #include <algorithm> class Base { public: // v على القيمة variable منشئ يضبط Base(int v): variable(v) {} int variable; }; bool compare(const Base & a, const Base & b) { return a.variable < b.variable; } int main() { std::vector < Base > vector; std::deque < Base > deque; std::list < Base > list; أنشئ عنصرين لترتيبهما: Base a(10); Base b(5); أدرج عنصرين في نهاية الحاوية: vector.push_back(a); vector.push_back(b); deque.push_back(a); deque.push_back(b); list.push_back(a); list.push_back(b); رتب البيانات باستخدام دالة الموازنة: std::sort(vector.begin(), vector.end(), compare); std::sort(deque.begin(), deque.end(), compare); list.sort(compare); return 0; } ترتيب التسلسلات باستخدام تعابير لامدا lambda (C++ 11) انظر المثال التوضيحي التالي: الإصدار ≥ C++ 11 // تضمين التسلسلات #include <vector> #include <deque> #include <list> #include <array> #include <forward_list> // تضمين خوارزمية الترتيب #include <algorithm> class Base { public: // v على القيمة variable منشئ يضبط Base(int v): variable(v) {} int variable; }; int main() { // أنشئ عنصرين لترتيبهما Base a(10); Base b(5); // لذا سنستخدم قائمة تهييئ لإدراج العناصر C++11 نحن نستخدم std::vector <Base> vector = {a, b}; std::deque <Base> deque = {a, b}; std::list <Base> list = {a, b}; std::array <Base, 2> array = {a, b}; std::forward_list<Base> flist = {a, b}; نستطيع ترتيب البيانات باستخدام تعبير لامدا ضمني (inline). std::sort(std::begin(vector), std::end(vector), [](const Base & a, const Base & b) { return a.variable < b.variable; }); كذلك يمكن أن نمرر كائن لامدا كموازن ونعيد استخدامه أكثر من مرة، انظر: auto compare = [](const Base & a, const Base & b) { return a.variable < b.variable; }; std::sort(std::begin(deque), std::end(deque), compare); std::sort(std::begin(array), std::end(array), compare); list.sort(compare); flist.sort(compare); return 0; } ترتيب المصفوفات المبنية مسبقًا ترتّب خوارزمية sort التسلسلات المُعرّفة بواسطة المُكرِّرات، وهذا كاف لترتيب مصفوفة ما مبنية مسبًا (تُعرَف أيضًا باسم مصفوفات من نمط C). الإصدار ≥ C++ 11 int arr1[] = {36, 24, 42, 60, 59}; // ترتيب الأعداد تصاعديا sort(std::begin(arr1), std::end(arr1)); // ترتيب الأعداد تنازليا sort(std::begin(arr1), std::end(arr1), std::greater < int > ()); قبل الإصدار C++ 11، كان من اللازم حساب نهاية المصفوفة باستخدام حجمها: الإصدار < C++ 11 // استخدام قيمة مباشرة sort(arr1, arr1 + 5); // استخدام تعبير كحل بديل const size_t arr1_size = sizeof(arr1) / sizeof( * arr1); sort(arr1, arr1 + arr1_size); ترتيب تسلسل باستخدام ترتيب مخصّص إن كانت للقيم الموجودة في الحاوية عوامل مُحمّلة تحميلًا زائدًا، فنستخدم std::sort مع كائن دالّي مُخصّص لترتيب عناصر الحاوية تصاعديًا أو تنازليًا: الإصدار ≥ C++ 11 #include <vector> #include <algorithm> #include <functional> std::vector v = {5,1,2,4,3}; // (1,2,3,4,5) الترتيب التصاعدي std::sort(v.begin(), v.end(), std::less < int > ()); // أو std::sort(v.begin(), v.end()); // (5,4,3,2,1) الترتيب التنازلي std::sort(v.begin(), v.end(), std::greater < int > ()); // أو std::sort(v.rbegin(), v.rend()); الإصدار ≥ C++ 14 في الإصدار C++ 14، ليس علينا تمرير وسيط القالب لدوالّ الموازنة، إذ يمكن استنتاج ذلك استنادًا إلى ما تم تمريره: std::sort(v.begin(), v.end(), std::less<>()); // ترتيب تصاعدي std::sort(v.begin(), v.end(), std::greater<>()); // ترتيب تنازلي هذا الدرس جزء من سلسلة دروس عن C++. ترجمة -بتصرّف- للفصل Chapter 67: Sorting من كتاب C++ Notes for Professionals
-
مولد قيم عشوائية حقيقيّة لإنشاء قيم عشوائية حقّا (generate true random) يمكن استخدامها في التشفير، يجب استخدام std::random_device كمُولِّد. #include <iostream> #include <random> int main() { std::random_device crypto_random_generator; std::uniform_int_distribution < int > int_distribution(0, 9); int actual_distribution[10] = {0,0,0,0,0,0,0,0,0,0}; for (int i = 0; i < 10000; i++) { int result = int_distribution(crypto_random_generator); actual_distribution[result]++; } for (int i = 0; i < 10; i++) { std::cout << actual_distribution[i] << " "; } return 0; } تُستخدم std::random_device بنفس طريقة استخدام مولّد القيم العشوائية الزائفة (pseudo random value). ورغم ذلك فيمكن تنفيذ std::random_device انطلاقًا من محرّك أعداد عشوائية زائفة تُحدد وفق التنفيذ في حال لم يتوفّر مصدر غير حتمي (non-deterministic) مثل جهاز خاص بتوليد القيم العشوائية لاستخدامه في التنفيذ. يمكن الكشف عن مثل هذه التنفيذات من خلال الدالة التابعة entropy (التي تعيد 0 في حال كان المولد حتميًّا تمامًا)، لكنّ العديد من المكتبات الشائعة (مثل libstdc++ و LLVM's libc++) تعيد دائمًا القيمة 0، حتى عند استخدام مولّدات عشوائية خارجية عالية الجودة . توليد عدد عشوائي زائف تنشئ مولّدات الأعداد شبه العشوائية قيمًا يمكن تخمينها استنادًا إلى القيم التي تم توليدها سابقًا، هذا يعني أنها حتمية. ولا تستخدم مولّدات الأعداد شبه العشوائية في الحالات التي تستلزم أعدادًا عشوائية حقًّا. #include <iostream> #include <random> int main() { std::default_random_engine pseudo_random_generator; std::uniform_int_distribution < int > int_distribution(0, 9); int actual_distribution[10] = {0,0,0,0,0,0,0,0,0,0}; for (int i = 0; i < 10000; i++) { int result = int_distribution(pseudo_random_generator); actual_distribution[result]++; } for (int i = 0; i <= 9; i++) { std::cout << actual_distribution[i] << " "; } return 0; } تنشئ هذه الشيفرة مولّدَ أعدادٍ عشوائية زائفة، وتوزيعًا يولّد أعدادًا صحيحة في نطاق [0،9] باحتمال متساوي، ويحسب المولّد بعد ذلك عدد مرّات إنشاء كل نتيجة. يحدّد معامل القالب std::uniform_int_distribution<T> نوع العدد الصحيح الذي يجب إنشاؤه. استخدم std::uniform_real_distribution<T> لتوليد الأعداد العشرية (floats) والأعداد العشرية المزدوجة (doubles). استخدام المولد مع عدة توزيعات من الممكن استخدام مولّد الأعداد العشوائية مع عدة توزيعات، ويجب ذلك حقيقة. #include <iostream> #include <random> int main() { std::default_random_engine pseudo_random_generator; std::uniform_int_distribution < int > int_distribution(0, 9); std::uniform_real_distribution < float > float_distribution(0.0, 1.0); std::discrete_distribution rigged_dice({1,1,1,1,1,100}); std::cout << int_distribution(pseudo_random_generator) << std::endl; std::cout << float_distribution(pseudo_random_generator) << std::endl; std::cout << (rigged_dice(pseudo_random_generator) + 1) << std::endl; return 0; } عرّفنا في هذا المثال مولِّدًا واحدًا فقط، ثمّ استخدمناه لإنشاء أعداد عشوائية في ثلاثة توزيعات مختلفة، وسيولّد التوزيع rigged_dice قيمة بين 0 و 5، لكن سيولّد في الأغلبية الساحقة من الحالات 5، لأنّ احتمال إنشاء 5 يساوي 100 / 105. هذا الدرس جزء من سلسلة دروس عن C++. ترجمة -بتصرّف- للفصل Chapter 65: Random number generation من كتاب C++ Notes for Professionals
-
std::next_permutation المثال التالي يبدل تسلسل المجال [first, last] ويحوّله إلى التبديل التالي الأعلى في الترتيب المعجمي (lexicographically higher permutation)، ويمكن تخصيص قاعدة التبديل عبر cmpFun. template < class Iterator > bool next_permutation(Iterator first, Iterator last); template < class Iterator, class Compare > bool next_permutation(Iterator first, Iterator last, Compare cmpFun); المعاملات first - بداية المجال المُراد تبديله (مُضمّن) last - نهاية المجال المراد تبديله (غير مُضمّن) القيمة المعادة تعيد true إن كان التبديل موجودًا، وخلاف ذلك، يُحوّل المجال إلى أصغر تبديل معجمية (lexicographically smallest permutation)، ثم تُعاد القيمة false. التعقيد Complexity التعقيد يساوي O(n)، حيث تمثّل n المسافة من first إلى last. إليك المثال التالي: std::vector< int > v { 1, 2, 3 }; do { for (int i = 0; i < v.size(); i += 1) { std::cout << v[i]; } std::cout << std::endl; } while (std::next_permutation(v.begin(), v.end())); هذا يطبع جميع تقليبات المجال 1،2،3 وفق ترتيب معجمي تصاعدي. الخرج: 123 132 213 231 312 321 std::for_each في الشيفرة أدناه، نطبّق الدالّة f على نتيجة تحصيل كلّ مُكرّر في المجال [first, last) بدءًا من first وحتى last - 1. template < class InputIterator, class Function > Function for_each(InputIterator first, InputIterator last, Function f); المعاملات first, last - المجال الذي ستُطبّق f عليه. f - كائن قابل للاستدعاء يُطبّق على نتيجة تحصيل كل مكرّر في المجال [first, last). القيمة المُعادة تعاد f إن كان الإصدار أقدم من C++ 11، وإلّا فستُعاد std::move(f). التعقيد تُطبّق f عدد last - first مرّة. مثال الإصدار ≥ C++ 11 std::vector<int> v { 1, 2, 4, 8, 16 }; std::for_each(v.begin(), v.end(), [](int elem) { std::cout << elem << " "; }); تطبيق الدالّة المُعطاة على كل عنصر من المتجه v يؤدي إلى طباعة هذا العنصر في مجرى الخرج stdout. std::accumulate هذه الخوارزمية مُعرّّفة في الترويسة template < class InputIterator, class T > T accumulate(InputIterator first, InputIterator last, T init); // (1) template < class InputIterator, class T, class BinaryOperation > T accumulate(InputIterator first, InputIterator last, T init, BinaryOperation f); // (2) تُجرى std::accumulate عملية الطي (fold operation) باستخدام الدالّة f على المجال [first, last) بدءا من init كقيمة تراكمية (accumulator value)، وهذا يكافئ: T acc = init; for (auto it = first; first != last; ++it) acc = f(acc, * it); return acc; في الإصدار (1)، يُستخدم operator+ بدلًا من f، لذا فإنّ مراكمة قيمِ حاويةٍ يعادل مجموع عناصر تلك الحاوية. المعاملات first, last - المجال الذي ستُطبّق f عليه. init - القيمة الأولية للتراكم. f - دالّة الطي الثنائية (binary folding function). القيمة المُعادة القيمة المتراكمة الناتجة عن التطبيق المتتابع للدالّة f. التعقيد التعقيد يساوي O(n × k)، و n هنا يمثّل المسافة من first إلى last، فيما تمثّل O(k) تعقيد الدالّة f. انظر المثال البسيط التالي عن مُراكمة الجَمْع: std::vector<int> v { 2, 3, 4 }; auto sum = std::accumulate(v.begin(), v.end(), 1); std::cout << sum << std::endl; الناتج سيكون: 10 تحويل الأرقام (digits) إلى عدد (number): الإصدار<C++ 11 class Converter { public: int operator()(int a, int d) const { return a * 10 + d; } }; ثمّ: const int ds[3] = {1, 2, 3}; int n = std::accumulate(ds, ds + 3, 0, Converter()); std::cout << n << std::endl; الإصدار ≥ C++ 11 const std::vector<int> ds = {1, 2, 3}; int n = std::accumulate(ds.begin(), ds.end(), 0, [](int a, int d) { return a * 10 + d; }); std::cout << n << std::endl; الناتج سيكون: 123 std::find تساعد std::find على العثور على أوّل ظهور لعنصر val داخل مجال [first, last). template < class InputIterator, class T > InputIterator find (InputIterator first, InputIterator last, const T& val); المعاملات first : يمثل مُكرّرًا يشير إلى بداية المجال. last يمثل مُكرّرًا يشير إلى نهاية المجال. val يمثل القيمة المبحوث عنها داخل المجال. القيمة المعادة يُعاد مكرِّر يشير إلى أوّل عنصر في المجال يساوي (==) val، أما إن لم يكن هناك عنصر يحقّق ذلك، فسيُعاد مكرّر يشير إلى last. انظر المثال التالي: #include <vector> #include <algorithm> #include <iostream> using namespace std; int main(int argc, const char * argv[]) { سننشئ متجهًا هنا: vector<int> intVec {4, 6, 8, 9, 10, 30, 55,100, 45, 2, 4, 7, 9, 43, 48}; ثم نعرِّف المكرِّرات: vector < int > ::iterator itr_9; vector < int > ::iterator itr_43; vector < int > ::iterator itr_50; والآن نستدعي find: itr_9 = find(intVec.begin(), intVec.end(), 9); //occurs twice itr_43 = find(intVec.begin(), intVec.end(), 43); //occurs once // قيمة غير موجودة في المتجهة itr_50 = find(intVec.begin(), intVec.end(), 50); //does not occur cout << "first occurrence of: " << * itr_9 << endl; cout << "only occurrence of: " << * itr_43 << endl; نستطيع إثبات أن itr_9 تشير إلى الظهور الأول لـ 9 عبر فحص القيمة التي تلي 9، والتي يجب أن تكون 10 وليس 43، نتابع: cout << "element after first 9: " << * (itr_9 + 1) << ends; وسنلقي نظرة على العنصر الموجود قبل النهاية لتجنب تحصيل ()intVec.end: cout << "last element: " << * (itr_50 - 1) << endl; return 0; } يكون ناتج ذلك كله: first occurrence of: 9 only occurrence of: 43 element after first 9: 10 last element: 48 std::min_element تُستخدم std::min_element لإيجاد أصغر عنصر في مجال معيّن. template < class ForwardIterator > ForwardIterator min_element(ForwardIterator first, ForwardIterator last); template < class ForwardIterator, class Compare > ForwardIterator min_element(ForwardIterator first, ForwardIterator last, Compare comp); المعاملات first - مكرّر يشير إلى بداية المجال last - مكرّر يشير إلى نهاية المجال comp - مؤشّر دالّة أو كائن دالّة يأخذ وسيطين ويعيد إما true أو false موضحًا إذا كان الوسيط الأول أصغر من الوسيط الثاني. يُشتَرَط ألًا تعدّل هذه الدالّة المُدخلاتِ. القيمة المعادة يُعاد مكرّر إلى أصغر عنصر في المجال. التعقيد التعقيد يساوي O(1) - n، حيث يمثل n عدد العناصر المُقارنَة. انظر المثال التالي: #include <iostream> #include <algorithm> #include <vector> #include <utility> // make_pair لاستخدام using namespace std; // دالّة تقارن زوجين bool pairLessThanFunction(const pair < string, int > & p1, const pair < string, int > & p2) { return p1.second < p2.second; } int main(int argc, const char * argv[]) { vector<int> intVec {30,200,167,56,75,94,10,73,52,6,39,43}; vector> pairVector = {make_pair("y", 25), make_pair("b", 2), make_pair("z", 26), make_pair("e", 5) }; // < العامل الافتراضي هو auto minInt = min_element(intVec.begin(), intVec.end()); // pairLessThanFunction استخدام auto minPairFunction = min_element(pairVector.begin(), pairVector.end(), pairLessThanFunction); // intVector اطبع أصغر قيمة في cout << "min int from default: " << * minInt << endl; // pairVector اطبع أصغر قيمة في cout << "min pair from PairLessThanFunction: " << ( * minPairFunction).second << endl; return 0; } الناتج سيكون: min int from default: 6 min pair from PairLessThanFunction: 2 std::find_if تُستخدم std::find_if لإيجاد أوّل عنصر في المجال يحقّق دالّة شرطية pred. template < class InputIterator, class UnaryPredicate > InputIterator find_if(InputIterator first, InputIterator last, UnaryPredicate pred); المُعاملات first => مكرّر يشير إلى بداية المجال last => مكرّر يشير إلى نهاية المجال pred => دالّة شرطية - predicate - (تعيد إمّا true أو false) القيمة المعادة يُعاد مكرّر يشير إلى أول عنصر في المجال يحقّق الدالّة الشرطية، وسيُعاد مكرّر إلى last في حال لم يكن هناك أيّ عنصر يحقّق ذلك. انظر المثال التالي: #include <iostream> #include <vector> #include <algorithm> using namespace std; /* عرِّف بعض الدوالّ الشرطية */ // يساوي 10 x إن كان true إعادة bool multOf10(int x) { return x % 10 == 0; } // إن كان العنصر أكبر من القيمة الممرّرة true إعادة class Greater { int _than; public: Greater(int th): _than(th) { } bool operator()(int data) const { return data > _than; } }; int main() { vector myvec {2, 5, 6, 10, 56, 7, 48, 89, 850, 7, 456}; // lambda مع دالّة vector < int > ::iterator gt10 = find_if(myvec.begin(), myvec.end(), [](int x) { return x > 10; }); // >= // مع مؤشّر دالّة vector < int > ::iterator pow10 = find_if(myvec.begin(), myvec.end(), multOf10); // مع كائن دالّي vector < int > ::iterator gt5 = find_if(myvec.begin(), myvec.end(), Greater(5)); // غير موجود vector < int > ::iterator nf = find_if(myvec.begin(), myvec.end(), Greater(1000)); // nf points to myvec.end(); // myvec.end() التحقّق مما إذا كان المؤشّر يشير إلى if (nf != myvec.end()) { cout << "nf points to: " << * nf << endl; } else { cout << "item not found" << endl; } cout << "First item > 10: " << * gt10 << endl; cout << "First Item n * 10: " << * pow10 << endl; cout << "First Item > 5: " << * gt5 << endl; return 0; } الناتج سيكون: item not found First item > 10: 56 First Item n * 10: 10 First Item > 5: 6 استخدام std::nth_element لإيجاد الوسيط تأخذ خوارزمية std::nth_element ثلاثة مُكرِّرات: مكرّر يشير إلى البداية، وآخر يشير إلى الموضع n، وآخر يشير إلى النهاية. وبمجرد عودة الدالّة، سيكون العنصر رقم n (في الترتيب وليس في الموضع) هو أصغر n عنصر. تحتوي الدالّة على تحميلات زائدة أكثر تفصيلًا، على سبيل المثال، بعضها تأخذ كائنات دالّية للموازنة. ملاحظة: هذه الدالّة فعّالة للغاية - إذ أنّ تعقيدها خطّي. في المثال التالي، سنُعرّف الوسيط median، وهو القيمة التي تفصل النصف الأعلى من التسلسل عن النصف الأصغر بحيث يتساوى على طرفيه عدد القيم بعد ترتيبها تصاعديًا، أي أنه العنصر الأوسط في الترتيب. للتّبسيط، سنُعرّف وسيط تسلسل مكوّن من n عنصر على أنّه العنصر الموجود في الموضع ⌈ n / 2 ⌉ حسب الترتيب. مثلًا، سيكون وسيط تسلسل طوله 5 هو العنصر الأصغر الثالث، وكذلك وسيط تسلسل بطول 6 عناصر. لاستخدام هذه الدالّة لإيجاد الوسيط، يمكننا استخدام ما يلي، لنبدأ بــ: std::vector v{5, 1, 2, 3, 4}; std::vector < int > ::iterator b = v.begin(); std::vector < int > ::iterator e = v.end(); std::vector < int > ::iterator med = b; std::advance(med, v.size() / 2); // الوسيط موجود في الموضع الثاني std::nth_element(b, med, e); // v[2] الوسيط الآن هو ولإيجاد نقطة تجزئة من الدرجة p (p-th quantile)، سنغيّر بعض السطور أعلاه: const std::size_t pos = p * std::distance(b, e); std::advance(nth, pos); ستكون نقطة التجزئة في الموضع pos. std::count تحسُب std::count عدد العناصر التي تساوي قيمةً معيّنة val: template < class InputIterator, class T > typename iterator_traits < InputIterator > ::difference_type count(InputIterator first, InputIterator last, const T & val); المعاملات first - مكرّر يشير إلى بداية المجال. *last => مكرّر يشير إلى نهاية المجال. val - القيمة المراد حساب تكرار حدوثها في المجال. القيمة المعادة عدد العناصر التي تساوي (==) val في المجال. انظر المثال التالي: #include <vector> #include <algorithm> #include <iostream> using namespace std; int main(int argc, const char * argv[]) { // إنشاء متجهة vector intVec{4,6,8,9,10,30,55,100,45,2,4,7,9,43,48}; //9, 55, 101 حساب مرات حدوث size_t count_9 = count(intVec.begin(), intVec.end(), 9); //occurs twice size_t count_55 = count(intVec.begin(), intVec.end(), 55); //occurs once size_t count_101 = count(intVec.begin(), intVec.end(), 101); //occurs once // اطبع النتيجة cout << "There are " << count_9 << " 9s" << endl; cout << "There is " << count_55 << " 55" << endl; cout << "There is " << count_101 << " 101" << ends; // 4 ابحث عن أول عنصر في المتجهة يساوي vector < int > ::iterator itr_4 = find(intVec.begin(), intVec.end(), 4); // حساب مرات حدوثه في المتجه size_t count_4 = count(itr_4, intVec.end(), * itr_4); // should be 2 cout << "There are " << count_4 << " " << * itr_4 << endl; return 0; } الناتج سيكون: There are 2 9s There is 1 55 There is 0 101 There are 2 4 std::count_if تحسب std::count_if عدد العناصر في المجال التي تحقّق شرطًا معينًا. template < class InputIterator, class UnaryPredicate > typename iterator_traits < InputIterator > ::difference_type count_if(InputIterator first, InputIterator last, UnaryPredicate red); المعاملات first - مكرّر يشير إلى بداية المجال. last - مكرّر يشير إلى نهاية المجال. red - دالّة شرطية (تعيد إما true أو false). القيمة المعادة عدد العناصر في المجال التي تحقّق الشرط. انظر المثال التالي: #include <iostream> #include <vector> #include <algorithm> using namespace std; /* عرِّف بعض الدوال لاستخدامها كشروط */ // فرديا number إن كان true إعادة bool isOdd(int i) { return i % 2 == 1; } // أكبر من قيمة وسيط المنشِئ number إن كان true كائن دالي يعيد provided class Greater { int _than; public: Greater(int th): _than(th) {} bool operator()(int i) { return i > _than; } }; int main(int argc, const char * argv[]) { // أنشئ متجهًا vector myvec = {1,5,8,0,7,6,4,5,2,1,5,0,6,9,7}; // لحساب عدد العناصر الزوجية lambda استخدام دالّة size_t evenCount = count_if(myvec.begin(), myvec.end(), [](int i) { return i % 2 == 0; }); // >= C++11 // استخدام مؤشّر دالّة لحساب عدد الأعداد الفردية في النصف الأول من المتجهة size_t oddCount = count_if(myvec.begin(), myvec.end() - myvec.size() / 2, isOdd); // استخدام كائن دالّي لحساب عدد العناصر الأصغر من 5 size_t greaterCount = count_if(myvec.begin(), myvec.end(), Greater(5)); cout << "vector size: " << myvec.size() << endl; cout << "even numbers: " << evenCount << " found" << endl; cout << "odd numbers: " << oddCount << " found" << endl; cout << "numbers > 5: " << greaterCount << " found" << endl; return 0; } الناتج سيكون: vector size: 15 even numbers: 7 found odd numbers: 4 found numbers > 5: 6 found هذا الدرس جزء من سلسلة دروس عن C++. ترجمة -بتصرّف- للفصل Chapter 62: Standard Library Algorithms من كتاب C++ Notes for Professionals
-
std::optional: القيم الاختيارية تُستخدم القيم الاختيارية (المعروفة أيضًا باسم "أنواع الشّك") لتمثيل نوع قد يكون محتواه موجودًا أو لا، وقد قُدِّمت في C++ 17 على هيئة صنف std::optional. فمثلًا، قد يحتوي كائن من النوع std::optional<int> على قيمة من النوع int، أو قد لا يحتوي على أيّ قيمة. وتُستخدَم القيم الاختيارية إمّا لتمثيل قيمة قد لا تكون موجودة، أو كنوعٍ للقيمة المعادة من دالّة قد تفشل أحيانًا في إعادة نتيجة ذات معنى. استخدام قيمة اختيارية لتمثيل غياب القيمة كانت المؤشّرات التي تحمل القيمة nullptr قبل C++ 17 تمثّل عدم وجود أيّ قيمة، يعد هذا حلاً جيدًا للكائنات الكبيرة التي خُصِّصت ديناميكيًا وتُدار بواسطة مؤشّرات. لكن لا يعمل هذا الحلّ بشكل جيّد مع الأنواع الصغيرة أو الأوّلية (primitive) مثل int، والتي نادرًا ما تُخصّص أو تُدار ديناميكيًا من قبل المؤشّرات، ويوفّر std::optional حلاً مثاليًا لهذه المشكلة الشائعة. في المثال أدناه، عرّفنا البنية Person، والتي تمثل شخصًا، هذا الشخص يمكنه أن يمتلك حيوانًا أليفًا (pet)، لكنّ ذلك ليس ضروريًا. ولكي نخبر المصرّف بأنّ الحقل pet اختياري هنا، فسنُصرّح عنه بواسطة المُغلّف std::optional. #include <iostream> #include <optional> #include <string> struct Animal { std::string name; }; struct Person { std::string name; std::optional < Animal > pet; }; int main() { Person person; person.name = "John"; if (person.pet) { std::cout << person.name << "'s pet's name is " << person.pet - > name << std::endl; } else { std::cout << person.name << " is alone." << std::endl; } } القيم الاختيارية كقيمة معادة انظر المثال التالي: std::optional < float > divide(float a, float b) { if (b != 0. f) return a / b; return {}; } في المثال أعلاه، سنعيد الكسر a/b، ولكن إذا لم يكن الكسر مُعرّفا (إن كان b يساوي 0 مثلًا)، فسَنعيد القيمة الاختيارية الفارغة. هذا مثال أكثر تعقيدًا: template < class Range, class Pred > auto find_if( Range&& r, Pred&& p ) { using std::begin; using std::end; auto b = begin(r), e = end(r); auto r = std::find_if(b, e, p); using iterator = decltype(r); if (r == e) return std::optional < iterator > (); return std::optional < iterator > (r); } template < class Range, class T > auto find( Range&& r, T const& t ) { return find_if( std::forward<Range>(r), [&t](auto&& x){return x==t;} ); } تبحث الدالّة find( some_range, 7 ) في الحاوية أو النطاق some_range عن شيء يساوي العدد 7، وتفعل الدالة find_if عبر دالّة شرطية (predicate)، فتعيد إمّا قيمة اختيارية فارغة إذا لم يُعثر على أيّ شيء يساوي العدد 7، أو عنصرًا اختياريا يحتوي على مُكرّر إلى العنصر في حال كان موجودًا. هذا يتيح لك القيام بما يلي: if (find(vec, 7)) { // code } أو حتى: if (auto oit = find(vec, 7)) { vec.erase( * oit); } دون الحاجة إلى استخدام مُكرّرات begin/end أو إجراء الاختبارات. value_or انظر المثال التالي: void print_name( std::ostream& os, std::optional<std::string> const& name ) { std::cout "Name is: " << name.value_or("<name missing>") << '\n'; } يعيد التابع value_or القيمة المخزّنة في القيمة الاختيارية، أو يعيد الوسيط إذا لم يكن هناك أيّ شيء مُخزّن. يتيح لك هذا إمكانية أخذ القيمة الاختيارية (التي يمكن أن تكون فارغة) وتحديد سلوك افتراضي عندما تكون بحاجة إلى قيمة، وهكذا الطريقة، يمكن ترك تحديد "السلوك الافتراضي" إلى أن تكون هناك حاجة إليه، بدلًا من إنشاء قيمة افتراضية داخل مُحرِّك ما. مقاربات أخرى للقيم الاختيارية هناك العديد من الطرق الأخرى لحلّ المشكلة التي تحلها القيم الاختياريّة std::optional، لكن لا طريقة كاملة من تلك الطرق: القيم الاختيارية مقابل المؤشّر في بعض الحالات، يمكننا تمثيل "اختياريّةِ كائنٍ" عبر توفير مؤشّر يشير إلى كائن موجود أو مؤشّر فارغ nullptr للإشارة إلى فشل العمليّة. ولكنّ استخدام هذه الطريقة يقتصر على الحالات التي تكون فيها الكائنات موجودة بالفعل - بالمقابل، يمكن للقيم الاختيارية optional أن تُستخدَم لإعادة كائنات جديدة دون الحاجة إلى تخصيص الذاكرة. القيم الاختيارية مقابل القيم التنبيهية من المقاربات الشائعة استخدام قيمة خاصّة للإشارة إلى أنّ القيمة لا معنى لها، وقد تكون هذه القيمة مثلًا 0 أو -1 بالنسبة للأعداد الصحيحة، أو nullptr بالنسبة للمؤشّرات. مثلًا، لنفترض أنّ هناك دالّة تحاول العثور على فهرس أول ظهور لحرف في سلسلة نصية، في حال كان الحرف موجودًا في السلسلة النصية، فستعيد فهرس أول ظهور له، أمّا في حال لم يكن موجودًا، فستعيد القيمة -1 للدلالة على أنّ الحرف غير موجود في السلسلة النصية. القيمة -1 تمثل القيمة التنبيهية، لأنها تنبّهنا إلى أمر ما (في هذا المثال، تنبِّهنا إلى عدم وجود الحرف المبحوث عنه في السلسلة النصية). المشكلة في هذه المقاربة أنّها تقلل من مساحة القيم الصالحة (لا يمكنك التمييز بين القيمة 0 الصالحة والقيمة 0 التي لا معنى لها)، وليست كل الأنواع فيها خيار طبيعي للقيم التنبيهية. القيم الاختيارية مقابل الأزواج std::pair,> من المقاربات الشائعة أيضًا توفير زوج يكون أحد عُنصَريه قيمةً بوليانية bool للإشارة إلى كون القيمة ذات معنى أم لا، ويشترط هذا أن يكون نوع القيمة قابلًا للإنشاء افتراضيًا (default-constructible) في حالة حدوث خطأ، وهو أمر غير ممكن في بعض الأنواع، وقد يكون ممكنًا ولكن غير مرغوب بالنسبة لأنواع أخرى. بالمقابل، لا تحتاج القيم الاختيارية optional<T> في حال حدوث خطأ إلى بناء أي شيء. استخدام القيم الاختيارية لتمثيل فشل دالة قبل C++ 17، كانت الدوالّّ تمثِّل الفشلَ عادة بإحدى الطرق التالية: إعادة مؤشّر فارغ. على سبيل المثال، استدعاء دالّة Delegate *App::get_delegate() على نسخة من الصنف App ليس لها مُفوّض (delegate) سيعيد nullptr. يُعدّ هذا حلاً جيدًا للكائنات التي خُصِّصت ديناميكيًا أو الكائنات الكبيرة التي تُدار عبر المُؤشّرات، لكنه ليس حلاً جيدًا بالنسبة للكائنات الصغيرة التي عادةً ما تكون مرصوصة (stack-allocated) وتُمرّر عن طريق النسخ. تحجز قيمة محدّدة من النوع المُعاد للإشارة إلى الفشل. على سبيل المثال، قد يؤدي استدعاء دالّة unsigned shortest_path_distance(Vertex a, Vertex b) على رأسين (vertices) غير مُتصلين إلى إعادة 0 للإشارة إلى هذه الحقيقة. يتم إقران القيمة المُعادة مع قيمة بوليانية bool لتحديد ما إذا كانت القيمة المعادة ذات معنى أم لا. على سبيل المثال، يؤدي استدعاء دالّة std::pair<int, bool> parse(const std::string &str) باستخدام وسيط يحتوي سلسلة نصّية لا تتضمّن عددًا صحيحًا إلى إعادة زوج يضمّ عددًا صحيحا غير محدد وقيمة بوليانية تساوي false. في هذا المثال، يُعطى لزيد حيوانين أليفَين، سوسن وسوسان، ثم تُستدعى الدالّة Person::pet_with_name() لاسترداد الكلب "وافي". ونظرًا لأنّ زيد لا يملك كلبًا باسم "وافي"، فإن الدالّة ستفشل، وستُعاد القيمة std::nullopt بدلاً من ذلك. #include <iostream> #include <optional> #include <string> #include <vector> struct Animal { std::string name; }; struct Person { std::string name; std::vector < Animal > pets; std::optional < Animal > pet_with_name(const std::string & name) { for (const Animal & pet: pets) { if (pet.name == name) { return pet; } } return std::nullopt; } }; int main() { Person john; zaid.name = "زيد"; Animal susan; susan.name = "سوسن"; zaid.pets.push_back(susan); Animal susanne; susanne.name = "سوسان"; zaid.pets.push_back(susanne); std::optional < Animal > dog = john.pet_with_name("وافي"); if (dog) { std::cout << "يملك زيد حيوانًا أليفًا اسمه وافي" << std::endl; } else { std::cout <<”لا يملك زيد حيوانًا أليفًا اسمه وافي" << std::endl; } } std::function: تغليف الكائنات القابلة للاستدعاء يوضح المثال التالي طريقة استخدام std::function: #include <iostream> #include <functional> std:: function < void(int, const std::string & ) > myFuncObj; void theFunc(int i, const std::string & s) { std::cout << s << ": " << i << std::endl; } int main(int argc, char * argv[]) { myFuncObj = theFunc; myFuncObj(10, "hello world"); } استخدام std::function مع std::bind إن احتجت إلى استدعاء دالّة مع تمرير وسائط إليها فإنّ استخدام std::function مع std::bind سيعطيك حلولًا فعّالة للغاية كما هو موضّح أدناه. class A { public: std:: function < void(int, const std::string & ) > m_CbFunc = nullptr; void foo() { if (m_CbFunc) { m_CbFunc(100, "event fired"); } } }; class B { public: B() { auto aFunc = std::bind( & B::eventHandler, this, std::placeholders::_1, std::placeholders::_2); anObjA.m_CbFunc = aFunc; } void eventHandler(int i, const std::string & s) { std::cout << s << ": " << i << std::endl; } void DoSomethingOnA() { anObjA.foo(); } A anObjA; }; int main(int argc, char * argv[]) { B anObjB; anObjB.DoSomethingOnA(); } ربط std::function مع نوع آخر قابل للاستدعاء يوضح المثال التالي كيفية استخدام std::function لاستدعاء دالة من نمط C، ودالة تابعة لصنف، وعامل ()operator، ودالة لامدا. ويتم استدعاء الدالة من خلال الوسائط الصحيحة ووسائط بترتيب مختلف، وكذلك بأنواع وأعداد مختلفة. #include <iostream> #include <functional> #include <iostream> #include <vector> using std::cout; using std::endl; using namespace std::placeholders; // دالّة بسيطة لتُستدعى double foo_fn(int x, float y, double z) { double res = x + y + z; std::cout << "foo_fn called with arguments: " << x << ", " << y << ", " << z << " result is : " << res << std::endl; return res; } // بنية مع دالة تابعة لاستدعائها struct foo_struct { // الدالة المراد استدعاؤها double foo_fn(int x, float y, double z) { double res = x + y + z; std::cout << "foo_struct::foo_fn called with arguments: " << x << ", " << y << ", " << z << " result is : " << res << std::endl; return res; } // هذا التابع له بصمة مختلفة، مع ذلك يمكن استخدامه // لاحظ أنّ ترتيب المعاملات قد تغير double foo_fn_4(int x, double z, float y, long xx) { double res = x + y + z + xx; std::cout << "foo_struct::foo_fn_4 called with arguments: " << x << ", " << z << ", " << y << ", " << xx << " result is : " << res << std::endl; return res; } // جعل الكائن بأكمله قابلا للاستدعاء operator() التحميل الزائد للعامل double operator()(int x, float y, double z) { double res = x + y + z; std::cout << "foo_struct::operator() called with arguments: " << x << ", " << y << ", " << z << " result is : " << res << std::endl; return res; } }; int main(void) { // typedefs using function_type = std:: function < double(int, float, double) > ; // foo_struct نسخة foo_struct fs; // سنخزّن هنا كل الدوالّ المربوطة std::vector < function_type > bindings; // var #1 - يمكن استخدام دالّة بسيطة وحسب function_type var1 = foo_fn; bindings.push_back(var1); // var #2 - يمكنك استخدام تابع function_type var2 = std::bind( & foo_struct::foo_fn, fs, _1, _2, _3); bindings.push_back(var2); // var #3 - يمكنك استخدام تابع مع بصمة مختلفة // لها عدد مختلف من المعاملات ومن أنواع مختلفة foo_fn_4 function_type var3 = std::bind(&foo_struct::foo_fn_4, fs, _1, _3, _2, 0l); bindings.push_back(var3); // var #4 - مُحمَّل تحميلا زائدا operator() يمكنك استخدام كائن ذي معامل function_type var4 = fs; bindings.push_back(var4); // var #5 - lambda يمكنك استخدام دالّة function_type var5 = [](int x, float y, double z) { double res = x + y + z; std::cout << "lambda called with arguments: " << x << ", " << y << ", " << z << " result is : " << res << std::endl; return res; }; bindings.push_back(var5); std::cout << "Test stored functions with arguments: x = 1, y = 2, z = 3" << std::endl; for (auto f: bindings) f(1, 2, 3); } انظر هذا المثال الحي. الناتج: Test stored functions with arguments: x = 1, y = 2, z = 3 foo_fn called with arguments: 1, 2, 3 result is : 6 foo_struct::foo_fn called with arguments: 1, 2, 3 result is : 6 foo_struct::foo_fn_4 called with arguments: 1, 3, 2, 0 result is : 6 foo_struct::operator() called with arguments: 1, 2, 3 result is : 6 lambda called with arguments: 1, 2, 3 result is : 6 تخزين وسائط الدالة في صف std::tuple تحتاج بعض البرامج إلى تخزين الوسائط إلى حين استدعاء بعض الدوالّ في المستقبل. ويوضّح هذا المثال كيفية استدعاء أيّ دالّة باستخدام الوسائط المخزّنة في صفّ std::tuple #include <iostream> #include <functional> #include <tuple> #include <iostream> // دالّة بسيطة لاستدعائها double foo_fn(int x, float y, double z) { double res = x + y + z; std::cout << "foo_fn called. x = " << x << " y = " << y << " z = " << z << " res=" << res; return res; } // مساعِدات من أجل تسريع الصف. template < int... > struct seq {}; template < int N, int...S > struct gens: gens < N - 1, N - 1, S... > {}; template < int...S > struct gens < 0, S... > { typedef seq < S... > type; }; //استدعاء المساعِدات template < typename FN, typename P, int...S > double call_fn_internal(const FN & fn, const P & params, const seq < S... > ) { return fn(std::get < S > (params)...); } // std::tuple استدعاء الدالّة مع الوسائط المُخزنة في template < typename Ret, typename...Args > Ret call_fn(const std:: function < Ret(Args...) > & fn, const std::tuple < Args... > & params) { return call_fn_internal(fn, params, typename gens < sizeof...(Args) > ::type()); } int main(void) { // الوسائط std::tuple < int, float, double > t = std::make_tuple(1, 5, 10); // الدالّة المراد استدعاؤها std:: function < double(int, float, double) > fn = foo_fn; // استدعاء الدالّة مع تمرير الوسائط المُخزّنة إليها call_fn(fn, t); } هذا مثال حي الناتج: foo_fn called. x = 1 y = 5 z = 10 res=16 استخدام std::function مع تعابير لامدا والصنف std::bind #include <iostream> #include <functional> using std::placeholders::_1; // std::bind ستُستخدَم في مثال int stdf_foobar(int x, std:: function < int(int) > moo) { return x + moo(x); // std::function moo استدعاء } int foo (int x) { return 2+x; } int foo_2 (int x, int y) { return 9*x + y; } int main() { int a = 2; /* مؤشّرات الدوالّ */ std::cout << stdf_foobar(a, & foo) << std::endl; // 6 ( 2 + (2+2) ) // stdf_foobar(2, foo) يمكن أن تكون أيضا /* Lambda تعابير */ /* std::function في كائن lambda يمكن تخزين دالّة مُغلقة من تعبير */ std::cout << stdf_foobar(a, [capture_value](int param) -> int { return 7 + capture_value * param; }) << std::endl; // result: 15 == value + (7 * capture_value * value) == 2 + (7 + 3 * 2) /* std::bind تعابير */ /* std::bind يمكن تمرير نتيجة التعبير * مثلا عبر ربط المعاملات باستدعاء مؤشّر دالّة */ int b = stdf_foobar(a, std::bind(foo_2, _1, 3)); std::cout << b << std::endl; // b == 23 == 2 + ( 9*2 + 3 ) int c = stdf_foobar(a, std::bind(foo_2, 5, _1)); std::cout << c << std::endl; // c == 49 == 2 + ( 9*5 + 2 ) return 0; } الحمل الزائد للدوال (function overhead) يمكن أن تتسبب std::function في حِمل زائد كبير لأنّ std::function لها دلالات قيمية (value semantics)، فسيكون من اللازم أن تنسخ أو تنقل ما استُدعِي إليها. ولكن بما أنها تستطيع أخذ كائن قابل للاستدعاء من أيّ نوع، فسيتعينّ عليها في كثير من الأحيان أن تخصّص ذاكرة ديناميكية بذلك. تحتوي بعض تطبيقات function على ما يسمى "تحسين الكائنات الصغيرة" (small object optimization)، وفيها تُخزّن الأنواع الصغيرة مثل مؤشّرات الدوالّ أو مؤشّرات الأعضاء أو الكائنات الدالّية (Functors) ذات الحالة الصغيرة، مباشرة في كائن الدالّة function. لكنّ هذا لن يعمل إلّا إن كان النوع قابلًا للإنشاء النقلي عند الاعتراض (noexcept move constructible). أيضًا، لا يتطلب معيار C++ أن توفّر جميع التطبيقات مثل هذا التحسين. إليك المثال التالي: // ملف الترويسة using MyPredicate = std::function<bool(const MyValue &, const MyValue &)>; void SortMyContainer(MyContainer &C, const MyPredicate &pred); // الملف المصدري void SortMyContainer(MyContainer &C, const MyPredicate &pred) { std::sort(C.begin(), C.end(), pred); } يُعدّ معامل القالب هو الحلّ المفضّل لـ SortMyContainer، ولكن إن افترضنا أنّ هذا غير ممكن أو غير مرغوب فيه لسبب من الأسباب، فلن تحتاج SortMyContainer إلى تخزين pred إلا في إطار استدعائها. ومع ذلك، فقد تُخصّص لـ pred ذاكرةً إن كان الكائن الدّالِّي (functor) المُعطى ذا حجم غير معروف. تخصّص function الذاكرة لأنّها تحتاج إلى شيءٍ لتنسخ أو تنقل إليه، كما أنها تأخذ ملكية الكائن القابل للاستدعاء الذي مُرِّر إليها، لكنّ SortMyContainer لا تحتاج إلى امتلاك المُستدعِي ولكنها تحتاج إلى مرجع إليه وحسب، لذا فلا داعي لاستخدام function هنا. كذلك لا يوجد أي نوع دالّة قياسي يشير حصرًا إلى كائنٍ قابل للاستدعاء، لذلك سيكون عليك أن تنتظر إيجاد حلّ لهذا، أو يمكنك التعايش مع الحمل الزائد. أيضّا، لا تملك function أيّ وسيلة فعّالة للتحكم في الموضع الذي تأتي منه تخصيصات الذاكرة الخاصّة بالكائن. صحيح أنّ لها مُنشئات تأخذ كائن تخصيص allocator، ولكنّ العديد من التقديمات لا تقدّمها بالشكل الصحيح … أو لا تقدما بتاتًا. الإصدار ≥ C++ 17 لم يعُد المنشئ function الذي يأخذ كائن تخصيصٍ allocator جزءًا من النوع. لذلك لا توجد أيّ طريقة لإدارة التخصيص. واعلم أن استدعاء function أبطأ من استدعاء المحتويات مباشرةً. كذلك يجب أن يكون الاستدعاء عبر function غير مباشر لأنّ نُسَخَ function يمكن أن تحتوي كائنًا قابلًا للاستدعاء، ويكافئ الحِمل الزائد الناتج عن استدعاء function الحِمل الزائد الناتج عن استدعاء دالّة وهمية. std::forward_list: القوائم الإدراجية النوع std::forward_list هو حاوية تدعم الإدراج السريع للعناصر من أي مكان فيها وكذلك إزالة تلك العناصر، لكنها لا تدعم الوصول العشوائي السريع. يُنفَّذ النوع std::forward_list كقائمة مرتبطة أحادية (singly-linked list)، وليس لها عمومًا أيّ حِمل زائد (overhead) مقارنة بتطبيقها في C. وتوفّر هذه الحاوية، مقارنة بـ std::list، مساحة تخزين أكثر كفاءة عند غياب الحاجة للتكرار ثنائي الاتجاه (bidirectional iteration). انظر المثال التالي: #include <forward_list> #include <string> #include <iostream> template < typename T > std::ostream & operator << (std::ostream & s, const std::forward_list < T > & v) { s.put('['); char comma[3] = { '\0', ' ', '\0' }; for (const auto & e: v) { s << comma << e; comma[0] = ','; } return s << ']'; } int main() { // c++11 صياغة قائمة المهييء في std::forward_list < std::string > words1 { "the", "frogurt", "is", "also", "cursed" }; std::cout << "words1: " << words1 << '\n'; // words2 == words1 std::forward_list < std::string > words2(words1.begin(), words1.end()); std::cout << "words2: " << words2 << '\n'; // words3 == words1 std::forward_list < std::string > words3(words1); std::cout << "words3: " << words3 << '\n'; // words4 is {"Mo", "Mo", "Mo", "Mo", "Mo"} std::forward_list < std::string > words4(5, "Mo"); std::cout << "words4: " << words4 << '\n'; } الناتج: words1: [the, frogurt, is, also, cursed] words2: [the, frogurt, is, also, cursed] words3: [the, frogurt, is, also, cursed] words4: [Mo, Mo, Mo, Mo, Mo] التوابع إليك قائمة التوابع الخاصة بالنوع std::forward_list: اسم التابع التعريف operator= يعيّن قيمًا إلى الحاوية assign يعيّن قيمًا إلى الحاوية get_allocator يعيد المخصِّص المرتبط به (associated allocator) front يصل إلى العنصر الأول before_begin يعيد مكررًا إلى العنصر قبل البداية cbefore_begin يعيد مكررًا ثابت إلى العنصر قبل البداية begin يعيد مكررًا إلى البداية cbegin يعيد مكررًا ثابت إلى البداية end يعيد مكررًا إلى النهاية cend يعيد مكررًا إلى النهاية empty يتحقق إن كانت الحاوية فارغة max_size يعيد الحد الأقصى للعدد الممكن من العناصر clear يمسح المحتويات insert_after يُدرِج عناصرًا بعد عنصر ما emplace_after ينشئ عنصرًا مكان عنصر آخر erase_after يمحو عنصرًا موجودا بعد عنصر ما push_front يدرج عنصرًا في البداية emplace_front ينشئ عنصرًا في البداية pop_front يزيل العنصر الأول resize يغير عدد العناصر المخزنة swap يبدل المحتويات merge يدمج قائمتين مرتبتين splice_after ينقل عناصر من قائمة أمامية أخرى remove يزيل العناصر التي تحقق شرطا محددا remove_if يزيل العناصر التي تحقق شرطا محددا reverse يعكس ترتيب العناصر unique يزيل العناصر المتساوية المتتالية sort يرتب العناصر table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; } std::pair: الأزواج عوامل الموازنة معامِلات هذه العوامل هي lhs و rhs: operator== - يتحقّق هذا العامل من أنّ عناصر كلا الزوجين lhs و rhs متساويان، وتكون القيمة المُعادة هي true إن كان lhs.first == rhs.first و lhs.second == rhs.second، وإلّا فستكون false. std::pair < int, int > p1 = std::make_pair(1, 2); std::pair < int, int > p2 = std::make_pair(2, 2); if (p1 == p2) std::cout << "equals"; else std::cout << "not equal"; // ستُظهر التعليمة هذا، لأن الزوجين غير متماثلين operator!= - يتحقّق هذا العامل ممّا إذا كان أيّ من عناصر الزوجيين lhs و rhs غير متساويين، وتكون القيمة المُعادة هيtrue إن كان lhs.first != rhs.first أو lhs.second != rhs.second، وإلا فستُعاد القيمة false. operator< - إذا كان lhs.first<rhs.first، فسيُعيد true وإن كان rhs.first<lhs.first فسيُعيد false. وكذلك إن كان lhs.second<rhs.second فسيُعيد true، أما خلاف ذلك سيُعيد false. operator<= - يُعيد !(rhs<lhs) operator> - يعيد rhs<lhs operator>= - يعيد !(lhs<rhs) هذا مثال آخر يستخدم حاويات أزواج، ويستخدم العامل operator< لترتيب الحاوية. #include <iostream> #include <utility> #include <vector> #include <algorithm> #include <string> int main() { std::vector<std::pair<int, std::string>> v = { {2, "baz"}, {2, "bar"}, {1, "foo"} }; std::sort(v.begin(), v.end()); for (const auto & p: v) { std::cout << "(" << p.first << "," << p.second << ") "; // (1,foo) (2,bar) (2,baz) :الناتج } } إنشاء زوج والوصول إلى عناصره تتيح لنا الأزواج أن نعامل كائِنين كما لو كانا كائنًا واحدًا، ويمكن إنشاء الأزواج بسهولة بمساعدة دالّة القالب std::make_pair. وهناك طريقة أخرى، وهي إنشاء زوج وتعيين عنصُريه (first و second) لاحقًا. #include <iostream> #include <utility> int main() { std::pair < int, int > p = std::make_pair(1, 2); // إنشاء الزوج std::cout << p.first << " " << p.second << std::endl; // الوصول إلى العناصر // يمكننا أيضا إنشاء الزوج وتعيين عناصره لاحقا std::pair < int, int > p1; p1.first = 3; p1.second = 4; std::cout << p1.first << " " << p1.second << std::endl; // يمكننا أيضا إنشاء زوج باستخدام منشئ std::pair < int, int > p2 = std::pair < int, int > (5, 6); std::cout << p2.first << " " << p2.second << std::endl; return 0; } std::atomics: الأنواع الذرية كل استنساخ (instantiation) وتخصيص للقالب std::atomic يعرّف نوعًا ذريًا (atomic type)، فإن قامت أحد الخيوط (threads) بالكتابة في كائن ذرّي أثناء قراءة مسلك آخر منه، فإنّ السّلوك سيكون مُعرّفًا بشكل جيد، ولن يحدث أيّ مشكل. إضافة إلى ذلك، قد يؤدّي الدخول إلى الكائنات الذرية إلى تهيئة تزامن بين الخيوط ويطلب دخولًا غير ذرّي للذاكرة (non-atomic memory accesses) كما هو مُعرَّف من قِبل std::memory_order. يمكن استنساخ std::atomic مع أيّ نوع قابل للنسخ (TriviallyCopyable type T. std::atomic)، لكن std::atomic ليست قابلة للنسخ أو النقل. توفّر المكتبة القياسية تخصيصات للقالب std::atomic للأنواع التالية: 1) تعريف تخصيص كامل للنوع البولياني bool، وعُرِّفت قيمة التعريف النوعي (typedef) الخاصة به بحيث يُتعامل معه على أنّه نوع ذرّي std::atomic<T> غير مُخصَّص، فيما عدا أنّه ستكون له مخطط (layout) قياسي، ومنشئ افتراضي أولي، ومدمّرات واضحة، كما سيدعم صيغة التهيئة الإجمالية (aggregate initialization syntax): Typedef التخصيص std::atomic_bool std::atomic<bool> 2) تخصيصات كاملة وتعريفات نوعية typedefs للأنواع العددية الصحيحة، كما يلي: Typedef التخصيص std::atomic_char std::atomic<char> std::atomic_char std::atomic<char> std::atomic_schar std::atomic<signed char> std::atomic_uchar std::atomic<unsigned char> std::atomic_short std::atomic<short> std::atomic_ushort std::atomic<unsigned short> std::atomic_int std::atomic<int> std::atomic_uint std::atomic<unsigned int> std::atomic_long std::atomic<long> std::atomic_ulong std::atomic<unsigned long> std::atomic_llong std::atomic<long long> std::atomic_ullong std::atomic<unsigned long long> std::atomic_char16_t std::atomic<char16_t> std::atomic_char32_t std::atomic<char32_t> std::atomic_wchar_t std::atomic<wchar_t> std::atomic_int8_t std::atomic<std::int8_t> std::atomic_uint8_t std::atomic<std::uint8_t> std::atomic_int16_t std::atomic<std::int16_t> std::atomic_uint16_t std::atomic<std::uint16_t> std::atomic_int32_t std::atomic<std::int32_t> std::atomic_uint32_t std::atomic<std::uint32_t> std::atomic_int64_t std::atomic<std::int64_t> std::atomic_uint64_t std::atomic<std::uint64_t> std::atomic_int_least8_t std::atomic<std::int_least8_t> std::atomic_uint_least8_t std::atomic<std::uint_least8_t> std::atomic_int_least16_t std::atomic<std::int_least16_t> std::atomic_uint_least16_t std::atomic<std::uint_least16_t> std::atomic_int_least32_t std::atomic<std::int_least32_t> std::atomic_uint_least32_t std::atomic<std::uint_least32_t> std::atomic_int_least64_t std::atomic<std::int_least64_t> std::atomic_uint_least64_t std::atomic<std::uint_least64_t> std::atomic_int_fast8_t std::atomic<std::int_fast8_t> std::atomic_uint_fast8_t std::atomic<std::uint_fast8_t> std::atomic_int_fast16_t std::atomic<std::int_fast16_t> std::atomic_uint_fast16_t std::atomic<std::uint_fast16_t> std::atomic_int_fast32_t std::atomic<std::int_fast32_t> std::atomic_uint_fast32_t std::atomic<std::uint_fast32_t> std::atomic_int_fast64_t std::atomic<std::int_fast64_t> std::atomic_uint_fast64_t std::atomic<std::uint_fast64_t> std::atomic_intptr_t std::atomic<std::intptr_t> std::atomic_uintptr_t std::atomic<std::uintptr_t> std::atomic_size_t std::atomic<std::size_t> std::atomic_ptrdiff_t std::atomic<std::ptrdiff_t> std::atomic_intmax_t std::atomic<std::intmax_t> std::atomic_uintmax_t std::atomic<std::uintmax_t> هذا مثال بسيط على استخدام std::atomic_int: #include <iostream> // std::cout #include <atomic> // std::atomic, std::memory_order_relaxed #include <thread> // std::thread std::atomic_int foo(0); void set_foo(int x) { foo.store(x, std::memory_order_relaxed); // تعيين القيمة الذرية } void print_foo() { int x; do { x = foo.load(std::memory_order_relaxed); // الحصول على القيمة الذرّية } while (x == 0); std::cout << "foo: " << x << '\n'; } int main() { std::thread first(print_foo); std::thread second(set_foo, 10); first.join(); //second.join(); return 0; } // foo: 10 std::variant: المتغايرات إنشاء مؤشرات للتوابع الزائفة (Create pseudo-method pointers) يمكنك استخدام كائن متغَاير (Variant) للشطب الخفيف للنوع (light weight type erasure). انظر المثال التالي: template < class F > struct pseudo_method { F f; // C++17 السماح باستنتاج نوع الصنف في pseudo_method(F && fin): f(std::move(fin)) {} // عامل بحث كوينج->* لا بأس بما أنه تابع زائف template < class Variant > // متغاير LHS للتحقق من أنّ SFINAE إضافة اختبار friend decltype(auto) operator->*( Variant&& var, pseudo_method const& method ) { // تعيد تعبير لامدا يعيد توجيه استدعاء دالة ما var->*method // مما يجعلها تبدو كأنها تتصرف كمؤشّر تابع return [&](auto&&...args)->decltype(auto) { // للحصول على نوع المتغاير visit استخدم return std::visit( [&](auto&& self)->decltype(auto) { return method.f( decltype(self)(self), decltype(args)(args)... ); }, std::forward < Var > (var) ); }; } }; يؤدي هذا إلى إنشاء نوع يزيد تحميل العامل operator->* بمتغاير Variant على الجانب الأيسر. في المثال التالي، سنستخدم استنتاج نوع الصنف الخاص بـ C++ 17 من أجل إيجاد وسيط القالب لـ print، يجب أن يكون self هو أول وسيط يأخذه تابع لامدا الزائف، ثم يأخذ بقية الوسائط ثم يستدعي الدالة. pseudo_method print = [](auto&& self, auto&&...args)->decltype(auto) { return decltype(self)(self).print( decltype(args)(args)... ); }; والآن إن كان لدينا نوعان لكل منهما تابع print: struct A { void print(std::ostream & os) const { os << "A"; } }; struct B { void print(std::ostream & os) const { os << "B"; } }; لاحظ أنّهما نوعان غير مترابطان، نستطيع تنفيذ ما يلي: std::variant<A,B> var = A{}; (var->*print)(std::cout); سيتم إرسال الاستدعاء مباشرة إلى A::print(std::cout)، لكن لو هيأنا var باستخدام B{}، فسيتم إرساله إلى B::print(std::cout). وإذا أنشأنا نوعًا جديدًا C … struct C {}; … فسيكون لدينا: std::variant<A,B,C> var = A{}; (var->*print)(std::cout); ستفشل عملية التصريف، لأنه لا يوجد تابع C.print(std::cout). سوف يسمح توسيع الشيفرة أعلاه باكتشاف واستخدام دوال print، ربّما باستخدام if constexpr ضمن التابع الزائف print. هذا مثال حي يستخدم boost::variant بدلاً من std::variant. الاستخدامات الرئيسية للمتغايرات تنشي الشيفرة التالية متغايرًا (اتحادًا موسومًا tagged union) يمكنه تخزين إمّا عدد صحيح (int) وإمّا سلسلة نصيةstring. std::variant< int, std::string > var; يمكننا تخزين أحد هذين النوعين في الكائن المتغاير: var = "hello"s; ويمكننا الوصول إلى مُحتوياته عبر std::visit: // "hello\n" طباعة visit( [](auto&& e) { std::cout << e << '\n'; }, var); عن طريق تمرير دالّّة لامدا متعددة الأشكال أو كائن دالّّة مشابه، وإذا كنا متأكّدين من النوع، فيمكننا الحصول عليه على النحو التالي: auto str = std::get<std::string>(var); ولكن هذا سوف يرفع اعتراضًا إن أخطأنا تقدير النوع. auto* str = std::get_if<std::string>(&var); إن أخطأت التقدير فستُعاد القيمة nullptr. تضمن المُتغايرات عدم تخصيص ذاكرة ديناميكية، باستثناء تلك التي تُخصَّص من قبل أنواعها المُضمّنة، ولا يُخزَّن إلا نوع واحد فقط في المتغاير، ويرافقها في حالات نادرة رفع اعتراضات أثناء الإسناد وغياب إمكانية آمنة للتراجع يمكن أن يصبح المتغاير فارغًا. كما تتيح لك المُتغايرات تخزين قيم من عدّة أنواع في متغيّر واحد بأمان وكفاءة. فهي أساسًا اتحاداتunion ذكية وآمنة. إنشاء متغاير هذا لا يشمل المُخصِّصات (allocators). struct A {}; struct B { B()=default; B(B const&)=default; B(int){}; }; struct C { C()=delete; C(int) {}; C(C const&)=default; }; struct D { D( std::initializer_list<int> ) {}; D(D const&)=default; D()=default; }; std::variant < A, B > var_ab0; // A() يحتوي std::variant < A, B > var_ab1 = 7; // a B(7) يحتوي std::variant < A, B > var_ab2 = var_ab1; // a B(7) يحتوي std::variant < A, B, C > var_abc0 { std::in_place_type < C > , 7 }; // a C(7) يحتوي std::variant < C > var_c0; // C لأجل ctor غير قانوني، لا توجد قيمة افتراضية لـ std::variant<A,D> var_ad0( std::in_place_type<D>, {1,3,3,4} ); // D{1,3,3,4} يحتوي std::variant < A, D > var_ad1(std::in_place_index < 0 > ); // A{} يحتوي std::variant<A,D> var_ad2( std::in_place_index<1>, {1,3,3,4} ); // D{1,3,3,4} يحتوي std::iomanip و std::any std::setprecision عند استخدام std::setprecision في التعبير out << setprecision(n) أو in >> setprecision(n)، فإنّها تضبط معامل الدقة (precision parameter) الخاصّ بمجرى الخرج أو الدخل عند القيمة n. معامل هذه الدالّة يكون عددًا صحيحًا، ويمثل قيمة الدقة الجديدة. انظر المثال التالي: #include <iostream> #include <iomanip> #include <cmath> #include <limits> int main() { const long double pi = std::acos(-1.L); std::cout << "default precision (6): " << pi << '\n' << "std::precision(10): " << std::setprecision(10) << pi << '\n' << "max precision: " << std::setprecision(std::numeric_limits < long double > ::digits10 + 1) << pi << '\n'; } //Output // 3.14159 : في الدقة الافتراضية (6) يكون الناتج // std::precision(10): 3.141592654 // 3.141592653589793239 :الدقة القصوى std::setfill عند استخدام std::setfill في تعبير out << setfill(c)، فإنّها تُضبط محرف الملء (fill character) الخاصّ بمجرى الخرج عند القيمة c. ملاحظة: يمكن الحصول على محرف الملء الحالي عبر الدالّة std::ostream::fill. مثال: #include <iostream> #include <iomanip> int main() { std::cout << "default fill: " << std::setw(10) << 42 << '\n' << "setfill('*'): " << std::setfill('*') << std::setw(10) << 42 << '\n'; } // 42 :الافتراضي // setfill('*'): ********42 std::setiosflags عند استخدام std::setiosflags في التعبير out << setiosflags(mask) أو in >> setiosflags(mask)، فإنّها تضبط كل رايات التنسيق (format flags) الخاصّة بمجرى الخرج أو الدخل كما هو محدّد من قِبل القناع (mask). هذه قائمة بكل رايات std::ios_base::fmtflags: dec: استخدام أساس عشري لدخل وخرج (I / O) الأعداد الصحيحة oct: استخدام أساس ثماني (octal base) لدخل وخرج الأعداد الصحيحة hex: استخدام أساس ست عشري (hexadecimal base) لدخل وخرج الأعداد الصحيحة basefield - dec|oct|hex|0: مفيدة لتقنيع (masking) العمليات left: التعديل الأيسر (إضافة محارف الملْء إلى اليمين) right: التعديل الأيمن (إضافة محارف الملء إلى اليسار) internal: التعديل الداخلي (إضافة محارف الملء إلى نقطة معيّنة في الداخل) adjustfield - left|right|internal : مفيدة لتقنيع العمليات. scientific: توليد الأنواع العددية العشرية باستخدام الصيغة العلمية، أو الصيغة الثمانية (hex notation) في حال اقترنت بـ fixed. fixed: توليد الأنواع العددية العشرية باستخدام الصيغة الثابتة (fixed notation)، أو الصيغة الثمانية (hex notation) في حال اقترنت بـ scientific. *floatfield - scientific|fixed|(scientific|fixed)|0: مفيدة لتقنيع لعمليات boolalpha: إدراج واستخراج نوع منطقي وفق تنسيق أبجدي رقمي showbase: إنشاء سابقة (prefix) تشير إلى الأساس الرقمي لخرج الأعداد الصحيحة، وتتطلّب إشارة إلى العُملة في حال الدخل والخرج الماليّ. showpoint: إنشاء محرفَ الفاصلة العشرية (decimal-point character) دون قيد أو شرط لخرج الأعداد العشرية showpos: توليد المحرف + للأعداد غير السالبة skipws: تخطي المسافات البيضاء الموجودة في البداية قبل عمليات الإدخال unitbuf: نقل (flush) الناتج بعد كل عملية خرج uppercase: استبدال بعض الأحرف الصغيرة بالأحرف الكبيرة المقابلة لها في بعض مخرجات عمليات الإخراج. أمثلة على المعدِّلات: #include <iostream> #include <string> #include<iomanip> int main() { int l_iTemp = 47; std::cout << std::resetiosflags(std::ios_base::basefield); std::cout << std::setiosflags(std::ios_base::oct) << l_iTemp << std::endl; // ==> 57 std::cout << std::resetiosflags(std::ios_base::basefield); std::cout << std::setiosflags(std::ios_base::hex) << l_iTemp << std::endl; // ==> 2f std::cout << std::setiosflags(std::ios_base::uppercase) << l_iTemp << std::endl; // ==> 2F std::cout << std::setfill('0') << std::setw(12); std::cout << std::resetiosflags(std::ios_base::uppercase); std::cout << std::setiosflags(std::ios_base::right) << l_iTemp << std::endl; // ==> 00000000002f std::cout << std::resetiosflags(std::ios_base::basefield | std::ios_base::adjustfield); std::cout << std::setfill('.') << std::setw(10); std::cout << std::setiosflags(std::ios_base::left) << l_iTemp << std::endl; // ==> 47........ std::cout << std::resetiosflags(std::ios_base::adjustfield) << std::setfill('#'); std::cout << std::setiosflags(std::ios_base::internal | std::ios_base::showpos); std::cout << std::setw(10) << l_iTemp << std::endl; // ==> +#######47 double l_dTemp = -1.2; double pi = 3.14159265359; std::cout << pi << " " << l_dTemp << std::endl; // ==> +3.14159 -1.2 std::cout << std::setiosflags(std::ios_base::showpoint) << l_dTemp << std::endl; // ==> -1.20000 std::cout << setiosflags(std::ios_base::scientific) << pi << std::endl; // ==> +3.141593e+00 std::cout << std::resetiosflags(std::ios_base::floatfield); std::cout << setiosflags(std::ios_base::fixed) << pi << std::endl; // ==> +3.141593 bool b = true; std::cout << std::setiosflags(std::ios_base::unitbuf | std::ios_base::boolalpha) << b; // ==> true return 0; } std::setw انظر المثال التالي حيث يطبع السطر الثاني val في أقصى يسار شاشة الخرج، بينما يطبعها السطر الثالث في حقل إخراج طوله 10 بدءًا من النهاية اليمنى للحقل: int val = 10; std::cout << val << std::endl; std::cout << std::setw(10) << val << std::endl; يكون الخرج ما يلي: 10 10 1234567890 (السطر الأخير موجود للمساعدة على رؤية مواضع الأحرف). عندما نحتاج إلى أن يكون الخرج مُنسّقًا بتنسيق معيّن، فقد نحتاج إلى ضبط عرض الحقل، ويمكن القيام بذلك باستخدام std::setw و std::iomanip. توضّح الشيفرة التالية صيغة std::setw: std::setw(int n) يمثّل n في هذا المثال طول حقل الخرج الذي سيُعيَّن. std::any يوضح المثال التالي كيفية استخدام std::any: std::any an_object{ std::string("hello world") }; if (an_object.has_value()) { std::cout << std::any_cast<std::string>(an_object) << '\n'; } try { std::any_cast<int>(an_object); } catch(std::bad_any_cast&) { std::cout << "Wrong type\n"; } std::any_cast<std::string&>(an_object) = "42"; std::cout << std::any_cast<std::string>(an_object) << '\n'; المخرجات الناتجة: hello world Wrong type 42 std::set و std::multiset: المجموعات والمجموعات المتعددة تمثّل المجموعات "set" نوعًا من الحاويات عناصرها مُرتّبة وغير مكرّرة، أمّا المجموعات المتعدّدة multiset، فتشبه المجموعات العادية، لكن العناصر المتعددة تكون لها نفس القيمة. تغيير الترتيب الافتراضي لمجموعة ما لدى الصّنفين set و multiset توابع مقارنة افتراضية، ولكن قد تحتاج أحيانًا في بعض الحالات إلى زيادة تحميلها. فمثلًا، لنفترض أنّنا نخزّن سلاسل نصية في مجموعة ما، ونحن نعلم أن تلك السلاسل تحتوي على قيم رقمية فقط. يكون الترتيب الافتراضي قائمًا على المقارنة الأبجدية للسلاسل النصّية، وعليه فلن يتطابق الترتيب مع الترتيب الرقمي. وإن أردت ترتيبها ترتيبًا عدديًا فستحتاج إلى كائن دالّي (functor) لزيادة تحميل تابع الموازنة: #include <iostream> #include <set> #include <stdlib.h> struct custom_compare final { bool operator()(const std::string & left, const std::string & right) const { int nLeft = atoi(left.c_str()); int nRight = atoi(right.c_str()); return nLeft < nRight; } }; int main() { std::set<std::string> sut({"1", "2", "5", "23", "6", "290"}); std::cout << "### Default sort on std::set<std::string> :" << std::endl; for (auto && data: sut) std::cout << data << std::endl; std::set<std::string, custom_compare> sut_custom({"1", "2", "5", "23", "6", "290"}, custom_compare {}); std::cout << std::endl << "### Custom sort on set :" << std::endl; for (auto && data: sut_custom) std::cout << data << std::endl; auto compare_via_lambda = [](auto &&lhs, auto &&rhs){ return lhs > rhs; }; using set_via_lambda = std::set<std::string, decltype(compare_via_lambda)>; set_via_lambda sut_reverse_via_lambda({"1", "2", "5", "23", "6", "290"}, compare_via_lambda); std::cout << std::endl << "### Lambda sort on set :" << std::endl; for (auto && data: sut_reverse_via_lambda) std::cout << data << std::endl; return 0; } يكون الخرج ما يلي: ### Default sort on std::set<std::string> : 1 2 23 290 5 6 ### Custom sort on set : 1 2 5 6 23 290 ### Lambda sort on set : 6 5 290 23 2 1 في المثال أعلاه، يمكن استخدام ثلاث طرق مختلفة لإضافة عمليات مقارنة إلى المجموعات "std::set"، ولكلّ منها فوائدها. الترتيب الافتراضي يستخدم الترتيب الافتراضي عامل المقارنة الخاصّ بالمفتاح (الوسيط الأول للقالب)، وغالبًا ما يكون المفتاح إعدادًا افتراضيًا مناسبًا للدالّة std::less<T>. وستستخدم هذه الدالة العامل operator< الخاص بالكائن ما لم تكن قد خُصِّصت، هذا مفيد خاصّة عندما تحاول شيفرة أخرى استخدام ترتيب معيّن، إذ يجعل الشيفرة متناسقة. ستؤدي كتابة الشيفرة بهذه الطريقة إلى تسهيل تحديثها عندما تكون تغييرات المفتاح جزءًا من واجهة برمجية (API)، فمثلًا إن كان لدينا صنف يحتوي على عضوين، وسيتغيّر إلى صنف يحتوي 3 أعضاء، فستُحدَّث جميع النُّسخ عبر تحديث operator< الخاص بالصنف. وكما تتوقع، فإن استخدام التصنيف الافتراضي كخيار افتراضي منطقي ومقبول. الترتيب المُخصّص يمكن إضافة ترتيب مُخصّص عبر كائن له عامل مقارنة عندما لا تكون المقارنة الافتراضية مناسبة، كما في المثال أعلاه إذ تشير السلاسل النصيّة إلى أعداد صحيحة. يمكن أيضًا استخدام الترتيب المُخصّص في حال كنت ترغب في مقارنة المؤشّرات (الذكية) استنادًا إلى الكائن الذي تشير إليه، أو في حال كنت تحتاج إلى قيود خاصّة في عملية المقارنة، كمقارنة الأزواج std::pair بقيمة العنصر الأول first فقط. يجب أن تحرص على أن يكون الترتيب مستقرًّا (stable sorting) عند إنشاء عامل مقارنة، أي أنّ نتيجة عامل المقارنة بعد الإدراج يجب ألا تتغيّر، وإلا فهذا يعني أنّ السلوك غير محدّد. لذلك احرص على ألّا يستخدم عامل المقارنة إلّا البيانات الثابتة (الأعضاء، الدوالّ الثابتة، …). وستصادف غالبًا -كما في المثال أعلاه- أصنافًا بدون عوامل مقارنة، وينتج عن هذا منشئاتٌ افتراضية ومنشئاتُ نسخ (copy constructors)، ويسمح لك المُنشئ الافتراضي بحذف النسخة في وقت الإنشاء، كما أنّ منشئ النسخ ضروريّ لأنّ المجموعة تأخذ نسخة من مُعامل المقارنة. الترتيب عبر تعابير لامدا تعابير لامدا هي طريقة مختصرة لكتابة الدوالّ، وتتيح لنا كتابة امل المقارنة في سطور قليلة مما يسهل قراءة الشيفرة الكلّية. ما يعيب استخدام تعابير لامدا هو أنّه سيكون لكلّ واحد منها نوع محدّد في وقت التصريف، لذلك سيكون التعبير decltype(lambda) مختلفًا في كل تُصرَّف نفس وحدة التصريف (ملف cpp) عند وجود أكثر من وحدة تصريف تكون مُدرجة في ملفات الترويسة، ولهذا يوصى باستخدام كائنات الدوالّ كعوامل مقارنة عند استخدامها داخل ملفات الترويسة. سترى هذا النوع من الإنشاء غالبًا عند استخدام مجموعة "std::set" ضمن النطاق المحلي للدالّة. في حين يُفضَّل استعمال كائن دالّة عند استخدامه كوسيط لدالّة أو كعضو في صنف. خيارات الترتيب الأخرى نظرًا لأنّ عامل المقارنة الخاصّ بالمجموعات std::set عبارة عن وسيط قالب، فيمكن استخدام جميع الكائنات القابلة للاستدعاء كعوامل مقارنة، وما الأمثلة أعلاه إلا حالات خاصّة وحسب، ولا توجد قيود على هذه الكائنات القابلة للاستدعاء إلا ما يلي: يجب أن تكون نسخة قابلة للإنشاء النّسخي (copy constructable) ويجب أن تكون قابلة للاستدعاء مع وسيطين من نوع المفتاح نفسه (التحويلات الضمنية مسموح بها رغم عدم استحسانها، لأنها قد تضرّ بالأداء). حذف قيم من مجموعة إذا كنت تريد إفراغ المجموعة أو المجموعة المتعدّدة من كل عناصرها، فيمكنك استخدام clear: std::set < int > sut; sut.insert(10); sut.insert(15); sut.insert(22); sut.insert(3); sut.clear(); // يساوي 0 sut حجم ثم يمكنك استخدام التابع erase الذي يوفّر بعض الوظائف التي تشبه عملية الإدراج: std::set < int > sut; std::set < int > ::iterator it; sut.insert(10); sut.insert(15); sut.insert(22); sut.insert(3); sut.insert(30); sut.insert(33); sut.insert(45); // الحذف البسيط sut.erase(3); // استخدام مكرّر it = sut.find(22); sut.erase(it); // حذف مجال من القيم it = sut.find(33); sut.erase(it, sut.end()); std::cout << std::endl << "Set under test contains:" << std::endl; for (it = sut.begin(); it != sut.end(); ++it) { std::cout << * it << std::endl; } الخرج سيكون: Set under test contains: 10 15 30 تنطبق كلّ هذه التوابع أيضًا على المجموعات المتعدّدة multiset، يرجى ملاحظة أنّه في حال طلبت حذف عنصر من مجموعة متعدّدة multiset وكان ذلك العنصر مُكرَّرًا، فستُحذَف جميع العناصر التي تساوي ذلك العنصر. إدراج قيم في مجموعة هناك ثلاث طرق لإدراج العناصر في المجموعات. إدراج بسيط للقيمة باستخدام التابع insert الذي يعيد زوجًا، ممّا يسمح للمُستدعي بالتحقق مما إذا كان الإدراج قد تمّ أم لا. يمكن الإدراج بإعطاء تلميح عن الموضع الذي ستُدرج فيه القيمة، والهدف من ذلك هو تحسين وقت الإدراج، لكن المشكلة أنّنا لا نعرف دائمًا الموضع الذي يجب أن تُدرج فيه القيمة. انتبه في هذه الحالة لأن طريقة إعطاء التلميح تختلف بحسب إصدارات المصرّفات. أخيرًا، يمكنك إدراج عدة قيم عن طريق إعطاء مؤشّر للبداية (مُضمّن) والنهاية (غير مُضمّن). #include <iostream> #include <set> int main() { std::set < int > sut; std::set < int > ::iterator it; std::pair < std::set < int > ::iterator, bool > ret; // إدراج بسيط sut.insert(7); sut.insert(5); sut.insert(12); ret = sut.insert(23); if (ret.second == true) std::cout << "# 23 has been inserted!" << std::endl; ret = sut.insert(23); // بما أنها مجموعة، والعدد 23 موجودا سلفا فيها، فستفشل عملية الإدراج if (ret.second == false) std::cout << "# 23 already present in set!" << std::endl; // إدراج مع تلميح لتسريع الأداء it = sut.end(); // وما بعده C++11 هذه الحالة محسَّنة في // بالنسبة للإصدارات السابقة، يمكن التأشير إلى العنصر الذي يسبق موضع الإدراج sut.insert(it, 30); // إدراج مجال من القيم std::set < int > sut2; sut2.insert(20); sut2.insert(30); sut2.insert(45); std::set < int > ::iterator itStart = sut2.begin(); std::set < int > ::iterator itEnd = sut2.end(); sut.insert(itStart, itEnd); // يُستثنى المكرر الثاني من الإدراج std::cout << std::endl << "Set under test contains:" << std::endl; for (it = sut.begin(); it != sut.end(); ++it) { std::cout << * it << std::endl; } return 0; } سينتج لنا الخرج التالي: # 23 has been inserted! # 23 already present in set! Set under test contains: 5 7 12 20 23 30 45 إدراج القيم في مجموعة متعددة جميع طرق الإدراج الخاصّة بالمجموعات تنطبق أيضًا على المجموعات المتعدّدة، لكن هناك خيار آخر، وهو تمرير قائمة تهيئة initializer_list: auto il = { 7, 5, 12 }; std::multiset < int > msut; msut.insert(il); البحث عن القيم في المجموعات والمجموعات المتعدّدة هناك عدّة طرق للبحث عن قيمة معيّنة في مجموعة std::set أو مجموعة متعدّدة std::multiset، وللحصول على مُكرِّر يشير إلى موضع أوّل ظهور لمفتاح مُعيّن، يمكن استخدام الدالّة find() التي تعيد end() إذا لم يكن المفتاح موجودًا. std::set < int > sut; sut.insert(10); sut.insert(15); sut.insert(22); sut.insert(3); // 3, 10, 15, 22 auto itS = sut.find(10); // *itS == 10 القيمة موجودة، لذا itS = sut.find(555); // itS == sut.end() لم يُعثَر على القيمة، لذا std::multiset < int > msut; sut.insert(10); sut.insert(15); sut.insert(22); sut.insert(15); sut.insert(3); // 3, 10, 15, 15, 22 auto itMS = msut.find(10); الطريقة الأخرى هي استخدام الدالّة count()، والتي تحسُب عدد القيم المطابقة التي عُثِر عليها في المجموعة أو المجموعة المتعدّدة (في حالة المجموعات، ستكون القيمة المُعادة إمّا 0 أو 1). باستخدام نفس القيم المذكورة أعلاه، سيكون لدينا: int result = sut.count(10); // 1 result = sut.count(555); // 0 result = msut.count(10); // 1 result = msut.count(15); // 2 في حالة المجموعات المتعدّدة، يمكن أن تكون هناك عدّة عناصر لها نفس القيمة، وللحصول على مجالٍ (range) يمثّل تلك العناصر يمكن استخدام الدالّة equal_range() التي تُعيد زوجًا يتألّف من مُكرّر الحدّ الأدنى (مُضمّن) ومُكرّر الحدّ الأعلى (غير مضمَن) على التوالي. وإذا لم يكن المفتاح موجودًا فسيشير كلا المُكرِّران إلى أقرب قيمة عليا وفق تابع المقارنة المُستخدم لترتيب المجموعة المتعدّدة المُعطاة. auto eqr = msut.equal_range(15); auto st = eqr.first; // '15' يشير إلى العنصر الأول auto en = eqr.second; // '22' يشير إلى العنصر eqr = msut.equal_range(9); // '10' يشيران إلى eqr.second و eqr.first كل من std::integer_sequence: تسلسلات الأعداد الصحيحة يمثّل قالب الصنف std::integer_sequence<Type, Values...> سلسلة من القيم العددية الصحيحة من نوع Type، حيث Type هو أحد أنواع الأعداد الصحيحة المُضمّنة. تُستخدم هذه التسلسلات عند تنفيذ قوالب الأصناف أو الدوالّ التي تحتاج إلى الوصول الموضعي (positional access)، وتحتوي المكتبة القياسية أيضًا على أنواع مصنَعيّة (factory types) تنشئ تسلسلات تصاعدية من الأعداد الصحيحة انطلاقًا من عدد العناصر المُراد. تحويل صفّ std::tuple<T...> إلى معاملات دالّة يمكن استخدام صفّ std::tuple<T...> لتمرير عدّة قيم إلى دالّة، فمثلًا يمكن استخدامه لتخزين سلسلة من المعاملات على شكل صف انتظار (queue)، ويجب تحويل عناصر هذه الصفوف عند معالجتها إلى وسائط استدعاء للدالة. #include <array> #include <iostream> #include <string> include <tuple> #include <utility> // ---------------------------------------------------------------------------- // الدوالّ المراد استدعاؤها void f(int i, std::string const & s) { std::cout << "f(" << i << ", " << s << ")\n"; } void f(int i, double d, std::string const & s) { std::cout << "f(" << i << ", " << d << ", " << s << ")\n"; } void f(char c, int i, double d, std::string const & s) { std::cout << "f(" << c << ", " << i << ", " << d << ", " << s << ")\n"; } void f(int i, int j, int k) { std::cout << "f(" << i << ", " << j << ", " << k << ")\n"; } // ---------------------------------------------------------------------------- // الدالّة الفعلية التي توسّع الصف template < typename Tuple, std::size_t...I > void process(Tuple const & tuple, std::index_sequence < I... > ) { f(std::get < I > (tuple)...); } // الواجهة المراد استدعاؤها، للأسف يجب أن تُرسل إلى دالّة أخرى لاستخلاص سلسلة الفهارس المُنشأة // std::make_index_sequence<N> من template < typename Tuple > void process(Tuple const & tuple) { process(tuple, std::make_index_sequence < std::tuple_size < Tuple > ::value > ()); } // ---------------------------------------------------------------------------- int main() { process(std::make_tuple(1, 3.14, std::string("foo"))); process(std::make_tuple('a', 2, 2.71, std::string("bar"))); process(std::make_pair(3, std::string("pair"))); process(std::array < int, 3 > { 1, 2, 3 }); } طالما كان الصنف يدعم std::get<I>(object) و std::tuple_size<T>::value، فيمكن توسيعه باستخدام الدالّة process() أعلاه، إذ أنّ الدالّة نفسها مستقلّة تمامًا عن عدد الوسائط. إنشاء حزمة مُعاملات مُكوّنة من أعداد صحيحة تُستخدَم std::integer_sequence لتخزين سلسلة من الأعداد الصحيحة التي يمكن تحويلها إلى حُزمة معاملات، وفائدتها الرئيسيّة هو إمكانية إنشاء قوالب الأصناف المصنعيّة التي ستنشئ تلك التسلسلات: #include <iostream> #include <initializer_list> #include <utility> template < typename T, T...I > void print_sequence(std::integer_sequence < T, I... > ) { std::initializer_list < bool > { bool(std::cout << I << ' ')... }; std::cout << '\n'; } template < int Offset, typename T, T...I > void print_offset_sequence(std::integer_sequence < T, I... > ) { print_sequence(std::integer_sequence < T, T(I + Offset)... > ()); } int main() { // تحديد التسلسلات بشكل صريح print_sequence(std::integer_sequence < int, 1, 2, 3 > ()); print_sequence(std::integer_sequence < char, 'f', 'o', 'o' > ()); // توليد التسلسلات print_sequence(std::make_index_sequence < 10 > ()); print_sequence(std::make_integer_sequence < short, 10 > ()); print_offset_sequence < 'A' > (std::make_integer_sequence < char, 26 > ()); } يَستخدم قالب الدّالة print_sequence() قائمة التهيئة std::initializer_list<bool> عند توسيع تسلسل الأعداد الصحيحة لضمان ترتيب التقييم، وتجنّب إنشاء متغيّر [مصفوفة] غير مستخدم. تحويل سلسلة من الفهارس إلى نُسخ من عنصر ما يؤدي توسيع حزمة معاملات من الفهارس في تعبير فاصلة (comma expression) يحمل قيمةً ما، إلى إنشاء نسخة من القيمة المقابلة لكل فهرس. ويرى المُصرِّفان gcc و clang أنّ الفهرس ليس له أيّ تأثير، لذا يطلقان تحذيرًا بشأنه (يمكن إسكات gcc عبر تحويل الفهرس إلى قيمة فارغة void): #include <algorithm> #include <array> #include <iostream> #include <iterator> #include <string> #include <utility> template < typename T, std::size_t...I > std::array < T, sizeof...(I) > make_array(T const & value, std::index_sequence < I... > ) { return std::array < T, sizeof...(I) > { (I, value)... }; } template < int N, typename T > std::array < T, N > make_array(T const & value) { return make_array(value, std::make_index_sequence < N > ()); } int main() { auto array = make_array < 20 > (std::string("value")); std::copy(array.begin(), array.end(), std::ostream_iterator < std::string > (std::cout, " ")); std::cout << "\n"; } هذا الدرس جزء من سلسلة مقالات عن C++. ترجمة -بتصرّف- للفصول 51 وحتى 60 من كتاب C++ Notes for Professionals
-
نبدأ بدايةً بسرد بعض الأمور المتعلقة بالقاموس أو النوع std::map: لاستخدام أحد الصنفين std::map (القواميس) أو std::multimap (القواميس المتعدّدة)، يجب تضمين الترويسة. تُبقي القواميس والقواميس المتعدّدة عناصرها مرتّبة تصاعديًا وفقًا للمفاتيح، وفي حالة القواميس المتعدّدة (std::multimap) فإن القيم التي لها نفس المفتاح لا تُرتّب. الاختلاف الأساسي بين القواميس والقواميس المتعدّدة هو أنّ القواميس لا تسمح بأن تكون هناك أكثر من قيمة واحدة مرتبطة بالمفتاح نفسه، وذلك على خلاف القواميس المتعدّدة. تُقدَّم القواميس كأشجار بحث ثنائية (binary search trees). لذا تستغرق دوالّّ search() و insert() و erase() وقتًا لوغاريتميًا يساوي Θ(log n) في المتوسط. إن أردت عمليات تأخذ وقتًا ثابتًا، فاستخدم std::unordered_map. تعقيد الدالّتين size() و empty() يساوي (Θ(1، إذ يُخزّن عدد العُقَد مؤقتًا لتجنّب المرور عبر الشجرة في كل مرّة تُستدعى فيه هاتان الدالتان. الوصول إلى العناصر تأخذ القواميس أزواجًا قيمة-مفتاح (key, value) كمُدخلات. يوضح المثال التالي كيفية تهيئة std::map: std::map < std::string, int > ranking { std::make_pair("stackoverflow", 2), std::make_pair("docs-beta", 1) }; يمكن إدراج العناصر في القواميس على النحو التالي: ranking["stackoverflow"]=2; ranking["docs-beta"]=1; في المثال أعلاه، إذا كان المفتاح stackoverflow موجودًا من قبل، فستُحدّث قيمته إلى 2. وإن لم يكن موجودًا، فسيُنشأ مدخل جديد. يمكن الوصول إلى عناصر القاموس std::map مباشرةً عن طريق تمرير المفتاح كفهرس: std::cout << ranking[ "stackoverflow" ] << std::endl; لاحظ أنّ استخدام عامل الفهرسة operator[] على القاموس سيؤدّي إلى إدراج قيمة جديدة باستخدام المفتاح الذي تمّ الاستعلام عنه في القاموس. هذا يعني أنّه لا يمكنك استخدامه على القواميس الثابتة const std::map، حتى لو كان المفتاح مخزّنًا سلفًا في القاموس. ولمنع هذا الإدراج، تحقق من وجود العنصر (مثلًا باستخدام find()) أو استخدم at() كما هو موضّح أدناه. الإصدار ≥ C++ 11 يمكن الوصول إلى عناصر القواميس باستخدام التابع at(): std::cout << ranking.at("stackoverflow") << std::endl; لاحظ أنّ التابع at() سيرفع اعتراض std::out_of_range إن لم تحتوي الحاوية على العنصر المطلوب. يمكن الوصول في القواميس والقواميس المتعدّدة إلى العناصر باستخدام المُكرّرات: الإصدار ≥ C++ 11 // begin() مثال على استخدام std::multimap < int, std::string > mmp { std::make_pair(2, "stackoverflow"), std::make_pair(1, "docs-beta"), std::make_pair(2, "stackexchange") }; auto it = mmp.begin(); std::cout << it -> first << " : " << it -> second << std::endl; // "1 : docs-beta" it++; std::cout << it -> first << " : " << it -> second << std::endl; // "2 : stackoverflow" it++; std::cout << it -> first << " : " << it -> second << std::endl; // "2 : stackexchange" // rbegin() مثال على استخدام std::map < int, std::string > mp { std::make_pair(2, "stackoverflow"), std::make_pair(1, "docs-beta"), std::make_pair(2, "stackexchange") }; auto it2 = mp.rbegin(); std::cout << it2 -> first << " : " << it2 -> second << std::endl; // "2 : stackoverflow" it2++; std::cout << it2 -> first << " : " << it2 -> second << std::endl; // "1 : docs-beta" إدراج عناصر في القواميس لا يمكن إدراج عنصر في قاموس إلّا إن كان مفتاحه غير موجود من قبل في القاموس. على سبيل المثال: std::map< std::string, size_t > fruits_count; يمكن إدراج زوج قيمة-مفتاح (key-value) في قاموس عبر التابع insert()، والذي يتطلب زوجًا (pair) كوسيط: fruits_count.insert({"grapes", 20}); fruits_count.insert(make_pair("orange", 30)); fruits_count.insert(pair<std::string, size_t>("banana", 40)); fruits_count.insert(map<std::string, size_t>::value_type("cherry", 50)); تعيد الدالّة insert() زوجًا مؤلّفًا من مُكرّر وقيمة بوليانية bool: إن نجحت عملية الإدراج فإنّ المكرّر يشير إلى العنصر المُدرج حديثًا، وستكون القيمة البوليانية bool المعادة صحيحة (true). إن كان في القاموس مدخل له نفس المفتاح key فستفشل عملية الإدراج. وحينها سيشير المكرّر إلى العنصر الذي له ذلك المفتاح، وستسَاوي bool القيمة false يمكن استخدام الطريقة التالية لدَمج عمليتي الإدراج والبحث معًا: auto success = fruits_count.insert({"grapes", 20}); if (!success.second) { // موجود سلفا في القاموس 'grapes' success.first -> second += 20; // الدخول إلى المكرر لتحديث القيمة } يُستخدم عامل الفهرسة للوصول إلى العناصر الموجودة في القاموس، أو إدراج عناصر جديدة في حال لم تكن موجودة: fruits_count["apple"] = 10; المشكلة في هذا العامل أنه يمنع المستخدم من التحقّق مما إذا كان العنصر موجودًا بالفعل. وفي حال لم يكن العنصر موجودًا، فسيُنشئه العامل std::map::operator[] ضمنيًا، إذ سيهيِّئه باستخدام المُنشئ الافتراضي قبل إعادة كتابته بالقيمة المعطاة.. يمكن استخدام التابع insert() لإضافة عدّة عناصر دفعة واحدة عبر تمرير قائمة من الأزواج، يعيد هذا الإصدار من insert() القيمة الفارغة void: fruits_count.insert({{"apricot", 1}, {"jackfruit", 1}, {"lime", 1}, {"mango", 7}}); يمكن أيضًا استخدام insert() لإضافة عدّة عناصر باستخدام مُكرّرات تشير إلى بداية ونهاية value_type: std::map< std::string, size_t > fruit_list{ {"lemon", 0}, {"olive", 0}, {"plum", 0}}; fruits_count.insert(fruit_list.begin(), fruit_list.end()); مثال: سنضيف عنصرًا يساوي مفتاحه fruit وقيمته 1، وإن المفتاح موجودًا فلن تفعل fruit_count أي شيء. std::map < std::string, size_t > fruits_count; std::string fruit; while (std::cin >> fruit) { auto ret = fruits_count.insert({ fruit, 1 }); if (!ret.second) { // موجود سلفا 'fruit' ++ret.first -> second; // زيادة العداد } } التعقيد الزمني لعملية الإدراج هو O(log n)، ذلك أنّ القواميس تُنفَّذ على هيئة أشجار. الإصدار ≥ C++ 11 يمكن إنشاء زوج (pair) بشكل صريح باستخدام make_pair() و emplace(): std::map< std::string , int > runs; runs.emplace("Babe Ruth", 714); runs.insert(make_pair("Barry Bonds", 762)); يمكننا استخدام emplace_hint() إذا علمنا أين سيُدرج العنصر الجديد من أجل تحديد مكرّر hint. إذا استطعنا إدراج العنصر الجديد قبل hint مباشرة فيمكن إجراء الإدراج في وقت ثابت، وإلا فإنّه سيتصرف مثل التابع emplace(). انظر المثال التالي حيث نطلب الحصول على مكرر يشير إلى العنصر المُدرَج، ويكون العنصر التالي قبل Barry Bonds لذا سيُدرَج قبل it: std::map < std::string, int > runs; auto it = runs.emplace("Barry Bonds", 762); runs.emplace_hint(it, "Babe Ruth", 714); البحث في القواميس والقواميس المتعدّدة هناك عدّة طرق للبحث عن مفتاح في قاموس أو قاموس متعدّد . للحصول على مُكرّر يشير إلى أوّل ظهور لمفتاح معيّن، استخدم الدالّة find() التي تعيد end() إن لم يكن المفتاح موجودًا. std::multimap< int , int > mmp{ {1, 2}, {3, 4}, {6, 5}, {8, 9}, {3, 4}, {6, 7} }; auto it = mmp.find(6); if (it != mmp.end()) std::cout << it -> first << ", " << it -> second << std::endl; // 6, 5 else std::cout << "Value does not exist!" << std::endl; it = mmp.find(66); if (it != mmp.end()) std::cout << it -> first << ", " << it -> second << std::endl; else std::cout << "Value does not exist!" << std::endl; // سيتم تنفيذ هذا السطر هناك طريقة أخرى لمعرفة ما إذا كان قاموس أو قاموس متعدّد يحتوي على مدخَل معيّن، وهي استخدام الدالّة count()، والتي تحسب عدد القيم المرتبطة بمفتاح معين، وبما أن القواميس لا تربط إلا قيمة واحدة فقط بكلّ مفتاح، فإنّ الدالّة count() ستُعيد إمّا 0 -إن كان المفتاح غير موجود-، أو 1 -إن كان موجودًا-، أما بالنسبة إلى القواميس المتعدّدة فيمكن أن تعيد الدالّةُ count() قيمًا أكبر من 1 لأنّها تُجيز ربط عدّة قيم بنفس المفتاح. std::map< int , int > mp{ {1, 2}, {3, 4}, {6, 5}, {8, 9}, {3, 4}, {6, 7} }; if (mp.count(3) > 0) // المفتاح 3 موجود في القاموس std::cout << "The key exists!" << std::endl; // سيتم تنفيذ هذا السطر else std::cout << "The key does not exist!" << std::endl; إن كان كل ما تريده هو التحقق من وجود عنصر ما، فإنّ find أفضل بلا شكّ: فعملُها واضح من اسمها، وبالنسبة للقواميس المتعدّدة multimaps، فإنّها تتوقف بمجرّد العثور على أول عنصر مطابِق. يمكن أن ترتبط عدّة عناصر بنفس المفتاح في حالة القواميس المتعدّدة، وللحصول على تلك العناصر، يمكن استخدام الدالّة equal_range() التي تُعيد زوجًا يحتوي مُكرّر الحدّ الأدنى - iterator lower bound - (مُضمّن) ومُكرّر الحدّ الأعلى - iterator upper bound - (غير مُضمّن) على التوالي. إذا لم يكن المفتاح موجودًا، فسيُشير كلا المُكرّرين إلى end(). auto eqr = mmp.equal_range(6); auto st = eqr.first, en = eqr.second; for (auto it = st; it != en; ++it) { std::cout << it -> first << ", " << it -> second << std::endl; } // 6, 7 تهيئة القواميس والقواميس المتعددة يمكن تهيئة قاموس أو قاموس متعدّد عبر توفير أزواج قيمة-مفتاح مفصولة بفاصلة ,، ويمكن توفير أزواج قيمة-مفتاح وفق الصيغة {key, value} أو إنشاؤها بشكل صريح بواسطة الدالّة std::make_pair(key, value). ولأنّ القواميس لا تسمح بالمفاتيح المكرّرة ولأن عامل الفاصلة (comma operator) يعمل من اليمين إلى اليسار، فسَتتم الكتابة على الزوج الموجود على اليمين باستخدام الزوج ذي المفتاح نفسه والمَوجود على اليسار. std::multimap < int, std::string > mmp { std::make_pair(2, "stackoverflow"), std::make_pair(1, "docs-beta"), std::make_pair(2, "stackexchange") }; // 1 docs-beta // 2 stackoverflow // 2 stackexchange std::map < int, std::string > mp { std::make_pair(2, "stackoverflow"), std::make_pair(1, "docs-beta"), std::make_pair(2, "stackexchange") }; // 1 docs-beta // 2 stackoverflow كلاهما يمكن تهِيئتهما عبر المكرّرات. انظر: من مكرر std::map أو std::multimap: std::multimap< int , int > mmp{ {1, 2}, {3, 4}, {6, 5}, {8, 9}, {6, 8}, {3, 4}, {6, 7} }; // {1, 2}, {3, 4}, {3, 4}, {6, 5}, {6, 8}, {6, 7}, {8, 9} auto it = mmp.begin(); std::advance(it,3); // {6, 5} حرك المؤشر على أول std::map< int, int > mp(it, mmp.end()); // {6, 5}, {8, 9} من مصفوفة الأزواج: std::pair< int, int > arr[10]; arr[0] = {1, 3}; arr[1] = {1, 5}; arr[2] = {2, 5}; arr[3] = {0, 1}; std::map< int, int > mp(arr,arr+4); //{0 , 1}, {1, 3}, {2, 5} من متجه std::vector من الأزواج std::pair: std::vector< std::pair<int, int> > v{ {1, 5}, {5, 1}, {3, 6}, {3, 2} }; std::multimap< int, int > mp(v.begin(), v.end()); // {1, 5}, {3, 6}, {3, 2}, {5, 1} التحقق من عدد العناصر حاوية std::map بها الدالة التابعة empty()، التي تعيد إحدى القيمتين true أو false حسب حالة القاموس هل فارغ أم لا، بينما تعيد size() عدد العناصر المخزّنة في القاموس: std::map<std::string , int> rank {{"facebook.com", 1} ,{"google.com", 2}, {"youtube.com", 3}}; if(!rank.empty()){ std::cout << "Number of elements in the rank map: " << rank.size() << std::endl; } else{ std::cout << "The rank map is empty" << std::endl; } أنواع القواميس القواميس العادية القاموس عبارة عن حاوية ترابطية (associative container) تحتوي على أزواج "قيمة-مفتاح". #include <string> #include <map> std::map<std::string, size_t> fruits_count; في المثال أعلاه، تمثّل std::string نوع المفتاح، وتمثل size_t القيمة. يتصرّف المفتاح كفهرس في القاموس، ويجب أن يكون كل مفتاح فريدًا، ومُرتّبًا. إذا كنت بحاجة إلى قاموس يسمح بأن ترتبط عدّة قيم بنفس المفتاح، فعليك استخدام قاموس متعدّد multimap كما هو موضّح أدناه. إذا لم يكن هناك أيّ ترتيب مرتبط بنوع القيمة، أو إذا كنت تريد تجاوز الترتيب الافتراضي، فيمكنك إنشاء ترتيب خاصّ بك عبر: #include <string> #include <map> #include <cstring> struct StrLess { bool operator()(const std::string & a, const std::string & b) { return strncmp(a.c_str(), b.c_str(), 8) < 0; // قارن الأحرف الثمانية الأولى فقط } } std::map < std::string, size_t, StrLess > fruits_count2; إن أعادت دالّة الموازنة StrLess القيمة false، فيكون المفتَاحان متساويَين، حتى لو كانت محتوياتهما مختلفة. القواميس المتعددة تسمح القواميس المتعدّدة بتخزين عدّة أزواج لها نفس المفتاح في القاموس، وإلا فإنّ واجهتها وطريقة إنشائها ستشبه القواميس العادية. #include <string> #include <map> std::multimap<std::string, size_t> fruits_count; std::multimap<std::string, size_t, StrLess> fruits_count2; قواميس التجزئة (القواميس غير المرتبة) تُخزِّن قواميس التجزئة (hash maps) أزواج "المفتاح-القيمة" بطريقة مشابهة للقواميس العادية، إلا أنها لا ترتّب العناصر وفقًا للمفاتيح، وإنما تُستخدم قيمة التجزئة (hash value) الخاصة بالمفتاح للوصول بسرعة إلى الأزواج قيمة-مفتاح المطلوبة. #include <string> #include <unordered_map> std::unordered_map<std::string, size_t> fruits_count; القواميس غير المرتّبة أسرع عادةً، بيْد أنه لا يمكن توقّع ترتيب عناصرها. على سبيل المثال، يكون التكرار على عناصر قاموس غير مرتب unordered_map بترتيب عشوائي. حذف العناصر إزالة جميع العناصر: std::multimap< int , int > mmp{ {1, 2}, {3, 4}, {6, 5}, {8, 9}, {3, 4}, {6, 7} }; mmp.clear(); // أصبح القاموس المتعدّد فارغا إزالة عنصر ما باستخدام مكرّر: std::multimap< int , int > mmp{ {1, 2}, {3, 4}, {6, 5}, {8, 9}, {3, 4}, {6, 7} }; // {1, 2}, {3, 4}, {3, 4}, {6, 5}, {6, 7}, {8, 9} auto it = mmp.begin(); std::advance(it,3); // {6, 5} إزاحة المؤشّر إلى أول mmp.erase(it); // {1, 2}, {3, 4}, {3, 4}, {6, 7}, {8, 9} إزالة جميع العناصر في نطاق معيّن: std::multimap< int , int > mmp{ {1, 2}, {3, 4}, {6, 5}, {8, 9}, {3, 4}, {6, 7} }; // {1, 2}, {3, 4}, {3, 4}, {6, 5}, {6, 7}, {8, 9} auto it = mmp.begin(); auto it2 = it; it++; // {3, 4} إزاحة المؤشّر إلى أول std::advance(it2,3); // {6, 5} إزاحة المؤشّر الثاني إلى أول mmp.erase(it,it2); // {1, 2}, {6, 5}, {6, 7}, {8, 9} إزالة جميع العناصر التي لها مفتاح معيّن: std::multimap< int , int > mmp{ {1, 2}, {3, 4}, {6, 5}, {8, 9}, {3, 4}, {6, 7} }; // {1, 2}, {3, 4}, {3, 4}, {6, 5}, {6, 7}, {8, 9} mmp.erase(6); // {1, 2}, {3, 4}, {3, 4}, {8, 9} إزالة العناصر التي تحقق شرطًا pred معيّنا: std::map < int, int > m; auto it = m.begin(); while (it != m.end()) { if (pred( *it)) it = m.erase(it); else ++it; } التكرار على القواميس والقواميس المتعددة يمكن التكرار على القواميس والقواميس المتعدّدة بالطرق التالية: std::multimap< int , int > mmp{ {1, 2}, {3, 4}, {6, 5}, {8, 9}, {3, 4}, {6, 7} }; // C++11 حلقة نطاقية - منذ for (const auto & x: mmp) std::cout << x.first << ":" << x.second << std::endl; //أمامي، سيمرُّ على العناصر من الأول حتى الأخير for مكرّر // std::map< int, int >::iterator سيكون من النوع for (auto it = mmp.begin(); it != mmp.end(); ++it) std::cout << it -> first << ":" << it -> second << std::endl; //افعل شيئا ما بالمكرر // عكسي، سيمُرّ على العناصر من الأخير إلى الأول for مكرر // std::map< int, int >::reverse_iterator سيكون من النوع for (auto it = mmp.rbegin(); it != mmp.rend(); ++it) std::cout << it -> first << " " << it -> second << std::endl; // افعل شيئا ما بالمكرّر يُفضّل أثناء التكرار على قاموس أو قاموس متعدّد استخدام auto لتجنّب التحويلات الضمنية غير المفيدة (راجع هذه الإجابة في موقع StackOverFlow لمزيد من التفاصيل). إنشاء قاموس باستخدام الأنواع المُعرَّفة من المستخدم كمفتاح لكي تكون قادرًا على استخدام صنف كمفتاح في القاموس، ينبغي أن يكون المفتاح قابلًا للنسخ copiable والإسناد assignable. يُحدَّد الترتيب داخل القاموس من قِبل الوسيط الثالث المُمرّر إلى القالب (والوسيط الممرّر إلى المُنشئ، في حال استخدامه) ويساوي افتراضيًا std::less<KeyType>، والذي يساوي (افتراضيًا) عامل المقارنة <. لكن لا يلزم استخدام عامل المقارنة الافتراضي إذ يمكنك كتابة معامل مقارنة خاصّ بك (يفضل أن يكون كائنًا داليًا - functional object): struct CmpMyType { bool operator()( MyType const& lhs, MyType const& rhs ) const { // ... } }; في C++، يجب أن يكون شرط "المقارنة" (compare predicate) ترتيبًا ضعيفًا صارمًا (strict weak ordering)، ويجب أن يعيد تعبيرُ compare(X,X) القيمة false مهما كانت قيمة X، فإن أعاد التعبير CmpMyType()(a, b) القيمة true، فإنّ التعبيرَ CmpMyType()(b, a) ينبغي أن يعيد false، وفي حال أعاد كلاهما القيمة false، فسيُعدُّ العنصران a و b متساويين. الترتيب الضعيف الصارم هذا مصطلح رياضيّاتي لتعريف العلاقة بين كائنين. ويعني: في C++، هذا يعني أنه إن كان لديك كائنان من نوع معيّن، فيجب أن يتحقّق الجدول التالي عند مقارنتهما بواسطة المُعامل <. X a; X b; Condition: Test: Result a is equivalent to b: a < b false a is equivalent to b b < a false a is less than b a < b true a is less than b b < a false b is less than a a < b false b is less than a b < a true تعتمد الطريقة التي تعرِّف بها مفهوم التكافؤ كليًا على نوع الكائن. القواميس غير المرتبة القواميس غير المُرتّبة std::unordered_map تشبه القواميس العادية، فهي حاوية ترابطية (Associative Container) تعمل على المفاتيح وقواميسها، فالمفاتيح تحقق التفرد في القاموس لعناصره، بينما تكون قيمة القاموس "map" مجرد محتوى مرتبط بالمفتاح، وأنواع البيانات الخاصة بهذا المفتاح والقاموس يمكن أن تكون أي نوع بيانات معرَّف مسبقًا أو عرَّفه المستخدم. كيفية التصريح والاستخدام يمكن التصريح عن قاموس غير مرتّب من أيّ نوع كما أوضحنا، وسنعرّف قاموسًا غير مرتّب في المثال التالي، يحمل الاسم first باستخدام النوعين string و integer. unordered_map < string, int > first; // إعلان القاموس first["One"] = 1; // [] استخدام العامل first["Two"] = 2; first["Three"] = 3; first["Four"] = 4; first["Five"] = 5; pair < string, int > bar = make_pair("Nine", 9); // إنشاء زوج من نفس النوع first.insert(bar); بعض الدوال الأساسية للتعامل مع القواميس هذه بعض الدوال الأساسية الخاصة بالقواميس غير المرتبة: unordered_map<data_type, data_type> variable_name; // التصريح variable_name[key_value] = mapped_value; // إدراج العناصر variable_name.find(key_value); // إعادة مكرّر إلى قيمة المفتاح variable_name.begin(); // مكرّر إلى العنصر الأول variable_name.end(); // مكرر إلى العنصر الأخير + 1 هذا الدرس جزء من سلسلة مقالات عن C++. ترجمة -بتصرّف- للفصلين Chapter 50: std::map و Chapter 61: Using std::unordered_map من كتاب C++ Notes for Professionals
-
المتّجه هو مصفوفة ديناميكية تخزّن البيانات تلقائيًّا، ويمكن الوصول إلى العناصر الموجودة في المتّجه بنفس طريقة المصفوفات، مع ميزة أنّ المتّجهات يمكن أن يتغير حجمها ديناميكيًا. وبشأن التخزين، تُوضع بيانات المتجه عادة في ذاكرة مُخصّصة ديناميكيًا، ومن ثم فإنها تتطلّب بعض الحمل الزائد (overhead)؛ بالمقابل تستخدم المصفوفات من نمط C ومن الصنف std::array وحدةَ تخزين تلقائية مرتبطة بالموضع المُصرَّح عنه، وهكذا لا يكون هناك أيّ حِمل زائد. الوصول إلى العناصر هناك طريقتان أساسيتان للوصول إلى عناصر المتّجهات: الفهارس المُكرّرات الوصول عبر الفهرس يمكن ذلك إما باستخدام عامل ألفهرسة [] أو الدالة التابعة at()، وكلاهما يعيد مرجعًا إلى العنصر الموجود في الموضع المناسب في المتّجه ما لم يكن نوعه vector<bool>، وعندها سيكون من الممكن قراءته أو تعديله إذا لم يكن المتّجه ثابت. يختلف كل من [] و at() في أنّ العامل [] لا يتحقّق من الحدود بالضرورة، وذلك على خلاف at(). عند محاولة الوصول إلى العناصر الموجودة في الفهارس الخارجة عن الحدود، أي في حال كان index < 0 أو index >= size، فسيحدث سلوك غير محدّد بالنسبة لعامل الفهرسة []، في حين أنّ دالة at() سترفع الاعتراض std::out_of_range. ملاحظة: تستخدم الأمثلة أدناه التهيئة بنمَط C++ 11 لغرض التبسيط، بيد أنّه يمكن استخدام العوامل مع جميع الإصدارات، إلّا إن ذُكر C++ 11 صراحة. الإصدار ≥ C++ 11 std::vector<int> v{ 1, 2, 3 }; // [] استخدام int a = v[1]; // تساوي 2 a v[1] = 4; // { 1, 4, 3 } تحتوي v // at() استخدام int b = v.at(2); // تساوي 3 b v.at(2) = 5; // { 1, 4, 5 } تساوي v int c = v.at(3); // std::out_of_range exception رفع نظرًا لأنّ التابع at() يتحقّق من الحدود ويرفع اعتراضًا في حال تخطّى الفهرس لحدود المتّجه، فهو أبطأ من العامل []، وهذا يجعل [] أنسب لمن أيقن أنّ الفهرس يقع داخل الحدود. وعمومًا فالوصول إلى عناصر المتّجهات يتم في وقت ثابت، أي أنّ الوصول إلى العنصر الأول من المتجه يستغرق نفس الوقت اللّازم للوصول إلى العنصر الثاني أو العنصر الثالث، … . انظر: for (std::size_t i = 0; i < v.size(); ++i) { v[i] = 1; } نعلم في هذا المثال أنّ متغيّرَ الفهرسِ i موجود دائمًا داخل الحدود، لذلك لا داعي للتحقّق ممّا إذا كان i داخل الحدود في كل استدعاء لعامل الفهرسة operator[]. تسمح الدالتان التابعتان front() و back() بالوصول المرجعي (reference access) إلى العنصر الأول والأخير في المتجه على الترتيب، يُستخدَم هذان الموضعان كثيرًا ويمكن أن يكونا أسهل قراءة من العامل []، انظر المثال التالي حيث نشرحه بتفصيل: std::vector<int> v{ 4, 5, 6 }; تكون الصياغة أكثر إسهابًا في الإصدارات التي قبل C++ 11. int a = v.front(); a تساوي 4، و v.front تكافئ [v[0. v.front() = 3; تحتوي v الآن على {3, 5, 6} int b = v.back(); b تساوي 6، و v.back تكافئ [v[v.size() - 1 v.back() = 7; v تحتوي الآن على {3, 5, 7}. ملاحظة: استدعاء front() أو back() على متّجه فارغ سيؤدّي إلى سلوك غير محدّد، لذا تحقّق أنّ الحاوية غير فارغة باستخدام الدالة التابعة empty() قبل استدعاء التابعين front() أو back(). يوضح المثال التالي استخدام empty() للتحقّق من فراغ المتّجه: int main() { std::vector < int > v; int sum(0); for (int i = 1; i <= 10; i++) v.push_back(i); // إنشاء وتهيئة المتّجه while (!v.empty()) // التكرار على المتّجه إلى أن يصبح فارغًا { sum += v.back(); v.pop_back(); // إصدار العنصر مع حذفه من المتّجه } std::cout << "total: " << sum << '\n'; return 0; } ينشئ المثال أعلاه متجهًا يحتوي الأعداد من 1 إلى 10. ثم يُخرج عناصر المتّجها إلى أن يصبح فارغًا (باستخدام empty()) لمنع حدوث سلوك غير محدّد، ثم يُحسَب مجموع الأعداد في المتّجه ويُعرَض للمستخدم. الإصدار C++ 11 يُعيد التابع data() مؤشّرًا إلى الذاكرة الخام (raw memory) التي يستخدمها المتّجه لتخزين عناصره داخليًا، ويُستخدم هذا غالبًا عند تمرير بيانات المتّجه إلى شيفرة قديمة تتوقّع مصفوفة من نمط C. std::vector<int> v{ 1, 2, 3, 4 }; // {1, 2, 3, 4} تحتوي على v int * p = v.data(); // يشير إلى 1 p *p = 4; // {4, 2, 3, 4} تحتوي الآن على v ++p; // يشير إلى 2 p *p = 3; // {4, 3, 3, 4} تحتوي الآن على v p[1] = 2; // {4, 3, 2, 4} تحتوي الآن على v *(p + 2) = 1; // {4, 3, 2, 1} تحتوي الآن على v الإصدار < C++ 11 يمكن محاكاة التابع data() في الإصدارات السابقة لـ C++ 11 عبر استدعاء التابع front() وأخذ عنوان القيمة المُعادة: std::vector<int> v(4); int* ptr = &(v.front()); // &v[0] أو وينجح ذلك لأنّ المتّجهات تخزّن عناصرها دائمًا في مواقع متجاورة في الذاكرة على افتراض أنّ محتويات المتجه لا تعيد تعريف (override) المعامل الأحادي operator&، وإلّا فسيتعيّن عليك إعادة تقديم std::addressof في الإصدارات السابقة للإصدار C++11. كما يُفترض أيضًا ألا يكون المتّجه فارغًا. المكررات تتصرف المُكرّرات (Iterators) بشكل مشابه للمؤشّرات التي تشير إلى عناصر المتّجه: الإصدار ≥ C++ 11 std::vector<int> v{ 4, 5, 6 }; auto it = v.begin(); int i = *it; // يساوي 4 i ++it; i = *it; // يساوي 5 i *it = 6; // { 4, 6, 6 } تحتوي v auto e = v.end(); // v إلى العنصر الموجود بعد e يشير // يمكن استخدامه للتحقّق مما إذا بلغ المكرر نهاية المتجه ++it; it == v.end(); // يشير إلى العنصر الموجود في الموضع 2 it : خطأ ++it; it == v.end(); // true ينصّ المعيار على أنّ المكرّرات std::vector<T> هي مؤشّرات في الواقع من النوع T*، لكنّ معظم المكتبات القياسية لا تطبّق ذلك من أجل تحسين رسائل الخطأ ورصد الشيفرات غير المحمولة، ولتجهيز المُكرّرات بعمليات التحقّق من الأخطاء في عمليات البناء غير المُصدَرة (non-release builds). ثمّ بعد ذلك يمكن حذف الصنف الذي يغلّف المؤشّر الأساسي في عمليات البناء المُصدَرة (release builds) من أجل تحسين الشيفرة. تستطيع استدامة مرجع أو مؤشّر يشير إلى أحد عناصر المتّجه لاستخدامه للوصول غير المباشر، وتظل هذه المراجع أو المؤشّرات التي تشير إلى عناصر المتجه مستقرة كما يظلّ الوصول ممكنًا إلّا في حال أضفت أو أزلت عناصر قبل موضع ذلك العنصر من المتّجه أو فيه، أو في حال تغيير سعة المتّجه. يكافئ هذا قواعد إبطال [المُكرّرات](رابط الفصل 9). الإصدار ≥ C++ 11 std::vector<int> v{ 1, 2, 3 }; // يشير إلى 2 p int* p = v.data() + 1; // ستؤدّي إلى سلوك غير محدّد *p أصبح غير صالح، محاولة الوصول إلى p v.insert(v.begin(), 0); // يشير إلى 1 p p = v.data() + 1; // ستؤدّي إلى سلوك غير محدّد *p أصبح غير صالح، محاولة الوصول إلى p v.reserve(10); // يشير إلى 1 p p = v.data() + 1; // ستؤدي إلى سلوك غير محدّد *p أصبح غير صالح، محاولة الوصول إلى p v.erase(v.begin()); تهيئة متجه يمكن تهيئة المتّجهات بعدة طرق أثناء التصريح عنها: الإصدار ≥ C++ 11 std::vector<int> v{ 1, 2, 3 }; // {1, 2, 3} // std::vector<int> v(3, 6) مختلفة عن std::vector<int> v{ 3, 6 }; // {3, 6} // std::vector<int> v{3, 6} in C++11 مختلفة عن std::vector < int > v(3, 6); // {6, 6, 6} std::vector < int > v(4); // {0, 0, 0, 0} يمكن تهيئة المتجه من حاوية أخرى عبر عدّة طرق: النسخ (من متّجه آخر)، ووهذا ينسخ البيانات من v2: std::vector<int> v(v2); std::vector<int> v = v2; الإصدار ≥ C++ 11 النّقل (من متّجه آخر)، والذي ينقل البيانات من v2: std::vector<int> v(std::move(v2)); std::vector<int> v = std::move(v2); استخدام مُكرّر (نطاقي) لنسخ العناصر إلى v: // من متّجه آخر std::vector < int > v(v2.begin(), v2.begin() + 3); // {v2[0], v2[1], v2[2]} // من مصفوفة int z[] = { 1, 2, 3, 4 }; std::vector < int > v(z, z + 3); // {1, 2, 3} // من قائمة std::list<int> list1{ 1, 2, 3 }; std::vector < int > v(list1.begin(), list1.end()); // {1, 2, 3} الإصدار ≥ C++ 11 النقل عبر مكرّر باستخدام std::make_move_iterator، والذي ينقل العناصر إلى v: // من متّجه آخر std::vector < int > v(std::make_move_iterator(v2.begin()), std::make_move_iterator(v2.end()); // من قائمة std::list<int> list1{ 1, 2, 3 }; std::vector < int > v(std::make_move_iterator(list1.begin()), std::make_move_iterator(list1.end())); يمكن إعادة تهيئة المتجه بعد إنشائه باستخدام التابع assign(): v.assign(4, 100); // {100, 100, 100, 100} v.assign(v2.begin(), v2.begin() + 3); // {v2[0], v2[1], v2[2]} int z[] = { 1, 2, 3, 4 }; v.assign(z + 1, z + 4); // {2, 3, 4} حذف العناصر حذف العنصر الأخير: std::vector<int> v{ 1, 2, 3 }; v.pop_back(); // {1, 2} حذف جميع العناصر: std::vector<int> v{ 1, 2, 3 }; v.clear(); // أصبحت فارغة v حذف العنصر الموجود عند فهرس معيّن: std::vector<int> v{ 1, 2, 3, 4, 5, 6 }; v.erase(v.begin() + 3); // {1, 2, 3, 5, 6} ملاحظة: عند حذف أي عنصر من المتّجه -باستثناء العنصر الأخير- فيجب نسخ جميع العناصر الموجودة بعد العنصر المحذوف أو نقلها لسدّ الفجوة التي خلّفتها عملية الحذف. حذف جميع العناصر الموجودة في نطاق معيّن: std::vector<int> v{ 1, 2, 3, 4, 5, 6 }; v.erase(v.begin() + 1, v.begin() + 5); // {1, 6} أصبحت تساوي v ملاحظة: التوابع المذكورة أعلاه لا تغيّر سعة المتّجه، وإنّما تغير حجمه وحسب (انظر أسفله في فقرة حجم المتّجهات وسعتها). يُستخدم تابع erase -الذي يزيل مجموعة من العناصر- كجزء من مقاربة الحذف والنقل، أي أنّه في البداية ينقل التابع std::remove بعض العناصر إلى نهاية المتّجه، ثم يقطع التابع erase تلك العناصر. هذه العملية مكلّفة نسبيًا إلّا في حال الفهرس الأخير، لأنّه يجب نقل جميع العناصر بعد القطعة المحذوفة إلى مواضع جديدة. أما إن كنت تعمل على تطبيقات تتطلب سرعة كبيرة في إزالة العناصر من الحاويات، فالأفضل استخدام القوائم (lists). حذف العناصر بالقيمة: std::vector<int> v{ 1, 1, 2, 2, 3, 3 }; int value_to_remove = 2; v.erase(std::remove(v.begin(), v.end(), value_to_remove), v.end()); // {1, 1, 3, 3} أصبحت تساوي v حذف العناصر التي تحقّق شرطًا معيّنا. في المثال التالي، تحتاج std::remove_if إلى دالة تأخذ متجهًا كوسيط، وتعيد true إن كان يجب حذف العنصر: bool _predicate(const int& element) { return (element > 3); // ستُحذف العناصر الأكبر من 3 } ... std::vector<int> v{ 1, 2, 3, 4, 5, 6 }; v.erase(std::remove_if(v.begin(), v.end(), _predicate), v.end()); // {1, 2, 3} أصبحت v حذف العناصر عبر تعابير لامدا دون إنشاء دالّة شرطية إضافية: الإصدار ≥ C++ 11 std::vector<int> v{ 1, 2, 3, 4, 5, 6 }; v.erase(std::remove_if(v.begin(), v.end(), [](auto& element){return element > 3;} ), v.end()); حذف العناصر التي تحقّق شرطًا معيّنا في حلقة: std::vector<int> v{ 1, 2, 3, 4, 5, 6 }; std::vector < int > ::iterator it = v.begin(); while (it != v.end()) { if (condition) it = v.erase(it); // v إلى العنصر الموالي في 'it' بعد الحذف، سيشير else ++it; // يشير إلى العنصر الموالي يدويا 'it' جعل } لذا يجب عليك التفكير في استخدام طريقة أخرى عند تكرار الحذف في الحلقات، بما أن من الضروري عدم زيادة it في حال حذف عنصر، فالتابع remove_if أكثر كفاءة وفعالية. حذف العناصر التي تحقّق شرطًا معيّنا في حلقة عكسية: std::vector<int> v{ -1, 0, 1, 2, 3, 4, 5, 6 }; typedef std::vector < int > ::reverse_iterator rev_itr; rev_itr it = v.rbegin(); while (it != v.rend()) { // v بعد الحلقة، لن تبقى إلا الأصفار في int value = * it; if (value) { ++it; it = rev_itr(v.erase(it.base())); } else ++it; } هذه بعض الملاحظات بخصوص الحلقة السابقة: إن كان المكرّر العكسي it يشير إلى عنصر ما، فإنّ التابع base سيعيد المُكرّر العادي (غير العكسي) الذي يشير إلى نفس العنصر. يمحو التابع vector::erase(iterator) العنصر المشار إليه من قبل المكرّر، ويعيد مكرّرًا إلى العنصر الذي يتبع العنصر المُعطى. ينشئ التابع reverse_iterator::reverse_iterator(iterator) مكرّرًا عكسيًّا من مُكرّر ما. هذا يعني أن السطر it = rev_itr(v.erase(it.base())) يقول: أخذ المُكرّر العكسي it، واجعل v تمحو العنصر الذي يشير إليه مُكرّرها العادي؛ ثم خذ المُكرِّر الناتج، وأنشئ مكرّرًا عكسيًّا منه، ثم عيّنه إلى المكرّر العكسي it. لن يؤدي استخدام v.clear() لحذف جميع عناصر المتّجه إلى تحرير الذاكرة، إذ تظل سعة المتّجه دون تغيير. ولتحرير مساحة الذاكرة، استخدم: std::vector<int>().swap(v); الإصدار ≥ C++ 11 يحرّر التابع shrink_to_fit سعة المتّجه غير المستخدم، لكنه لا يضمن تحرير المساحدة بالضرورة، رغم أن أغلب التطبيقات (Implementations) الحالية تضمن ذلك. v.shrink_to_fit(); التكرار على المتجهات تُعرّف v في الأمثلة التالية على النحو التالي: std::vector<int> v; التكرار الأمامي الإصدار ≥ C++ 11 حلقة for نطاقية: for (const auto& value: v) { std::cout << value << "\n"; } استخدام حلقة for مع مكرِّر: for (auto it = std::begin(v); it != std::end(v); ++it) { std::cout << *it << "\n"; } استخدام خوارزمية for_each باستخدام دالة أو صنف كائن دالي (functor): void fun(int const& value) { std::cout << value << "\n"; } std::for_each(std::begin(v), std::end(v), fun); استخدام خوارزمية for_each باستخدام لامدا: std::for_each(std::begin(v), std::end(v), [](int const& value) { std::cout << value << "\n"; }); الإصدار استخدام حلقة for مع مكرِّر:++> std::for_each(std::rbegin(v), std::rend(v), [](auto const& value) { std::cout << *it << "\n"; } استخدام حلقة for مع فهرس: for (std::size_t i = 0; i < v.size(); ++i) { std::cout << v[i] << "\n"; } التكرار في الاتجاه العكسي الإصدار ≥ C++ 14 لا توجد طريقة معيارية لاستخدام حلقة for النطاقية هنا، وإنما لدينا بدائل لها، انظر ما يلي: استخدام خوارزمية for_each، لاحظ استخدام تعبير لامدا للتوضيح، لكن اعلم أنك تستطيع استخدام صنف كائن دالّي (functor): std::for_each(std::rbegin(v), std::rend(v), [](auto const& value) { std::cout << value << "\n"; }); استخدام حلقة for مع مكرر: for (auto rit = std::rbegin(v); rit != std::rend(v); ++rit) { std::cout << *rit << "\n"; } استخدام حلقة for مع فهرس: for (std::size_t i = 0; i < v.size(); ++i) { std::cout << v[v.size() - 1 - i] << "\n"; } رغم أنّه لا يوجد تابع مُضمّن يستخدم حلقة for النطاقية للتكرار العكسي، إلا أننا نستطيع استخدام التابعين begin() و end() للحصول على المُكرّرات، ومن ثم محاكاة ذلك باستخدام كائن مُغلّف (wrapper) للحصول على النتائج التي نريد. الإصدار ≥ C++ 14 template < class C > struct ReverseRange { C c; // يمكن أن تكون مرجعا أو نسخة في حال كانت القيمة الأصلية مؤقتة ReverseRange(C&& cin): c(std::forward < C > (cin)) {} ReverseRange(ReverseRange&& ) = default; ReverseRange& operator = (ReverseRange&& ) = delete; auto begin() const { return std::rbegin(c); } auto end() const { return std::rend(c); } }; template < class C > ReverseRange<C> make_ReverseRange(C&& c) {return {std::forward<C>(c)};} int main() { std::vector<int> v { 1,2,3,4}; for(auto const& value: make_ReverseRange(v)) { std::cout << value << "\n"; } } فرض العناصر الثابتة منذ الإصدار C++ 11، يسمح لك التابعان cbegin() و cend() بالحصول على مُكرّر ثابت (constant iterator) لمُتجه حتى لو كان المتّجه غير ثابت، وتسمح المكرّرات الثابتة بقراءة محتويات المتّجه لكن لا تسمح بتعديلها، وهو أمر مفيد لفرض الثباتية (const correctness). الإصدار ≥ C++ 11 التكرار الأمامي: for (auto pos = v.cbegin(); pos != v.cend(); ++pos) { // type of pos is vector<T>::const_iterator // *pos = 5; // Compile error - can't write via const iterator } التكرار العكسي: for (auto pos = v.crbegin(); pos != v.crend(); ++pos) { // type of pos is vector<T>::const_iterator // *pos = 5; // Compile error - can't write via const iterator } // Functor::operand()(T&) تتوقُّع for_each(v.begin(), v.end(), Functor()); // Functor::operand()(const T&) تتوقُّع for_each(v.cbegin(), v.cend(), Functor()) الإصدار ≥ C++ 17 يوسّع as_const هذا إلى التكرار النطاقي: for (auto const& e : std::as_const(v)) { std::cout << e << '\n'; } هذا سهل التنفيذ في الإصدارات السابقة لـ C++: الإصدار ≥ C++ 14 template < class T > constexpr std::add_const_t<T>& as_const(T& t) noexcept { return t; } ملاحظات حول الكفاءة نظرًا لأنّ الصنف std::vector هو في الأساس صنف يدير مصفوفةً ديناميكية ذات ذاكرة متجاورة، فسَينطبق المبدأ نفسه المُوضّح هنا على متّجهات C++، وسيكون الوصول إلى محتوى المتّجه عبر الفهرس أسهل عند اتّباع مبدأ ترتيب الصفوف الرئيسية (row-major order principle). لا شك أن كل محاولة وصول إلى المتّجه ستؤدّي إلى وضع محتوى إدارتها في ذاكرة التخزين المؤقت أيضًا، لكن الفرق في الأداء عند التكرار على متجه صغير ومهمل إن قورن بمصفوفة خام، انظر هذه المناقشات -بالإنجليزية- عن الأمر للتوضيح وهذه أيضًا). وعليه ينطبق مبدأ الكفاءة نفسه الخاص بالمصفوفات الخام في C على المتّجهات في C++. المتجه vector: الاستثناء الكبير ينصّ المعيار (قسم 23.3.7) على أن يُوفَّر تخصيص للمتّجه vector<bool>، من أجل تحسين إدارة الذاكرة بترشيد تخزين القيم البوليانية bool بحيث يأخذ كل منها بتّة واحدة فقط. ونظرًا لأنّه لا يمكن معالجة البتات في C++، فهذا يعني أنّ العديد من مُتطلبات المتّجهات العادية لن تنطبق على vector<bool>: لا يلزم أن تكون البيانات المُخزّنة متجاورة، لذا لا يمكن تمرير متجه vector<bool> إلى واجهة برمجية للغة C تتوقّع مصفوفة ذات قيم منطقية. لا يعيد التابع at() ولا المعامل operator[] ولا تحصيل المُكرّرات مرجعًا إلى قيمة منطقية، بل تعيد كائنًا وكيلًا (proxy object) يحاكي بشكل تقريبي مرجعًا إلى bool من خلال زيادة تحميل عامل إسناده، فقد لا تكون الشيفرة التالية صالحة بالنسبة للمتّجهات من النوع std::vector<bool> لأنّ تحصيل المُكرّر لا يُعيد مرجعًا: الإصدار ≥ C++ 11 std::vector<bool> v = {true, false}; for (auto &b: v) { } // خطأ وبالمثل، لا يمكن استخدام الدوالّ التي تتوقّع وسيطَا من النوع bool& مع النتيجة المُعادة من قبل operator [] أو at() عند تطبيقها على متّجه منطقيّ vector<bool>، أو مع نتيجة تحصيل المكرّر الخاص بها: void f(bool& b); f(v[0]); // خطأ f(*v.begin()); // خطأ يتعلّق تطبيق المتجه std::vector<bool> بكل من المُصرّف والمعمارية، ويُطبَّق التخصيص الخاصّ بهذا النوع من المتّجهات عن طريق تعبئة n قيمة منطقية في أقل قسم من الذاكرة، وهنا تمثّل n الحجم لأقل ذاكرة يمكن معالجتها بالبِتّْ، والتي تساوي في معظم الأنظمة الحديثة بايت واحد، أو 8 بتّات. هذا يعني أنّ بايت واحد يمكنه تخزين 8 قيم منطقية، وهذا أفضل من التنفيذ التقليدي حيث تُخزّن كل قيمة منطقية في بايت واحد من الذاكرة. ملاحظة: يُظهر المثال التالي القيم المحتملة للبايتات في الصيغة التقليدية مقابل الصيغة المُحسّنة، لن يكون هذا صحيحًا في جميع الأنظمة، لكنّه لغرض توضيح الفرق بين الطريقتين. في الأمثلة أدناه، يُمثّل البايت على هيئة [x، x، x، x، x، x، x، x]. الطريقة التقليدية: تخزين 8 قيم بوليانية في المتجه std::vector<char>: الإصدار ≥ C++ 11 std::vector<char> trad_vect = {true, false, false, false, true, false, true, true}; التمثيل البتّي (Bitwise representation): [0,0,0,0,0,0,0,1], [0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,1], [0,0,0,0,0,0,0,0], [0,0,0,0,0,0,0,1], [0,0,0,0,0,0,0,1] المُخصّص: تخزين 8 قيم بوليانية في المتجه std::vector<bool>: الإصدار ≥ C++ 11 std::vector<bool> optimized_vect = {true, false, false, false, true, false, true, true}; التمثيل البتّي: [1,0,0,0,1,0,1,1] لاحظ في المثال أعلاه، أنّه في النسخة التقليدية للمتجه std::vector<bool>، ستحتل 8 قيم بوليانية 8 بايتات من الذاكرة، بينما في النسخة المُحسّنة من std::vector<bool>، فإنها تحتل بايت واحد فقط من الذاكرة. وهذا تحسّن كبير في استخدام الذاكرة. إن احتجت إلى تمرير متجه vector<bool> إلى واجهة برمجية من نمَط C فقد تحتاج إلى نسخ القيم إلى مصفوفة، أو البحث عن طريقة أفضل لاستخدام الواجهة البرمجية (API) في حال كان الأداء واستخدام الذاكرة غير فعّالين. إدراج العناصر إلحاق عنصر بنهاية المتجه عن طريق النسخ أو النقل: struct Point { double x, y; Point(double x, double y): x(x), y(y) {} }; std::vector < Point > v; Point p(10.0, 2.0); v.push_back(p); // في المتجه p نسخ الإصدار ≥ C++ 11 إلحاق عنصر بنهاية المتجه عبر إنشاء العنصر فوريًّا: تُمرر الوسائط إلى المنشئ الخاص بالنوع المعطى، وينشأ الكائن في المتجه لتجنب النسخ. std::vector<Point> v; v.emplace_back(10.0, 2.0); لاحظ أنّ المتّجهات ليس لها تابع push_front() لأسباب تتعلق بالأداء، وتؤدّي إضافة عنصر إلى بداية المتجه إلى نقل جميع عناصره، وإن أردت إدراج العناصر بشكل متكرر في بداية الحاوية فالأفضل استخدام النوع std::list أو std::deque. إدراج عنصر في أيّ موضع في المتجه: std::vector<int> v{ 1, 2, 3 }; v.insert(v.begin(), 9); // {9, 1, 2, 3} تحتوي الآن على v الإصدار ≥ C++ 11 إدراج عنصر في أيّ موضع من المتجه عن طريق إنشاء العنصر فوريًّا: std::vector<int> v{ 1, 2, 3 }; v.emplace(v.begin()+1, 9); // {1, 9, 2, 3} تحتوي الآن على v إدراج متجه آخر في أيّ موضع في المتجه: std::vector<int> v(4); // 0, 0, 0, 0 تحتوي std::vector<int> v2(2, 10); // 10, 10 تحتوي v.insert(v.begin()+2, v2.begin(), v2.end()); // 0, 0, 10, 10, 0, 0 تحتوي إدراج مصفوفة في أيّ موضع من المتجه: std::vector<int> v(4); // 0, 0, 0, 0 int a [] = {1, 2, 3}; // 1, 2, 3 v.insert(v.begin()+1, a, a+sizeof(a)/sizeof(a[0])); // 0, 1, 2, 3, 0, 0, 0 استخدم التابع reserve() قبل إدراج عدّة عناصر دفعة واحدة إن كان حجم المتّجه الناتج معروفًا مُسبقًا، لتجنّب تكرير عمليّات إعادة تخصيص الذاكرة (انظر فقرة حجم المتّجهات وسعتها أدناه): std::vector < int > v; v.reserve(100); for (int i = 0; i < 100; ++i) v.emplace_back(i); لا تستدع التابع resize() في هذه الحالة، وإلا فستنشئ عن غير قصد متجه يحتوي 200 عنصر، وستوضع القيم التي تريدها في المئة عنصر الأخيرة من المتجه. استخدام المتجهات كمصفوفات من نمط C هناك عدّة أسباب لاستخدام المتّجهات كمصفوفات من نمَط C، كالتوافق مع مكتبات C مثلًا، وهذا ممكن لأنّ عناصر المتجه مُخزّنة في مواضع متجاورة. الإصدار ≥ C++ 11 std::vector<int> v{ 1, 2, 3 }; int* p = v.data(); يجوز تطبيق التابع .data() على المتّجهات الفارغة أيضًا على عكس الحلول السابقة القائمة على معايير C++ (انظر أدناه)، إذ أنّه لن يتسبّب في سلوك غير محدّد في هذه الحالة. كان عليك قبل الإصدار C++ 11 أن تأخذ عنوان العنصر الأوّل في المتجه للحصول على مؤشّر مكافئ، وإذا لم يكن المتجه فارغًا، فإنّ هذين التابعين سيكونان متكافئين: int* p = &v[0]; int* p = &v.front(); // مؤشّر إلى العنصر الأول ملاحظة: إذا كانت المتجه فارغًا، فلا يمكن استخدام v[0] و v.front() لأنّ سلوكهما سيكون غير محدّد. عند تخزين العنوان الأساسي لبيانات المتجه، لاحظ أنّ العديد من العمليات (مثل push_back و resize وغيرها) يمكن أن تغيّر مواضع بيانات المتّجة في الذاكرة، وذلك سيبطل مؤشّرات البيانات السابقة. مثلا: std::vector<int> v; int* p = v.data(); v.resize(42); // صالحا p تغيّر مواضع الذاكرة داخليا، لذا لم يعد إيجاد عنصر في متجه يمكن استخدام الدالّة std::find، المُعرّفة في الترويسة ، لإيجاد عنصر في متجه، ويستخدم التابعُ std::find المعاملoperator== للتحقّق من تساوي العناصر، ويعيد مكرّرًا إلى أول عنصر في النطاق يساوي القيمة المبحوث عنها. إذا لم يُعثر على عنصر يحقّق ذلك، فإنّ التابع std::find سيُعيد std::vector::end (أو std::vector::cend إذا كانت المتجه ثابتًا const). الإصدار < C++11 ++> static const int arr[] = {5, 4, 3, 2, 1}; std::vector<int> v (arr, arr + sizeof(arr) / sizeof(arr[0]) ); std::vector<int>::iterator it = std::find(v.begin(), v.end(), 4); std::vector<int>::difference_type index = std::distance(v.begin(), it); // إلى العنصر الثاني في المتجه `it` يشير std::vector < int > ::iterator missing = std::find(v.begin(), v.end(), 10); std::vector < int > ::difference_type index_missing = std::distance(v.begin(), missing); // تساوي 5 index_missing و v.end() تساوي `missing` الإصدار ≥ C++ 11 std::vector<int> v { 5, 4, 3, 2, 1 }; auto it = std::find(v.begin(), v.end(), 4); auto index = std::distance(v.begin(), it); // إلى العنصر الثاني في المتجه `it` يشير auto missing = std::find(v.begin(), v.end(), 10); auto index_missing = std::distance(v.begin(), missing); // تساوي 5 index_missing و v.end() تساوي `missing` إذا كنت بحاجة إلى إجراء العديد من عمليات البحث في متجه كبير فقد يكون الأفضل أن ترتّب المتجه أولاً، ثم تستخدم خوارزمية البحث الثنائي. لإيجاد أوّل عنصر يحقّق شرطًا ما في متجه، يمكنك استخدام std::find_if. بالإضافة إلى المُعاملين المُمرّرين إلى الدالّة std::find، فإنّ الدالّة std::find_if تقبل معاملًا ثالثًا، وهو عبارة عن كائن دالّة (function object)، أو مؤشّر دالّة يشير إلى دالّة شرطية (predicate function). يجب أن تقبل الدالّة الشرطية عنصرًا من الحاوية كوسيط ثم تعيد قيمة قابلة للتحويل إلى قيمة بوليانية bool لكن دون تعديل الحاوية: الإصدار ≥ C++11 ++> bool isEven(int val) { return (val % 2 == 0); } struct moreThan { moreThan(int limit): _limit(limit) {} bool operator()(int val) { return val > _limit; } int _limit; }; static const int arr[] = {1, 3, 7, 8}; std::vector < int > v(arr, arr + sizeof(arr) / sizeof(arr[0])); std::vector < int > ::iterator it = std::find_if(v.begin(), v.end(), isEven); // إلى 8، أول عنصر زوجي `it`يشير std::vector < int > ::iterator missing = std::find_if(v.begin(), v.end(), moreThan(10)); //لأن كل العناصر أصغر من 10 v.end() يساوي `missing` الإصدار ≥ C++ 11 // إيجاد أول عنصر زوجي std::vector<int> v = {1, 3, 7, 8}; auto it = std::find_if(v.begin(), v.end(), [](int val) { return val % 2 == 0; }); // إلى 8، أول عنصر زوجي `it`يشير auto missing = std::find_if(v.begin(), v.end(), [](int val) { return val > 10; }); // لأن كل العناصر أصغر من 10 v.end() يساوي `missing` ضم المتجهات (Concatenating Vectors) يمكن ضم متجه إلى آخر باستخدام التابع insert(): std::vector<int> a = {0, 1, 2, 3, 4}; std::vector<int> b = {5, 6, 7, 8, 9}; a.insert(a.end(), b.begin(), b.end()); سيفشل هذا الحلّ إذا حاولت ضمّ متجه إلى نفسه لأنّ المواصفات القياسية تنصّ على أنّ المكرّرات المُمرّرة إلى insert() يجب ألّا تكون من نفس نطاق عناصر الكائن المستقبِل. الإصدار < C++ 11 يمكن استخدام الدالتين std::begin() و std::end() بدلاً من استخدام توابع المتجه: a.insert(std::end(a), std::begin(b), std::end(b)); هذا حلّ أكثر شمولية لأنّ b يمكن أن تكون مصفوفة مثلًا، لكن للأسف، فهذا الحل أيضًا لا يسمح لك بضمّ متجه إلى نفسه. إذا لم يكن ترتيب العناصر في المتجه المستقبِل مهمًا، فإنّ معرفة عدد العناصر في كل متجه قد يجنّبك عمليات النسخ غير الضرورية: if (b.size() < a.size()) a.insert(a.end(), b.begin(), b.end()); else b.insert(b.end(), a.begin(), a.end()); استخدام المتجهات كمصفوفات متعدّدة الأبعاد يمكن استخدام المتّجهات كمصفوفات ثنائية الأبعاد (2D matrix) عبر تعريفها كمُتّجهة مؤلّفة من متّجهات. ويمكن تعريف مصفوفة ذات 3 صفوف و 4 أعمدة ذات قيم مساوية للصفر على النحو التالي: std::vector<std::vector<int> > matrix(3, std::vector<int>(4)); الإصدار ≥ C++ 11 صياغة التهيئة باستخدام قوائم التهيئة مشابه للمتّجهات العادية: std::vector<std::vector<int>> matrix = { {0,1,2,3}, {4,5,6,7}, {8,9,10,11} }; يمكن الوصول إلى قيم هذه المتجه بنفس طريقة الوصول إلى عناصر المصفوفات ثنائية الأبعاد int var = matrix[0][2]; التكرار على مصفوفة متعددة الأبعاد يشبة التكرارَ على المتّجهات العادية ولكن مع بعد إضافي. for (int i = 0; i < 3; ++i) { for (int j = 0; j < 4; ++j) { std::cout << matrix[i][j] << std::endl; } } الإصدار ≥ C++ 11 for (auto & row: matrix) { for (auto & col: row) { std::cout << col << std::endl; } } تمثيل مصفوفة بمتّجه مكوّن من متّجهات يُعدُّ طريقة مناسبة لتمثيل المصفوفات متعددة الأبعاد لكنّها ليست الأفضل: إذ أنّها تُشتّت المتّجهات الفردية في الذاكرة كما أنّ هيكلة البيانات لا تساعد على التخزين المؤقت. أيضًا، يجب أن تكون أطوال صفوف المصفوفة المتعدّدة متماثلة، وهذا ليس حال متّجه المتّجهات، كما أنّ المرونة الإضافية قد تسبب الأخطاء. استخدام المتجهات المرتبة لتسريع عمليات البحث توفر الترويسة عددًا من الدوالّ المفيدة لاستخدامها على المتّجهات المُرتبة (sorted vectors)، ولا يمكن العمل مع المتّجهات المُرتّبة إلا إن كانت قيمها المرتّبة قابلة للمقارنة عبر المعامل <. يمكن ترتيب متجه غير مرتب باستخدام الدالّة std::sort(): std::vector<int> v; // بالعناصر v إضافة شيفرة لملء std::sort(v.begin(), v.end()); تتيح المتّجهات المرتّبة بحثًا سريعًا عن العناصر باستخدام الدالّة std::lower_bound()، وعلى عكس std::find() فإن هذا التابع يجري بحثًا ثنائيا (binary search) فعّالًا على المتجه، لكن الجانب السلبي فيه أنه لا يعطي نتائج صحيحة إلّا إن كانت المتّجهات المُدخلة مُرتّبة: // بحث عن أول عنصر يساوي 42 std::vector<int>::iterator it = std::lower_bound(v.begin(), v.end(), 42); if (it != v.end() && *it == 42) { // عثرنا على العنصر } ملاحظة: إذا لم تكن القيمة المطلوبة جزءًا من المتجه، فسيعيد std::lower_bound() مكرّرًا إلى أوّل عنصر يكون أكبر من القيمة المطلوبة، يسمح لنا هذا السلوك بإدراج العنصر الجديد في مكانه الصحيح في المتّجهات المُرتّبة سلفًا: int const new_element = 33; v.insert(std::lower_bound(v.begin(), v.end(), new_element), new_element); إذا أردت إدراج الكثير من العناصر دفعة واحدة، فقد يكون الأفضل استدعاء push_back() عليهم جميعًا، ثم استدعاء std::sort() بعد إدراج جميع العناصر. في هذه الحالة، يمكن أن تعادل التكلفة المتزايدة لترتيب المتجه التكلفةَ المخُفّضة لإدراج عناصر جديدة في نهاية المتجه بدلًا من البحث عن موضعه المناسب. إذا كان المتجه يحتوي على عدّة عناصر بنفس القيمة، فسيعيد std::lower_bound() مُكرِّرًا إلى العنصر الأول من القيمة التي يُبحث عنها. لكن إن أردت إدراج عنصر جديد بعد العنصر الأخير من القيمة التي تبحث عنها، فيجب استخدام الدالّة std::upper_bound() لأنّها تقلّل من تحويل العناصر: v.insert(std::upper_bound(v.begin(), v.end(), new_element), new_element); إذا أردت الحصول على مكرّر الحدّ الأعلى (upper bound iterators) ومكرّر الحدّ الأدنى (lower bound iterators)، فيمكنك استخدام الدالة std::equal_range() للحصول عليهما معًا: std::pair<std::vector<int>::iterator, std::vector<int>::iterator> rg = std::equal_range(v.begin(), v.end(), 42); std::vector<int>::iterator lower_bound = rg.first; std::vector<int>::iterator upper_bound = rg.second; للتحقّق من وجود عنصر ما في متّجه مُرتّب، يمكنك استخدام الدالّة std::binary_search() مع أنها غير مقصورة على المتّجهات: bool exists = std::binary_search(v.begin(), v.end(), value_to_find); تقليص سعة متجه تزيد المتّجهات في سعتها تلقائيًا عند إدراج عناصر جديدة إن كانت هناك حاجة لذلك، لكنها لا تقلّص سعتها بعد إزالة عنصر ما. // تهيئة متّجه مئوي std::vector < int > v(100); // سعة المتّجه دائما ما تكون أكبر من حجمه auto const old_capacity = v.capacity(); // old_capacity >= 100 // إزالة نصف العناصر v.erase(v.begin() + 50, v.end()); // تخفيض الحجم من 50 إلى 100 // (v.capacity() == old_capacity) يمكننا نسخ محتويات المتجه إلى متجه مؤقت جديد إن أردنا تقليل سعته، وسيكون للمتجه الجديد الحد الأدنى من السعة اللازمة لتخزين جميع عناصر المتجه الأصلية والتي يمكن أن تكون أقل بكثير من سعة المتجه الأولى في حال تقليص الحجم الأصلي بشكل كبير. يمكننا بعد ذلك تبديل المتجه الأصلي بالمتجه المؤقت المُقلّص: std::vector<int>(v).swap(v); الإصدار ≥ C++ 11 في C++ 11، يمكننا استخدام التابع shrink_to_fit() لتحقيق التأثير نفسه: v.shrink_to_fit(); ملاحظة: التابع shrink_to_fit() هو مجرد طلب، ولا يضمن تقليص السعة. حجم المتجهات وسعتها حجم المتّجه هو عدد عناصره: يمكنك الحصول على الحجم الحالي للمتّجه بواسطة الدالة التابعة size()، وتعيد الدالّة empty() القيمة true إن كان الحجم يساوي 0: vector<int> v = { 1, 2, 3 }; // 3 الحجم يساوي const vector<int>::size_type size = v.size(); cout << size << endl; // 3 cout << boolalpha << v.empty() << endl; // false تبدأ المتّجهات المُنشأة افتراضيا بالحجم 0: vector<int> v; // 0 الحجم يساوي cout << v.size() << endl; // 0 تؤدّي إضافة N عنصر إلى المتجه (مثلا، عبر الدوالّ push_back() أو insert() أو resize()) إلى زيادة الحجم بالقيمة N. تؤدّي إزالة N عنصر من المتجه (على سبيل المثال عند استخدام الدوالّ pop_back() أو erase() أو clear()) إلى تقليص الحجم بالقيمة N. الحد الأقصى لحجم المتجه يتعلق بالتنفيذ (implementation-specific)، ولكن لا تشغل نفسك بهذا، فعلى الأرجح ستُستنزف ذاكرة الوصول العشوائي (RAM) قبل بلوغ الحد الأقصى: vector < int > v; const vector < int > ::size_type max_size = v.max_size(); cout << max_size << endl; v.resize(max_size); // لن تعمل على الأرجح v.push_back(1); // بالتأكيد لن تعمل خطأ شائع: حجم المتجه لا يكون بالضرورة (أو حتى عادة) من النوع int: // شيفرة سيئة vector < int > v_bad(N, 1); // N إنشاء متجه كبير حجمه for (int i = 0; i < v_bad.size(); ++i) { // int ليس بالضرورة أن يكون الحجم من النوع do_something(v_bad[i]); } تختلف سعة المتجه عن حجمه، ففي حين أنّ الحجم يمثّل ببساطة عدد العناصر التي يحتويها المتجه حاليًا، إلا أنّ السعة تمثّل عدد العناصر التي حُجِزت ذاكرة لها، وهذا مفيد لأنّ (إعادة) تخصيص أحجام كبيرة في الذاكرة يمكن أن يكون مكلّفا للغاية. يمكن الحصول على السعة الحالية للمتّجه عبر التابع capacity(). وتذكّر أنّ السعة دائمًا أكبر من أو تساوي الحجم: vector<int> v = { 1, 2, 3 }; // الحجم يساوي 3 لكنّ السعة يمكن أن تكون أكبر const vector<int>::size_type capacity = v.capacity(); cout << capacity << endl; يمكنك حجز السعة يدويًا عبر دالّة reserve( N ) (تغيّر سعة المتجه إلى N): // شيفرة سيئة vector < int > v_bad; for (int i = 0; i < 10000; ++i) { v_bad.push_back(i); // الكثير من تخصيصات الذاكرة } // شيفرة جيدة vector < int > v_good; v_good.reserve(10000); // هذا جيد، لأنه ستكون هناك عملية تخصيص واحدة فقط for (int i = 0; i < 10000; ++i) { v_good.push_back(i); // لا حاجة لتخصيص الذاكرة } يمكنك أن تطلب تحرير السعة الزائدة عبر shrink_to_fit() (لكنّ ذلك غير مضمون). هذا مفيد لتوفير الذاكرة المستخدمة: vector<int> v = { 1, 2, 3, 4, 5 }; // الحجم يساوي 5، ونفترض أنّ السعة تساوي 6 v.shrink_to_fit(); // السعة من الممكن أنها تساوي الآن 5، لكن يمكن أن تساوي 6 cout << boolalpha << v.capacity() == v.size() << endl; يدير المتّجهُ السّعةَ جزئيًّا، فقد تقرّر زيادة سعته عند إضافة عناصر إليه. يفضل الكثير من المنفِّذين (Implementers) استخدام 2 أو 1.5 كعامل توسيع. وعلى الناحية الأخرى، لا تتقلّص سعة المتّجهات تلقائيًّا في العادة. مثلا: // السعة يمكن أن تساوي 0، لكن ليس بالضرورة vector < int > v; // السعة تساوي 1 على الأرجح الآن v.push_back(1); // الحجم صار 0 لكن السعة ما تزال تساوي 1 v.clear(); // لنفترض أن الحجم والسعة يساويان 4 v = { 1, 2, 3, 4 }; // ازدياد السعة، لنفترض أنها أصبحت تساوي 6، أي بمعدل توسع 1.5 v.push_back(5); // لا تغيير في السعة v.push_back(6); // ازدياد السعة، لنفترض أنها أصبحت تساوي 9، أي بمعدل توسّع 1.5 v.push_back(7); // السّعة لم تتغير v.pop_back(); v.pop_back(); v.pop_back(); v.pop_back(); إبطال المكررات والمؤشرات (Iterator/Pointer Invalidation) لا يمكن أن تَبطُل المُكرّرات والمؤشّرات التي تشير إلى متجه ما إلا عند إجراء عمليات معينة، إذ سيؤدّي استخدام مكرّرات أو مؤشّرات غير صالحة إلى سلوك غير محدّد. إليك بعض العمليات التي تُبطل المكرّرات والمؤشّرات: أيّ عملية إدخال تغيّر سعة المتجه ستُبطل جميع المُكرّرات والمؤشّرات التي تشير إلى ذلك المتجه: // حجم المتجه يساوي 5 وسعتها غير معروفة vector < int > v(5); int *p1 = &v[0]; // بما أن السعة مجهولة p1 ربما أُبطِل v.push_back(2); // السعة تساوي 20 على الأقل الآن v.reserve(20); int *p2 = &v[0]; // يساوي 7 الآن v لأن حجم p2 لم يتم إبطال v.push_back(4); // إدارج 30 عنصرا في النهاية، سيتجاوز الحجم السعة السابقة، لذا v.insert(v.end(), 30, 9); // صار باطلا الآن `p2` فعلى الأرجح أن int *p3 = &v[0]; // باطلا `p3`تجاوز السعة، وسيصبح v.reserve(v.capacity() + 20); الإصدار ≥ C++ 11 auto old_cap = v.capacity(); v.shrink_to_fit(); if(old_cap != v.capacity()) // إبطال المكررات أيّ عملية إدراج لا تزيد في السعة، ستُبطل المكررات والمؤشّرات التي تشير إلى العناصر الموجودة في الموضع الذي حدث الإدراج عنده أو بعده. يتضمّن ذلك المُكرر end: vector < int > v(5); v.reserve(20); // السعة تساوي 20 على الأقل int *p1 = &v[0]; int *p2 = &v[3]; v.insert(v.begin() + 2, 5, 0); // أصبح باطلا، ولكن بما أنّ السعة لم تتغير `p2` // يبقى صالحا `p1` فإن int *p3 = &v[v.size() - 1]; v.push_back(10); // ما زالا صالحين `p3` و`p1` السعة لم تتغير لذا فإنّ ستؤدّي أيّ عملية إزالة إلى إبطال المكرّرات أو المؤشّرات التي تشير إلى العناصر التي أزيلت أو التي بعدها. يتضمن ذلك المكرّر end: vector<int> v(10); int *p1 = &v[0]; int *p2 = &v[5]; v.erase(v.begin() + 3, v.end()); // `p2` ما زال صالحا على عكس `p1` المعامل operator= و التابع clear() سيبطلان كل المكرّرات أو المؤشّرات التي تشير إلى المتجه. إيجاد العنصر الأكبر/الأصغر مع فهرسه في متجه لإيجاد أكبر أو أصغر عنصر مخزّن في متجه، يمكنك استخدام التابعين std::max_element و std::max_element على الترتيب، هذان التابعان مُعرّفان في الترويسة . إذا كانت هناك عدّة عناصر تساوي القيمة الأكبر (أو الأصغر) في متجه، فإن التوابع ستعيد المُكرّر الذي يشير إلى أوّل تلك العناصر. بالنسبة للمتّجهات الفارغة، تُعاد v.end(). std::vector<int> v = {5, 2, 8, 10, 9}; int maxElementIndex = std::max_element(v.begin(), v.end()) - v.begin(); int maxElement = * std::max_element(v.begin(), v.end()); int minElementIndex = std::min_element(v.begin(), v.end()) - v.begin(); int minElement = * std::min_element(v.begin(), v.end()); std::cout << "maxElementIndex:" << maxElementIndex << ", maxElement:" << maxElement << '\n'; std::cout << "minElementIndex:" << minElementIndex << ", minElement:" << minElement << '\n'; المخرجات: maxElementIndex:3, maxElement:10 minElementIndex:1, minElement:2 الإصدار ≥ C++ 11 يمكن الحصول على الحدّ الأصغر والحد الأكبر في متجه معًا، باستخدام التابع std::minmax_element، وهو مُعرّف في الترويسة : std::vector<int> v = {5, 2, 8, 10, 9}; auto minmax = std::minmax_element(v.begin(), v.end()); std::cout << "minimum element: " << *minmax.first << '\n'; std::cout << "maximum element: " << *minmax.second << '\n'; المخرجات: minimum element: 2 maximum element: 10 تحويل مصفوفة إلى متجه يمكن تحويل مصفوفة بسهولة إلى متجه باستخدام std::begin و std::end: الإصدار≥ C++ 11 int values[5] = { 1, 2, 3, 4, 5 }; // المصفوفة المصدرية std::vector < int > v(std::begin(values), std::end(values)); // نسخ المصفوفة إلى متجه جديد for (auto &x: v) std::cout << x << " "; std::cout << std::endl; المخرجات: 1 2 3 4 5 تحويل وسائط main إلى متجه من السلاسل النصية: int main(int argc, char* argv[]) { std::vector<std::string> args(argv, argv + argc); } يمكن أيضًا استخدام قائمة تهيئة <>initializer_list (الإصدار C++11) لتهيئة المتجه على النحو التالي: initializer_list<int> arr = { 1,2,3,4,5 }; vector < int > vec1 { arr }; for (auto & i: vec1) cout << i << endl; الدوال التي تعيد متجهات كبيرة الإصدار ≥ C++ 11 في الإصدار C++ 11، يُطلب من المُصرّفات النقل ضمنيًّا من المتغير المحلي المٌعاد. وكذلك يمكن لمعظم المُصرّفات إهمال النسخ (copy elision) في كثير من الحالات، وإسقاط النقل تمامًا. ونتيجةً لذلك فإنّ إعادة الكائنات الكبيرة التي يمكن نقلها بدون كلفة كبيرة لا تتطّلب معالجة خاصة: #include <vector> #include <iostream> // (NRVO) “إن عجز المصرف عن إنجاز “ترشيد القيمة المعادة المسماة // إلى القيمة المعادة v وتجنّب النقل بشكل كامل، فعليه أن ينقل من std::vector < int > fillVector(int a, int b) { std::vector < int > v; v.reserve(b - a + 1); for (int i = a; i <= b; i++) { v.push_back(i); } return v; // نقل ضمني } int main() { // إعلان المتجه وملؤه std::vector < int > vec = fillVector(1, 10); // طباعة المتجه for (auto value: vec) std::cout << value << " "; // "1 2 3 4 5 6 7 8 9 10 " std::cout << std::endl; return 0; } الإصدار < C++11 ++> قبل الإصدار C++ 11، كان يُسمَح بإهمال النسخ (copy elision)، وقد طُبِّق ذلك في معظم المُصرّفات. ومع ذلك، ونظرًا لغياب دلالات النقل (move semantics)، فقد ترى في الشيفرات القديمة أو الشيفرات التي يجب تصريفها بمصرّفات قديمة لا تدعم هذه الميزة أنّ المتّجهات تُمرَّر كوسائط إخراج (output arguments) لمنع النسخ غير الضروري: #include <vector> #include <iostream> // تمرير المتجه بالمرجع void fillVectorFrom_By_Ref(int a, int b, std::vector < int > & v) { assert(v.empty()); v.reserve(b - a + 1); for (int i = a; i <= b; i++) { v.push_back(i); } } int main() { // التصريح عن متجه std::vector < int > vec; // ملء المتجه fillVectorFrom_By_Ref(1, 10, vec); // طباعة المتجه for (std::vector < int > ::const_iterator it = vec.begin(); it != vec.end(); ++it) std::cout << * it << " "; // "1 2 3 4 5 6 7 8 9 10 " std::cout << std::endl; return 0; } هذ الدرس جزء من سلسلة مقالات عن C++. ترجمة -بتصرف- للفصل Chapter 49: std::vector من كتاب C++ Notes for Professionals
-
النوع std::string والذي يدعى السلسلة النصية في العربية هو كائن يمثّل سلاسل من المحارف، ويوفر صنف string القياسي خيارًا بسيطًا وآمنًا ومتعدّد الاستخدامات لتخزين سلاسل المحارف ومعالجتها، وذلك موازنة بمصفوفات المحارف، وstring هو جزء من فضاء الاسم std، وقد صار معياريًا في عام 1998. تقطيع السلاسل النصية يمكن تقطيع سلسلة نصيَّة إلى سلاسل نصيَّة أصغر تدعى الوحدات أو القطع (tokens)، تُسمّى هذه العمليَّة بالترميز المُقطَّع للسلاسل النصيَّة أو تقطيع السلاسل النصية (String Tokenization). ولدينا عدّة طرق لتقطيع سلسلة نصّية، وسنُدرجها من الأقل إلى الأكثر تكلفة في وقت التشغيل: الطريقة الأولى: std::strtok هي أقل طرق تقطيع السلاسل النصّية تكلفة، كما تسمح بتعديل الفاصل (delimiter) بين القطع (tokens)، غير أن هذه الطريقة تعتريها ثلاثة إشكاليات في C++: لا يمكن استخدام std::strtok على عدّة سلاسل نصّية في نفس الوقت (باستثناء بعض التطبيقات implementations التي تمكّن من ذلك، مثل: strtok_s) لا يمكن استخدام std::strtok على عدّة خيوط أو عمليات (threads) في وقت واحد (لكن قد يتغيّر ذلك بحسب التطبيق كما هو الحال في Visual Studio) استدعاء std::strtok يُعدّل السلسلة النصية التي يعمل عليها، لذلك لا يمكن استخدامه على السلاسل النصّية من النوع const strings أو const char* أو السلاسل النصية المجردة، ويجب نسخ السلاسل النصّية المُدخلة لتقطيع أيٍّ من هذه السلاسل باستخدام std::strtok أو للعمل على السلاسل النصّية التي ينبغي حفظ محتوياتها، ثم يمكن العمل على النسخة بعد ذلك . دورة تطوير التطبيقات باستخدام لغة Python احترف تطوير التطبيقات مع أكاديمية حسوب والتحق بسوق العمل فور انتهائك من الدورة اشترك الآن ستكون تكلفة هذه الخيارات جزءًا من تكلفة حجز القطع، لكن إذا كنت تريد استخدام خوارزميات سريعة ولم تتمكن من تجاوز إشكاليات std::strtok، فربما عليك النظر في خيار hand-spun solution. // السلسلة النصية المراد تقطيعها std::string str { "The quick brown fox" }; // المتجه الذي سنخزن فيه القطع. vector < std::string > tokens; for (auto i = strtok( & str[0], " "); i != NULL; i = strtok(NULL, " ")) tokens.push_back(i); انظر هذا المثال الحي. الطريقة الثانية: std::istream_iterator يستخدم std::istream_iterator هنا عامل الاستخراج من المجرى بشكل تكراري، وإذا كانت السلسلة النصّية المعطاة مفصولة بمسافات فارغة فسنستطيع التوسعة على المعامِل std::strtok بالتخلص من صعوباته، مما يسمح بالترميز المقطَّع المُضمَّن (inline)، ومن ثم دعم إنشاء متجهات السلاسل النصّية الثابتة (const vector<string>) ودعم محرف مسافات فاصلة متعددة، انظر: // السلسلة النصية المراد تقطيعها const std::string str("The quick \tbrown \nfox"); std::istringstream is(str); // المتجه الذي سنخزن فيه القطع. const std::vector<std::string> tokens = std::vector<std::string>( std::istream_iterator<std::string>(is), std::istream_iterator<std::string>()); انظر هذا المثال الحي. الطريقة الثالثة: std::regex_token_iterator تَستخدم std::regex_token_iterator تعبيرًا نمطيًّا أو std::regex للقيام بعملية التقطيع بشكل تكراري، وهذا يسمح بتعريف الفاصل بشكل أكثر مرونة، مثل الفاصلات , والمسافات الفارغة، انظر: الإصدار ≥ C++ 11 // السلسلة النصية المُراد تقطيعها const std::string str { "The ,qu\\,ick ,\tbrown, fox" }; const std::regex re { "\\s*((?:[^\\\\,]|\\\\.)*?)\\s*(?:,|$)" }; // المتجه الذي سنخزن فيه القطع. const std::vector < std::string > tokens { std::sregex_token_iterator(str.begin(), str.end(), re, 1), std::sregex_token_iterator() }; انظر هذا المثال الحي، وهذا مثال آخر في درس التعابير النمطية للمزيد من التفاصيل. التحويل إلى مؤشر (const) char* استخدم الدالة التابعة c_str() لتمكين مؤشّر const char* من الوصول إلى بيانات سلسلة نصّية، واعلم أنّ المؤشّر يبقى صالحًا ما دامت السلسلة النصّية ضمن النطاق (scope) ولم يمسّها تغيير، هذا يعني أنه لا يمكن استدعاء توابع غير ثابتة على الكائن. الإصدار ≥ C++ 17 استخدم الدالة التابعة data() للحصول على مؤشّر قابل للتغيير char*، والذي يمكن استخدامه لمعالجة السلسلة النصّية. الإصدار ≥ C++ 11 ويمكن الحصول على مؤشّر char* قابل للتعديل عن طريق أخذ عنوان الحرف الأول: &s[0]، سيُعيد ذلك في C++ 11 سلسلةً نصّية مُنسّقة جيدًا ومنتهية بالمحرف الفارغ (null-terminated). لاحظ أنّ &s[0] ستكون مُنسّقة حتى لو كانت s فارغة، بينما تكون &s.front() غير مُحدّدة إن كانت s فارغة. الإصدار ≥ C++ 17 في المثال التالي، تشير كل من cstr و data إلى "This is a string.\0": std::string str("This is a string."); const char* cstr = str.c_str(); const char* data = str.data(); std::string str("This is a string."); انسخ محتويات str لفك lifetime من كائن std::string: std::unique_ptr<char []> cstr = std::make_unique<char[]>(str.size() + 1); // حل بديل للسطر أعلاه، غير محصن من الاعتراضات. // char* cstr_unsafe = new char[str.size() + 1]; std::copy(str.data(), str.data() + str.size(), cstr); cstr[str.size()] = '\0'; // ينبغي إضافة سلسلة نصّية منتهية بحرف فارغ // delete[] cstr_unsafe; std::cout << cstr.get(); استخدام الصنف std::string_view الإصدار ≥ C++ 17 قدّمت C++ 17 الصنف std::string_view، وهو ببساطة مدىً غير مالك (non-owning range) من المحارف الثابتة (const char)، والقابلة للتنفيذ إمّا على هيئة زوج من المؤشّرات، أو مؤشّر وطول (length)، وهو نوع معاملات أفضل للدوالّ التي تتطلب سلاسل نصّية غير قابلة للتعديل. وكان قبل الإصدار C++ 17 ثلاثة خيارات لفعل هذا: الأول: وسيط واحد، قد يقوم بالتخصيص إن لم تكن بيانات المستدعي في سلسلة نصية مثل سلسلة نصية مجردة أو <vector<char: void foo(std::string const& s); الثاني: وسيطان، يجب أن يمررهما في كل مكان. void foo(const char* s, size_t len); والثالث: وسيط واحد، لكن يجب أن يستدعي ()strlen. void foo(const char* s); يستطيع المستدعي تمرير مزود بيانات محرفية لكن سيكون على ()foo أن يتواجد في ترويسة. template < class StringT > void foo(StringT const& s); يمكن استبدال كلّ ما في المثال السابق بالشيفرة التالية، ومزاياها أنها بوسيط واحد، مع ربط أقوى، وبدون نُسخ بغض النظر عن كيفية تخزين المستدعي للبيانات. void foo(std::string_view s); لاحظ أنّ std::string_view لا يمكنها تعديل البيانات الأساسية (underlying data) الخاصّة بها. string_view مفيدة في حال أردت تجنّب عمليات النسخ غير الضرورية، كما تقدم مجموعة من وظائف السلاسل النصّية، رغم أنّ سلوك بعض الدوالّ قد يختلف، انظر المثال التالي لسلسلة نصية طويلة بدون داعي: std::string str = "lllloooonnnngggg sssstttrrriiinnnggg"; // سلسلة نصّية طويلة استخدام string::subsr سيعيد سلسلة نصية جديدة، وهذا مكلف إن كانت السلسلة طويلة، انظر: std::cout << str.substr(15, 10) << '\n'; وعليه فلا نحبذ هذا الأسلوب، ومن ناحية أخرى فلن تنشئ الطريقة الصحيحة هنا أي نسخ إضافية، انظر: std::string_view view = str; وستعيد string_view::substr سلسلة string_view جديدة: std::cout << view.substr(15, 10) << '\n'; التحويل إلى std::wstring تُمثَّل تسلسلات المحارف في C++ عبر تخصيص الصنف std::basic_string بنوع محرفي أصلي (native)، والمجموعتان الرئيسيتان المُعرّفتان في المكتبة القياسية هما std::string و std::wstring: std::string هي سلسلة مبنية بعناصر من نوع char std::wstring مبنية بعناصر من نوع wchar_t استخدم لأجل التحويل بين النوعين، wstring_convert: #include <string> #include <codecvt> #include <locale> std::string input_str = "this is a -string-, which is a sequence based on the -char- type."; std::wstring input_wstr = L"this is a -wide- string, which is based on the -wchar_t- type."; // التحويل std::wstring str_turned_to_wstr = std::wstring_convert<std::codecvt_utf8<wchar_t>>().from_bytes(input_str); std::string wstr_turned_to_str = std::wstring_convert<std::codecvt_utf8<wchar_t>>().to_bytes(input_wstr); لتحسين قابلية الاستخدام وسهولة القراءة، عرِّف دوالًا لتنفيذ عملية التحويل: #include <string> #include <codecvt> #include <locale> using convert_t = std::codecvt_utf8 < wchar_t > ; std::wstring_convert < convert_t, wchar_t > strconverter; std::string to_string(std::wstring wstr) { return strconverter.to_bytes(wstr); } std::wstring to_wstring(std::string str) { return strconverter.from_bytes(str); } مثال تطبيقي: std::wstring a_wide_string = to_wstring("Hello World!"); هذا أفضل وأوضح بكثير من السطر التالي: td::wstring_convert<std::codecvt_utf8<wchar_t>>().from_bytes("HelloWorld!") لاحظ أنّ char و wchar_t لا تقتضيان الترميز (encoding) ولا تُعطيان أيّ إشارة عن الحجم بالبايتات، على سبيل المثال، تُستخدم wchar_t عادة كنوع بيانات ثنائي البايت، وعادة ما تحتوي على بيانات مُرمّزة بترميز UTF-16 في ويندوز (أو UCS-2 في الإصدارات السابقة لنظام ويندوز 2000) أو كنوع بيانات رباعي البايت مُرمّز باستخدام الترميز UTF-32 في لينكس. هذا على النقيض من الأنواع الحديثة char16_t و char32_t، والتي جاءت في الإصدار C++ 11، إذ هي كبيرة بما يكفي لاحتواء أيّ محرف من UTF16 أو UTF32 على الترتيب. الموازنة المعجمية يمكن موازنة سلسلتين نصّيتين معجميًا باستخدام المعاملات == و != و < و <= و > و >=: std::string str1 = "Foo"; std::string str2 = "Bar"; assert(!(str1 < str2)); assert(str > str2); assert(!(str1 <= str2)); assert(str1 >= str2); assert(!(str1 == str2)); assert(str1 != str2); تستخدِم كل هذه الدوالّ التابع std::string::compare() لإجراء المقارنات وإعادة القيمة البوليانية المناسبة. وفيما يلي شرح عامّ لمعَاملات الموازنة: المُعامل == إذا كانت str1.length() == str2.length()، وتطابقت أزواج المحارف (character pairs)، فسيعيد المُعاملُ القيمةَ true، وإلا فسَيعيد false. المُعامل != إذا كانت str1.length() != str2.length() أو لم تتطابق أزواج المحارف، فسَيعيد المُعامل القيمة true، وإلا فسيعيد false. المُعامل < أو المُعامل > يبحثان عن أول زوج غير متطابق من المحارف، ويقارن بينهما ثم يعيد النتيجة البوليانية تبعًا لنتيجَة الموازنة. المُعامل <= أو المُعامل >= يبحث عن أول زوج غير متطابق من المحارف، ويقارن بينهما ثم يعيد النتيجة البوليانية. ملاحظة: يشير مصطلح زوج المحارف (character pair) إلى المحارف المتقابلة في كلا السلسلتين، أي الحرفين الذين يوجَدين في نفس الموضع من السلسلتين النّصّيتين. مثلًا، لنفترض أنّ لدينا سلسلتين نصّيتين str1 و str2، وطولاهما n و m على التوالي، في هذه الحالة ستكون أزواج المحارف في كلتَي السلسلتين النصّيتين هي الأزواج str1 و str2 حيث i = 0, 1, 2, …, max(n,m). في حال عدم وجود حرف يقابل الفهرس i، أي عندما يكون i أكبر من أو يساوي n أو m، فسيتم اعتبارُه القيمةَ الأصغر. فيما يلي مثال على استخدام <: std::string str1 = "Barr"; std::string str2 = "Bar"; assert(str2 < str1); خطوات الموازنة هي كالتالي: موازنة الحرفين الأوّلين، 'B' == 'B' - ثم متابعة. موازنة بين الحرفين الثانيين، 'a' == 'a' - ثم متابعة. موازنة بين الحرفين الثّالثين، 'r' == 'r' - ثم متابعة. استُنفِذ نطاق str2 الآن، في حين أنّ نطاق str1 ما يزال فيه محارف زائدة. وبالتالي يكون لدينا: str2 < str1. تقليم المحارف في بداية أو نهاية السلسلة النصية يتطلب هذا المثال الترويسات و و . الإصدار ≥ C++ 11 تقليم (trim) تسلسل أو سلسلة نصّية يعني إزالة جميع العناصر الأولى أو الأخيرة التي تحقق شرطًا معينًا، ونقلّم أولًا العناصر الأخيرة لأنها لا تتطلب تحريك أيّ عنصر، ثم نقلّم العناصر الأولى. لاحظ أنّ التعميمات أدناه تعمل مع جميع أنواع السلاسل النصّية الأساسية std::basic_string، مثل std::string و std::wstring، وأيضًا على الحاويات مثل std::vector و std::list. template < typename Sequence, // list أو vector أي سلسلة نصّية أساسية، مثل typename Pred > // شرط Sequence& trim(Sequence& seq, Pred pred) { return trim_start(trim_end(seq, pred), pred); } يعتمد تقليم العناصر الأخيرة على إيجاد العنصر الأخير غير المطابق للشّرط، وبدء عملية المحو من هناك: template < typename Sequence, typename Pred > Sequence & trim_end(Sequence & seq, Pred pred) { auto last = std::find_if_not(seq.rbegin(), seq.rend(), pred); seq.erase(last.base(), seq.end()); return seq; } أمّا تقليم العناصر الأولية، فيعتمد على تحديد العنصر الأوّل الذي لا يحقّق الشرط وبدء عملية المحو من بداية السلسلة وحتى هناك: template < typename Sequence, typename Pred > Sequence & trim_start(Sequence & seq, Pred pred) { auto first = std::find_if_not(seq.begin(), seq.end(), pred); seq.erase(seq.begin(), first); return seq; } استخدم الدالّة std::isspace() كشرط إذا أردت تقليم المسافات الفارغة في سلسلة نصّية : std::string& trim(std::string& str, const std::locale& loc = std::locale()) { return trim(str, [&loc](const char c){ return std::isspace(c, loc); }); } std::string& trim_start(std::string& str, const std::locale& loc = std::locale()) { return trim_start(str, [&loc](const char c){ return std::isspace(c, loc); }); } std::string& trim_end(std::string& str, const std::locale& loc = std::locale()) { return trim_end(str, [&loc](const char c){ return std::isspace(c, loc); }); } وبالمثل، يمكننا استخدام الدالّة std::iswspace() مع السلاسل النصّية std::wstring. إذا كنت ترغب في إنشاء تسلسل جديد عبر إنشاء نسخة مُقلَّمة، فيمكنك استخدام دالّة منفصلة على النحو التالي: template < typename Sequence, typename Pred > Sequence trim_copy(Sequence seq, Pred pred) { // بالقيمة seq تمرير trim(seq, pred); return seq; } استبدال السلاسل الصّية الاستبدال بالموضع استخدم التابع replace الخاص بالصنف std::string لاستبدال جزء من سلسلة نصّية. أيضًا، التابع replace له الكثير من التحميلات الزائدة (overloads) المفيدة: // عرِّف سلسلة نصّية std::string str = "Hello foo, bar and world!"; std::string alternate = "Hello foobar"; //1) str.replace(6, 3, "bar"); //"Hello bar, bar and world!" //2) str.replace(str.begin() + 6, str.end(), "nobody!"); //"Hello nobody!" //3) str.replace(19, 5, alternate, 6, 6); //"Hello foo, bar and foobar!" الإصدار ≥ C++ 14 //4) str.replace(19, 5, alternate, 6); //"Hello foo, bar and foobar!" //5) str.replace(str.begin(), str.begin() + 5, str.begin() + 6, str.begin() + 9); //"foo foo, bar and world!" //6) str.replace(0, 5, 3, 'z'); //"zzz foo, bar and world!" //7) str.replace(str.begin() + 6, str.begin() + 9, 3, 'x'); //"Hello xxx, bar and world!" الإصدار ≥ C++ 11 //8) str.replace(str.begin(), str.begin() + 5, { 'x', 'y', 'z' }); //"xyz foo, bar and world!" استبدال سلسلة نصّية بأخرى استبدل بالظهور الأول فقط للسلسلة النصّية with السلسلة replace في str: std::string replaceString(std::string str, const std::string& replace, const std::string& with) { std::size_t pos = str.find(replace); if (pos != std::string::npos) str.replace(pos, replace.length(), with); return str; } استبدال السّلسلة النصّية with بالسلسلة replace أينما ظهرت في str: std::string replaceStringAll(std::string str, const std::string& replace, const std::string& with) { if (!replace.empty()) { std::size_t pos = 0; while ((pos = str.find(replace, pos)) != std::string::npos) { str.replace(pos, replace.length(), with); pos += with.length(); } } return str; } التحويل إلى سلسلة نصية استخدم std::ostringstream لتحويل أيّ نوع قابل للإجراء (streamable type) إلى تمثيله النصي -أي إلى سلسلة نصّية-، عن طريق إدراج الكائن المُراد تحويله في كائن std::ostringstream (عبر معامل إدراج المجرى <<)، ثم تحويل std::ostringstream بأكملها إلى سلسلة نصّية. مثلًا، بالنسبة للأعداد الصحيحة int: #include <sstream> int main() { int val = 4; std::ostringstream str; str << val; std::string converted = str.str(); return 0; } اكتب دالّة التحويل الخاصة بك: template < class T > std::string toString(const T & x) { std::ostringstream ss; ss << x; return ss.str(); } هذه الدالّة ستعمل بنجاح، لكنّ أداءها أقلّ كفاءة. يمكن أن تنفذ الأصناف المُعرَّفة من قبل المستخدم عاملَ إدراج المجرى، انظر المثال التالي حيث نكتب تمثيلًا نصيًا ل a في out: std::ostream operator << (std::ostream & out, const A & a) { return out; } الإصدار ≥ C++ 11 بصرف النظر عن مجاري التدفق، أصبح ممكنًا منذ الإصدار C++ 11 أن نستخدم الدالّة std::to_string (و std::to_wstring) والتي حُمِّلت تحميلًا زائدًا في جميع الأنواع الأساسية، وصارت تعيد تمثيلًا نصيًّا للمعامل المُمرّر إليها. std::string s = to_string(0x12f3); // "4851" القيمة s بعد هذا ستحتوي التقسيم (Splitting) استخدم std::string::substr لتقسِيم السلاسل النصّية. هناك نوعان من هذا التابع، يأخذ الأول موضع البداية، والذي ستبدأ منه السلسلة النصّية الفرعية (substring) المُعادة. يجب أن يكون موضع البداية جزءًا من النطاق (0, str.length()]: std::string str = "Hello foo, bar and world!"; std::string newstr = str.substr(11); // "bar and world!" فيما يأخذ التابع الثاني موضع البداية والطول الإجمالي للسّلسلة النصّية الفرعية الجديدة. ولن تتجاوز السلسلة النصّية الفرعية نهاية السلسلة النصّية المصدرية مهما كان الطول المُمرَّر: std::string str = "Hello foo, bar and world!"; std::string newstr = str.substr(15, 3); // "and" لاحظ أنّك تستطيع استدعاء substr بدون أي وسائط، وفي هذه الحالة ستُعاد نسخة مضبوطة من السلسلة النصية: std::string str = "Hello foo, bar and world!"; std::string newstr = str.substr(); // "Hello foo, bar and world!" الوصول إلى محرف من السلسلة النصية هناك عدة طرق لاستِخراج الحروف من السلاسل النصّية، ولكلّ منها خصوصياتها. std::string str("Hello world!"); operator يعيد مرجعًا إلى الحرف الموجود عند الفهرس n. لا يتحقّق المُعامل td::string::operator[] من حدود السلسلة النصّية، ولا يرفع اعتراضًا (exception) في حال تجاوزها، لذلك فالمُستدعي هو المسؤول عن التحقق من أنّ الفهرس يقع في حدود السلسلة النصّية: char c = str[6]; // 'w' at(n) يعيد مرجعًا إلى الحرف الموجود عند الفهرس n. يتحقق المُعامل std::string::at من الحدود، ويرفع اعتراض std::out_of_range إذا لم يكن الفهرس ضمن حدود السلسلة النصّية: char c = str.at(7); // 'o' الإصدار ≥ C++ 11 ملاحظة: في كلا المثالين أعلاه، سيكون السّلوك غير محدد في حال كانت السلسلة النصية فارغة. front() يعيد مرجعًا إلى الحرف الأول: char c = str.front(); // 'H' back() يعيد مرجعًا إلى الحرف الأخير: char c = str.back(); // '!' التحقق من كون سلسلة نصية سابقة (prefix) لسلسلة أخرى الإصدار ≥ C++ 14 في الإصدار C++ 14، يمكن إنجاز ذلك بسهولة عبر std::mismatch، والذي يُعيد الزوج الأوّل غير المتطابق من النطاقين: std::string prefix = "foo"; std::string string = "foobar"; bool isPrefix = std::mismatch(prefix.begin(), prefix.end(), string.begin(), string.end()).first == prefix.end(); قبل الإصدار C++ 14، كان هناك ما يُسمّى إصدار النطاق ونصف (range-and-a-half version) من التابع mismatch()، ولكنه لم يكن آمنًا في حال كانت السلسلة النصّية الثانية أقصر من الأولى. الإصدار < C++ 14 ++> لا زال بإمكاننا استخدام إصدار النطاق ونصف من التابع std::mismatch()، لكن سيكون عليك التحقّق أولًا من أنّ السلسلة النصّية الأولى أقصر من الثانية أو تعادلها: bool isPrefix = prefix.size() <= string.size() && std::mismatch(prefix.begin(), prefix.end(), string.begin(), string.end()).first == prefix.end(); الإصدار ≥ C++ 17 يمكننا كتابة الموازنة التي نريدها مباشرة مع std::string_view، دون الحاجة إلى القلق بشأن الحِمل الزائد في الذاكرة (allocation overhead) أو إنشاء النُّسخ: bool isPrefix(std::string_view prefix, std::string_view full) { return prefix == full.substr(0, prefix.size()); } التكرار على المحارف الإصدار ≥ C++ 17 تدعم السلاسل النصّية المُكرّرات، وعليه تستطيع استخدام حلقة نطاقية (ranged based loop) للتكرار على المحارف: std::string str = "Hello World!"; for (auto c: str) std::cout << c; يمكنك أيضًا استخدام حلقة for "تقليدية" للتكرار على المحارف: std::string str = "Hello World!"; for (std::size_t i = 0; i < str.length(); ++i) std::cout << str[i]; التحويل إلى الأعداد الصحيحة أو العَشرية يمكن تحويل سلسلة نصّية تحتوي على عدد إلى نوع عددي صحيح أو عشري باستخدام دوالّ التحويل. ملاحظة: تتوقف جميع هذه الدوالّ عن تحليل السلسلة النصّية المُدخلة بمجرد أن تصادف محرفًا غير رقمي. مثلاً، ستُحوّل السلسلة النصّية "123abc" إلى 123. تحوّل مجموعة الدوالّ std::ato* السلاسل النصّية من نمط لغة C (مصفوفات المحارف) إلى أنواع عددية صحيحة أو عشرية: std::string ten = "10"; double num1 = std::atof(ten.c_str()); int num2 = std::atoi(ten.c_str()); long num3 = std::atol(ten.c_str()); الإصدار ≥ C++ 11 long long num4 = std::atoll(ten.c_str()); إلا أنّه يفضَّل تجنّب استخدام هذه الدوالّ لأنها تُعيد 0 في حال فشلت في تحليل السلسلة النصّية. وهذا سيء لأنّ القيمة 0 قد تُفسَّر على أنّها نتيجة عددية صالحة. مثلًا، إن كانت السلسلة النصية المُدخلة هي "0"، فسيكون من المستحيل تحديد ما إذا كان التحويل قد فشل أم لا بغضّ النظر عن النتيجة. وتحوّل الدوالّ الحديثة std::sto* السلاسل النصّية إلى أعداد صحيحة أو عشرية، وتطلق استثناءً في حال فشلت في تحليل السلسلة النصية. استخدم هذه الدّوال كلما أمكن: الإصدار ≥ C++ 11 std::string ten = "10"; int num1 = std::stoi(ten); long num2 = std::stol(ten); long long num3 = std::stoll(ten); float num4 = std::stof(ten); double num5 = std::stod(ten); long double num6 = std::stold(ten); كذلك، فإنّ هذه الدوالّ يمكنها أن تتعامل أيضًا مع السلاسل النصّية الثُمانية (octal) والست عشرية (hex)، وذلك على عكس دوالّ std::ato*. والمعامل الثاني هو مؤشّر يشير إلى أوّل حرف غير مُحوَّل في السلسلة النصية المُدخَلَة، أمّا المعامل الثالث فيمثّل الأساس الرقمي (base) الذي يجب استخدامه. يُستخدَم الحرف 0 للرصد التلقائي للأعداد الثُمانية (تبدأ بـ 0) والست عشرية (تبدأ بـ 0x أو 0X)، أما القيم الأخرى فتمثّل الأساس الذي يجب استخدامه. std::string ten = "10"; std::string ten_octal = "12"; std::string ten_hex = "0xA"; int num1 = std::stoi(ten, 0, 2); // 2 int num2 = std::stoi(ten_octal, 0, 8); // 10 long num3 = std::stol(ten_hex, 0, 16); // 10 long num4 = std::stol(ten_hex); // 0 long num5 = std::stol(ten_hex, 0, 0); // 0x تعيد 10 لأنها رصدت ضم السلاسل النصّية يمكنك ضمّ (concatenate) السلاسل النصّية باستخدام المُعاملين المُحمَّلين تحميلًا زائدًا + و +=. استخدام المُعامل +: std::string hello = "Hello"; std::string world = "world"; std::string helloworld = hello + world; // "Helloworld" استخدام المُعامل +=: std::string hello = "Hello"; std::string world = "world"; hello += world; // "Helloworld" يمكنك أيضًا ضمّ السلاسل النصّية من نمط لغة C، بما في ذلك السلاسل النصّية المجردة: std::string hello = "Hello"; std::string world = "world"; const char *comma = ", "; std::string newhelloworld = hello + comma + world + "!"; // "Hello, world!" يمكنك أيضًا استخدام push_back() لإضافة حرف واحد إلى السلسلة النصية: std::string s = "a, b, "; s.push_back('c'); // "a, b, c" لدينا يوجد أيضًا تابع append()، والذي يشبه إلى حد كبير +=: std::string app = "test and "; app.append("test"); // "test and test" التحويل بين ترميزات المحارف التحويل بين الترميزات (encodings) أمر سهل في C++ 11، ويمكن لمعظم المصرّفات إنجازه بطريقة مستقلّة عن المنصة (cross-platform) عبر الترويستين و . #include <iostream> #include <codecvt> #include <locale> #include <string> using namespace std; int main() { // wstring و utf8 يحول بين سلاسل wstring_convert < codecvt_utf8_utf16 < wchar_t >> wchar_to_utf8; // utf16 وسلاسل utf8 يحول بين سلاسل wstring_convert < codecvt_utf8_utf16 < char16_t > , char16_t > utf16_to_utf8; wstring wstr = L"foobar"; string utf8str = wchar_to_utf8.to_bytes(wstr); wstring wstr2 = wchar_to_utf8.from_bytes(utf8str); wcout << wstr << endl; cout << utf8str << endl; wcout << wstr2 << endl; u16string u16str = u"foobar"; string utf8str2 = utf16_to_utf8.to_bytes(u16str); u16string u16str2 = utf16_to_utf8.from_bytes(utf8str2); return 0; } لاحظ أنّ Visual Studio 2015 يدعم مثل هذه التحويلات، ولكن بسبب خلل موجود في مكتبتها، فينبغي استخدام قالب مختلف لـ wstring_convert عند التعامل مع الترميز char16_t: using utf16_char = unsigned short; wstring_convert<codecvt_utf8_utf16<utf16_char>, utf16_char> conv_utf8_utf16; void strings::utf16_to_utf8(const std::u16string& utf16, std::string& utf8) { std::basic_string<utf16_char> tmp; tmp.resize(utf16.length()); std::copy(utf16.begin(), utf16.end(), tmp.begin()); utf8 = conv_utf8_utf16.to_bytes(tmp); } البحث عن محرف أو أكثر في السلسلة النصية استخدام التابع std::string::findلإيجاد حرف أو سلسلة نصّية أخرى، والذي يعيد موضع الحرف الأوّل من التطابق الأوّل. أما إن لم يُعثر على أيّ تطابق، فستُعاد القيمة std::string::npos std::string str = "Curiosity killed the cat"; auto it = str.find("cat"); if (it != std::string::npos) std::cout << "Found at position: " << it << '\n'; else std::cout << "Not found!\n"; ستكون النتيجة 21. تُوسَّع فرص البحث بالدوال التالية: find_first_of // إيجاد أول ظهور للمحارف find_first_not_of // إيجاد أول غياب للمحارف find_last_of // إيجارد آخر ظهور للمحارف find_last_not_of // إيجاد آخر غياب للمحارف تتيح لك لك هذه الدوالّ أن تبحث عن المحارف من نهاية السلسلة النصّية، وكذلك البحث عن الحالات السلبية (أي المحارف غير الموجودة في السلسلة). انظر المثال التالي: std::string str = "dog dog cat cat"; std::cout << "Found at position: " << str.find_last_of("gzx") << '\n'; ستكون النتيجة 6. ملاحظة: لا تبحث الدوالّ المذكورة أعلاه عن السلاسل النصّية الفرعية (substrings)، بل عن المحارف الموجودة في سلسلة البحث، ففي المثال أعلاه عُثِر على الظهور الأخير لـ 'g' في الموضع 6 بينما لم يُعثر على المحارف الأخرى. هذا الدرس جزء من سلسلة مقالات عن C++. ترجمة -بتصرّف- للفصل Chapter 47: std::string من كتاب C++ Notes for Professionals
-
مثال بسيط يحتوي المثال التالي على شيفرة يُراد تقسيمها إلى عدّة ملفّات مصدرية، سننظر الآن في كل ملف على حدة: الملفات المصدرية my_function.h في هذا الملف، لاحظ أن الترويسة التالية تحتوي على تصريح للدالة فقط، ولا تعرِّف دوال الترويسة تطبيقات للتصريحات إلا إن وجب معالجة الشيفرة أكثر أثناء التصريف، كما هو الحال في القوالب. وعادة ما تحتوي ملفات الترويسة على واقيات معالج مسبق (Preprocessor Guards) حتى لا تضاف نفس الترويسة مرتين. ويُنفَّذ الواقي بالتحقق من كون المفتاح الرمزي (Token) الفريد للمعالج المسبق معرَّفًا أم لا، ولا تُضمَّن الترويسة إلا إن كانت غير مضمَّنة من قبل. سيتم التعرف على كل من global_value و ()my_function على أنهما نفس البُنية إن أضيفت هذه الترويسة من قبَل عدة ملفات. // my_function.h #ifndef MY_FUNCTION_H #define MY_FUNCTION_H const int global_value = 42; int my_function(); #endif // MY_FUNCTION_H my_function.cpp في هذا الملف، لاحظ أن الملف المصدري المقابل للترويسة يدرِج الواجهة المعرَّفة في الترويسة، كي ينتبه المصرِّف إلى ما ينفذه الملف المصدري. ويتطلب الملف المصدري في هذا الحالة معرفة الثابت العام global_value المعرَّف في ملف my_function.h الذي استعرضناه قبل قليل، ولن يصرَّف هذا الملف المصدري بدون الترويسة. // my_function.cpp #include "my_function.h" // or #include "my_function.hpp" int my_function() { return global_value; // 42; } main.cpp تُدرج ملفّات الترويسة بعد ذلك في الملفّات المصدرية الأخرى التي ترغب في استخدام الوظائف المُعرّفة في واجهة الترويسة، دون الحاجة إلى معرفة تفاصيل تنفيذها، مما يساعد على اختزال الشيفرة. يستخدم البرنامج التالي الترويسة my_function.h: // main.cpp #include <iostream> // ترويسة مكتبة قياسية #include "my_function.h" // ترويسة خاصة int main(int argc, char** argv) { std::cout << my_function() << std::endl; return 0; } عملية التصريف (The Compilation Process) تكون ملفّات الترويسة غالبًا جزءًا من عملية التصريف، لذا يحدث ما يلي خلال عملية التصريف في العادةً: على افتراض أنّ ملفّ الترويسة وملفّّ الشيفرة المصدرية موجودان في نفس المجلّد، فيمكن تصريف البرنامج عبر تنفيذ الأوامر التالية: g++ - c my_function.cpp g++main.cpp my_function.o السطر الأولُ في الشيفرة السابقة يصرِّف الملفَّ المصدري my_function.cpp إلى my_function.o، ويربط السطر الثاني ملفَّ الكائن الذي يتحوي تنفيذ ()int my_function إلى نسخة الكائن المصرَّفة من main.cpp ثم ينتج النسخة التنفيذية النهائية a.out. بالمقابل، إذا رغبت في تصريف main.cpp إلى ملفّ كائنٍ أولًا، ثم ربط ملفّات التعليمات المصرّفة في النهاية، فيمكنك ذلك عبر الشيفرة التالية: g++ -c my_function.cpp g++ -c main.cpp g++ main.o my_function.o القوالب في ملفات الترويسات تتطلّب القوالب إنشاء الشيفرة وقت التصريف: على سبيل المثال، تُحوّل دالّة مُقولَبة (templated function) إلى عدة دوالّ مختلفة بمجرد جعل الدالّة المُقَولبة معامِلًا (parameter) عبر استخدامها في الشيفرة المصدرية. هذا يعني أنّه لا يمكن تفويض الدوالّ والتوابع وتعريفات الأصناف في القوالب إلى ملفّ مصدري آخر، ذلك أنّ أيّ شيفرة تستخدم بنية مُقولَبَة تحتاج إلى أن تَطَّلِع على تعريف تلك البنية لإنشاء الشيفرة المشتقّة. وعليه يجب أن تحتوي الشيفرة المُقولبَة على تعريفها في حال وُضِعت في الترويسات. هذا مثال على ذلك: // templated_function.h template < typename T > T* null_T_pointer() { T* type_point = NULL; // وما بعدها C++11 في NULL بدلا من nullptr أو return type_point; } معالجة التاريخ والوقت باستخدام الترويسة <chrono> قياس الوقت باستخدام <chrono> يمكن استخدام system_clock لقيَاس الوقت المنقضي منذ مرحلة معيّنة من تنفيذ البرنامج. الإصدار = C++ 11 #include <iostream> #include <chrono> #include <thread> int main() { auto start = std::chrono::system_clock::now(); { // الشيفرة المراد اختبارها std::this_thread::sleep_for(std::chrono::seconds(2)); } auto end = std::chrono::system_clock::now(); std::chrono::duration < double > elapsed = end - start; std::cout << "Elapsed time: " << elapsed.count() << "s"; } استخدمنا في هذا المثال sleep_for لجعل الخيط النشط ينام (sleep) لفترة زمنية مُقاسة بالثواني std::chrono::seconds. حساب عدد الأيام بين تاريخين يوضّح هذا المثال كيفية حساب عدد الأيّام بين تاريخين، ويُحدَّد التاريخ بالصيغة سنة/شهر/يوم (year/month/day)، بالإضافة إلى ساعة/دقيقة/ثانية (hour/minute/second). يحسب البرنامج التالي المستوحى من موقع cppreference عدد الأيام منذ عام 2000. سننشئ بُنية std::tm من التاريخ الخام على النحو التالي: year يجب أن تكون 1900 أو أكبر. month من يناير (1 - 12). day اليوم من الشهر (1 - 31). minutes الدقائق بعد الساعة (0 - 59). seconds الثواني بعد الدقيقة (0 - 61)، و (0 - 60) منذ C++ 11. #include <iostream> #include <string> #include <chrono> #include <ctime> std::tm CreateTmStruct(int year, int month, int day, int hour, int minutes, int seconds) { struct tm tm_ret = { 0 }; tm_ret.tm_sec = seconds; tm_ret.tm_min = minutes; tm_ret.tm_hour = hour; tm_ret.tm_mday = day; tm_ret.tm_mon = month - 1; tm_ret.tm_year = year - 1900; return tm_ret; } int get_days_in_year(int year) { using namespace std; using namespace std::chrono; // نريد أن تكون النتيجة بالأيام typedef duration < int, ratio_multiply < hours::period, ratio < 24 > > ::type > days; // بداية الوقت std::tm tm_start = CreateTmStruct(year, 1, 1, 0, 0, 0); auto tms = system_clock::from_time_t(std::mktime( & tm_start)); // نهاية الوقت std::tm tm_end = CreateTmStruct(year + 1, 1, 1, 0, 0, 0); auto tme = system_clock::from_time_t(std::mktime( & tm_end)); // حساب الوقت الذي مرّ بين التاريخين auto diff_in_days = std::chrono::duration_cast < days > (tme - tms); return diff_in_days.count(); } int main() { for (int year = 2000; year <= 2016; ++year) std::cout << "There are " << get_days_in_year(year) << " days in " << year << "\n"; } هذا الدرس جزء من سلسلة دروس عن C++. ترجمة -بتصرّف- للفصل Chapter 45: Header Files والفصل Chapter 66: Date and time using header من كتاب C++ Notes for Professionals
-
تُستخدم فضاءات الأسماء (Namespaces) لمنع التضارب الذي قد يحدث عند استخدام عدّة مكتبات في نفس البرنامج، وفضاء الاسم سابقةٌ (prefix) تعريفية للدوالّ والأصناف والأنواع وغيرها. ما هي فضاءات الاسم؟ فضاء الاسم في لغة ++C هو مجموعة من كيانات C++ سواءً كانت دوالًا أو أصنافًا أو متغيرات، وتُسبق أسماءها باسم "فضاء الاسم"، وعند كتابة شيفرة ضمن فضاء الاسم فلا يلزم إسباق الكيانات المسمّاة التي تنتمي إلى فضاء الاسم باسم ذلك الفضاء، أمّا الكيانات الموجودة خارجه فيجب أن تُؤهّل أسماؤها (أي تُسبق باسم "فضاء الاسم"). الاسم المؤهّل يأخذ الشكل التالي <namespace>::<entity>. انظر الشيفرة التالية: namespace Example { const int test = 5; const int test2 = test + 12; // `Example` تعمل داخل فضاء الاسم } const int test3 = test + 3; // غير موجودة خارج فضاء الاسم `test` خطأ، لأن const int test3 = Example::test + 3; // هذا يعمل لأنّ المتغير مؤهل فضاءات الاسم مفيدة لتجميع التعريفات المرتبطة ببعضها معًا، ويمكن تشبيهها بمراكز التسوف التي تُقسّم إلى عدة متاجر يبيع كل منها سلعًا من صنف محدد، فقد يكون أحد المتاجر متخصصًأ في الإلكترونيات بينما يكون مجال الآخر في بيع الأحذية. هذا التصنيف المنطقي لأنواع المتاجر يساعد المتسوّقين على العثور على السلع التي يبحثون عنها. وبالمثل فإن فضاءات الاسم تساعد مبرمجي C++ في العثور على الدوالّ والأصناف والمتغيرات التي يبحثون عنها من خلال تنظيمها بطريقة منطقية. مثلًا: namespace Electronics { int TotalStock; class Headphones { // (color, brand, model number) وصف السماعات }; class Television { // (color, brand, model number) وصف التلفاز }; } namespace Shoes { int TotalStock; class Sandal { // (color, brand, model number) وصف الصندل }; class Slipper { // (color, brand, model number) وصف الخُف }; } لدينا فضاء اسم معرّف مسبقًا وهو فضاء الاسم العام ( global namespace)، وليس لهذا الفضاء اسم، لكن يُرمَز إليه بـ ::. مثلًا: void bar() { // مُعرّفة في فضاء الاسم العام } namespace foo { void bar() { // foo مُعرّفة في فضاء الاسم } void barbar() { bar(); // foo::bar() استدعاء ::bar(); // المُعرّفة في فضاء الاسم العام bar() استدعاء } } البحث القائم على الوسائط عند استدعاء دالّة ليس لها مؤهّلُ فضاءِ اسمٍ (namespace qualifier) صريح، فإن للمصرّف الحرية في استدعاء تلك الدالّة داخل فضاء اسم ما إن وُجد أحد أنواع المعاملات الخاصّة بتلك الدالّة في هذا الفضاء، وهذا يُسمّى "البحث القائم على الوسائط" (Argument Dependent Lookup أو ADL)، انظر: namespace Test { int call(int i); class SomeClass {...}; int call_too(const SomeClass & data); } call(5); // خطأ: اسم دالّة غير مؤهل Test::SomeClass data; call_too(data); // ناجح. تفشل call لأنّ أنواع المعاملات الخاصة بها لا تنتمي إلى فضاء الاسم Test، على عكس call_too التي تعمل بنجاح لأنّ SomeClass عضو في Test، ومن ثم فإنه مؤهّل للبحث القائم على الوسائط. ما الذي يمنع البحث القائم على الوسائط (ADL) لا يحدث البحث القائم على الوسائط إذا كان البحث العادي غير المؤهّل (normal unqualified lookup) يجد عضوًا من صنف أو دالّة مُصرَّحة في نطاق الكتلة أو كائنًا من غير نوع الدالّة. انظر: void foo(); namespace N { struct X {}; void foo(X ) { std::cout << '1'; } void qux(X ) { std::cout << '2'; } } struct C { void foo() {} void bar() { // لا تأخذ أي وسائط C::foo() خطأ: البحث القائم على الوسائط معطّل والدالّة foo(N::X{}); } }; void bar() { extern void foo(); // إعادة تعريف ::foo // لا تأخذ أي وسائط ::foo() خطأ: البحث القائم على الوسائط معطّل والدالّة foo(N::X{}); } int qux; void baz() { qux(N::X{}); // qux خطأ: إعلان المتغير يعطل البحث القائم على الوسائط بالنسبة لـ } توسيع فضاءات الاسم إحدى الميزات المفيدة لفضاءات الاسم namespace هو أنّه يمكنك توسيعها، أي إضافة أعضاء إليها. namespace Foo { void bar() {} } // أشياء أخرى namespace Foo { void bar2() {} } المُوجّه 'using' كلمة using المفتاحية لها ثلاث نكهات، وعندما نجمعها مع كلمة namespace فإننا نكتب موجِّهَ using أو (Using Directive)، فإذا لم ترد أن تكتب Foo:: قبل كلّ كيان في فضاء الاسم Foo، فيمكنك استخدام التعبير using namespace Foo; لاستيراد كل ما تحتويه Foo. انظر: namespace Foo { void bar() {} void baz() {} } // Foo::bar() ينبغي استخدام Foo::bar(); // Foo استيراد using namespace Foo; bar(); // جيد baz(); // جيد من الممكن أيضًا استيراد كيانات محدّدة في فضاء الاسم بدلاً من استيراد فضاء الاسم بأكمله: using Foo::bar; bar(); // جيد، تم استيراده بنجاح. baz(); // خطأ، لم يُستورَد. تحذير: غالبًا ما يُعدّ وضع using namespace في ملفات الترويسة من الممارسات السيئة لأنّ ذلك سيؤدي إلى استيراد فضاء الاسم في كلّ ملف يتضمن تلك الترويسة. ونظرًا لعدم وجود أيّ طريقة لتعطيل استيراد فضاء الاسم، فقد يؤدي ذلك إلى تلويث فضاء الاسم (أي حشو فضاء الاسم العام برموز كثيرة أو غير متوقعة)، أو قد يؤدّي إلى التضارب بين الأسماء. انظر المثال التالي لتوضيح هذا الأمر: /***** foo.h *****/ namespace Foo { class C; } /***** bar.h *****/ namespace Bar { class C; } /***** baz.h *****/ #include "foo.h" using namespace Foo; /***** main.cpp *****/ #include "bar.h" #include "baz.h" using namespace Bar; C c; // Foo::C و Bar::C خطأ: تضارب بين كما ترى، فلا يمكن استخدام الموجّه using في نطاق الصنف. التصريح عبر using عندما تصرّح عن كائنٍ ما باستخدام using فسيؤدّي ذلك إلى إدخال اسم سبق تصريحه في موضع آخر إلى النطاق الحالي. استيراد الأسماء فرديًّا من فضاء اسم انظر المثال التالي: #include <iostream> int main() { using std::cout; cout << "Hello, worlبd!\n"; } عند استخدام using لإدخال الاسم cout من فضاء اسم std في نطاق دالّة main، سنتمكن من الإشارة إلى الكائن std::cout بالاسم cout فقط. إعادة التصريح عن أعضاء صنف أساسي لتجنّب إخفاء الأسماء لا يُسمح بإعادة تعريف الأعضاء خلا أعضاء الصنف الأساسي عند التصريح عن أعضاء عبر using في نطاق الصنف. على سبيل المثال، using std::cout غير مسموح بها في نطاق الصنف. الاسم المُعاد تصريحه كان سيُخفى غالبًا، على سبيل المثال، تشير d1.foo في المثال أدناه إلى Derived1::foo(const char*) وحسب، لذلك سيحدث خطأ في التصريف. مسألة أنّ الدالّة Base::foo(int) ستُخفى ليس لها تأثير. لكن من الناحية الأخرى فإن d2.foo(42) صحيحة لأنّ التصريح باستخدام using يُدرِج Base::foo(int) في مجموعة الكيانات المُسمّاة foo في Derived2. ستنتهي عملية البحث عن الأسماء بإيجاد كلا الدالّتين المُسمّاتين foo، وبعد تحليل زيادة التحميل (overload resolution)، يُختار Base::foo. struct Base { void foo(int); }; struct Derived1: Base { void foo(const char * ); }; struct Derived2: Base { using Base::foo; void foo(const char * ); }; int main() { Derived1 d1; d1.foo(42); // خطأ Derived2 d2; d2.foo(42); // صحيح } وراثة المُنشئات الإصدار ≥ C++ 11 كحالة خاصة، يمكن أن يشير استخدام using للتصريح في نطاق الصنف إلى مُنشئات (constructors) صنف أبٍ مباشر (direct base class)، ثم تُورَّث تلك المُنشئات إلى الصنف المشتقّ، ويمكن استخدامها لتهيئة ذلك الصنف. انظر: struct Base { Base(int x, const char * s); }; struct Derived1: Base { Derived1(int x, const char * s): Base(x, s) {} }; struct Derived2: Base { using Base::Base; }; int main() { Derived1 d1(42, "Hello, world"); Derived2 d2(42, "Hello, world"); } في الشيفرة أعلاه، يحتوي كل من Derived1 و Derived2 على مُنشئات تعيد توجيه الوسائط مباشرةً إلى المُنشئ المقابل للصنف Base. وينفّذ الصنف Derived1 إعادة التوجيه بشكل صريح، بينما يستخدم الصنف Derived2 ميزة توريث المُنشئات الموجودة في C++ 11 لفعل ذلك ضمنيًا. إنشاء فضاءات الاسم من السهل إنشاء فضاءات الاسم، انظر المثال التالي إذ سننشئ فضاء الاسم foo ثم نصرح عن الدالة bar فيه: namespace Foo { void bar() {} } لاستدعاء bar، يجب عليك تحديد فضاء الاسم أولاً، متبوعًا بعامل تحليل النطاق (scope resolution operator) وهو ::: Foo::bar(); يُسمح بإنشاء فضاء اسمٍ داخل فضاء اسم آخر، مثلًا: namespace A { namespace B { namespace C { void bar() {} } } } الإصدار ≥ C++ 17 يمكن تبسيط الشيفرة أعلاه على النحو التالي: namespace A::B::C { void bar() {} } فضاءات الاسم غير المُسمّاة أو المجهولة يمكن استخدام فضاء اسم غير مُسمَّى (unnamed namespace) لضمان وجود ارتباط داخلي بين الأسماء التي لا يمكن الإشارة إليها إلّا بوحدة الترجمة الحالية (translation unit)، وتُعرّف فضاءات الاسم غير المسمّاة بنفس طريقة تعريف فضاءات الاسم الأخرى، ولكن من دون اسم: namespace { int foo = 42; } لن تكون foo مرئيّة إلّا في وحدة الترجمة التي تظهر فيها، ويوصى بعدم استخدام فضاءات-الاسم-غير-المُسمّاة في ملفات الترويسة (Header Files) لأنّ هذا سيعطي إصدارًا من المحتوى لكل وحدة ترجمة أُدرِج فيها، وهذا مهم جدًا خاصّة عند تعريف متغيرات-عامة-غير-ثابتة. انظر: // foo.h namespace { std::string globalString; } // 1.cpp #include "foo.h" //< تولّد: unnamed_namespace{1.cpp}::globalString ... globalString = "Initialize"; // 2.cpp #include "foo.h" //< تولّد: unnamed_namespace{2.cpp}::globalString ... std::cout << globalString; //< ستطبع دائما سلسلة نصية فارغة فضاءات الاسم المضغوطة والمتشعبة الإصدار ≥ C++ 17 انظر المثال التالي: namespace a { namespace b { template < class T > struct qualifies: std::false_type {}; } } namespace other { struct bob {}; } namespace a::b { template < > struct qualifies < ::other::bob >: std::true_type {}; } ابتداءً من C++ 17، صار بإمكانك الدخول إلى فضاء الاسم a و كذلك b بخطوة واحدة عبر التعبير namespace a::b. كنية فضاء الاسم يمكن إعطاء فضاء الاسم كُنيةً أو اسمًا بديلًا (alias)، أي اسمًا آخر يمثّل فضاء الاسم نفسه باستخدام الصيغة namespace identifier =، ويمكن الوصول إلى أعضاء فضاء الاسم المُكَنَّى عبر تأهيلهم (أي إسباقهم) بالكُنية. في المثال التالي، فضاء الاسم الُمتشعّب AReallyLongName::AnotherReallyLongName طويل جدًّا، لذا تصرح الدالّة qux عن الكُنية N. يمكن الآن الوصول إلى أعضاء فضاء الاسم باستخدام N::. انظر: namespace AReallyLongName { namespace AnotherReallyLongName { int foo(); int bar(); void baz(int x, int y); } } void qux() { namespace N = AReallyLongName::AnotherReallyLongName; N::baz(N::foo(), N::bar()); } فضاء الاسم المضمن (Inline namespace) الإصدار ≥ C++ 11 تُدرِج inline namespace محتوى فضاء الاسم المُضمّن في فضاء الاسم المحيط (Enclosing)، لذلك فإنّ الشيفرة التالية: namespace Outer { inline namespace Inner { void foo(); } } تكافئ بشكل كبير الشيفرة أدناه: namespace Outer { namespace Inner { void foo(); } using Inner::foo; } غير أن عناصر Outer::Inner:: وعناصر Outer:: متطابقة، لذلك فإن السَّطرن التالين متكافئان: Outer::foo(); Outer::Inner::foo(); لكنّ التعبير using namespace Inner; لن يكون مكافئًا لبعض الأجزاء مثل تخصيص القوالب (template specialization): #include <outer.h> // انظر أسفله class MyCustomType; namespace Outer { template < > void foo < MyCustomType > () { std::cout << "Specialization"; } } يسمح فضاء الاسم المُضمَّن بتخصيص Outer::foo، انظر المثال التالي حيث أهملنا include guard من أجل التبسيط: // outer.h namespace Outer { inline namespace Inner { template < typename T > void foo() { std::cout << "Generic"; } } } بينما لا يسمح التعبير using namespace بتخصيص Outer::foo، انظر أدناه أيضًا حيث أهملنا include guard من أجل التبسيط: // outer.h namespace Outer { namespace Inner { template < typename T > void foo() { std::cout << "Generic"; } } using namespace Inner; // `Outer::foo` لا يمكن تخصيص // `Outer::Inner::foo` الصيغة الصحيحة هي } تتيح فضاءات الاسم المُضمّنة لعدة إصدارات أن تتواجد في نفس الوقت بحيث تعود كلها افتراضيًّا إلى فضاء الاسم المُضمّن (inline): namespace MyNamespace { // تضمين الإصدار الأخير inline namespace Version2 { void foo(); // إصدار جديد void bar(); } namespace Version1 // الإصدار القديم { void foo(); } } ومع استخدام: MyNamespace::Version1::foo(); // الإصدار القديم MyNamespace::Version2::foo(); // الاصدار الجديد MyNamespace::foo(); // MyNamespace::Version1::foo(); الاصدار الافتراضي تكنية فضاء اسم طويل من الممكن تكنِية فضاءات الاسم الطويلة في ++C، وهو مفيد مثلًا في حال أردت الإشارة إلى مكوّنات مكتبة ما. انظر: namespace boost { namespace multiprecision { class Number... } } namespace Name1 = boost::multiprecision; // كلا التصريحين متكافئين boost::multiprecision::Number X // كتابة فضاء الاسم بأكمله Name1::Number Y // استخدام الكنية نطاق تصريح الكنية يتأثّر تصريح الكنية باستخدام تعليمات using الموجودة قبله: namespace boost { namespace multiprecision { class Number... } } using namespace boost; // فضاءا الاسم متكافئان namespace Name1 = boost::multiprecision; namespace Name2 = multiprecision; لكن قد تكون عمليّة تكنية فضاء الاسم مربكة أحيانًا حول اختيار الفضاء الذي تريد وضع اسم بديل أو كنية له، كما في المثال التالي: namespace boost { namespace multiprecision { class Number... } } namespace numeric { namespace multiprecision { class Number... } } using namespace numeric; using namespace boost; لا يُنصح بهذا لأنه ليس واضحًا إن كان Name1 يشير إلى numeric::multiprecision أم boost::multiprecision، نتابع: namespace Name1 = multiprecision; // يُفضل استخدام المسار الكامل namespace Name2 = numeric::multiprecision; namespace Name3 = boost::multiprecision; هذا الدرس جزء من سلسلة دروس عن C++. ترجمة -بتصرّف- للفصل Chapter 44: Namespaces والفصل Chapter 46: Using declaration من كتاب C++ Notes for Professionals
-
تُسمى الدوال المُعرّفة بالكلمة المفتاحية inline دوالًا مُضمّنة (inline functions)، ويمكن تعريفها أكثر من مرة دون انتهاك قاعدة التعريف الواحد (One Definition Rule)، وعليه يمكن تعريفها في الترويسة مع الارتباطات الخارجية. التصريح عن دالّة ما بأنها مضمّنة يُخبر المصرّف أنّ تلك الدالّة يجب أن تُضمَّن أثناء توليد الشِّيفرة البرمجية (لكنّ ذلك غير مضمون). الدوال المضمنة تعريف الدوال غير التابعة المضمنة (Non-member inline function definition) inline int add(int x, int y) { return x + y; } الدوال التابعة المضمنة (Member inline functions) انظر المثال التالي: // header (.hpp) struct A { void i_am_inlined() {} }; struct B { void i_am_NOT_inlined(); }; // source (.cpp) void B::i_am_NOT_inlined() {} ما المقصود بتضمين دالّة؟ نظر المثال التالي: inline int add(int x, int y) { return x + y; } int main() { int a = 1, b = 2; int c = add(a, b); } في الشيفرة أعلاه، عندما تكون add مضمّنة، فستصبح الشيفرة الناتجة كما يلي: int main() { int a = 1, b = 2; int c = a + b; } لا يمكن رؤية الدالة المضمّنة إذ يُضمَّن متنها في متن المُستدعي، ولو لم يكن التابع add مضمّنًا، لاستدعيَت الدالّة. الحِمل الإضافي (overhead) الناتج عن استدعاء دالة -مثل إنشاء إطار مكدِّس (stack frame) جديدة، ونسخ الوسائط، وإنشاء المتغيرات المحلية، وغير ذلك - سيكون مكلفًا. التصريح عن الدوال المضمنة غير التابعة يمكن التصريح عن الدوالّ المضّمنة غير التابعة على النحو التالي: inline int add(int x, int y); الدوال التابعة الخاصة المنشئ الافتراضي (Default Constructor) المُنشئ الافتراضي هو نوع من المُنشئات التي لا تتطلّب أيّ معاملات عند استدعائها وتُسمى باسم النوع الذي تنشئه، كما أنها تُعدّ دالة تابعة من توابعه، كما هو حال جميع المُنشئات. class C { int i; public: // تعريف المنشئ الافتراضي C(): i(0) { // إلى 0 i قائمة تهيئة العضو، هيئ // إنشاء جسم الدالّة -- يمكن فعل أشياء أكثر تعقيدا هنا } }; C c1; C c2 = C(); C c3(); C c4 {}; C c5[2]; C * c6 = new C[2]; تحليل الشيفرة السابقة: C c1: يستدعي منشئ C الافتراضي لإنشاء الكائن c1. C c2: يستدعي المنشئ الافتراضي صراحة. C c3: خطأ: هذا الإصدار غير ممكن بسبب غموض في التحليل، أو ما يعرف بمشكلة “The most vexing parse”. C c4: يمكن استخدام { } في C++ 11 بطريقة مماثلة. C c5[2]: يستدعي المنشئ الافتراضي على عنصري المصفوفة. C * c6: يستدعي المنشئ الافتراضي على عنصري المصفوفة. هناك طريقة أخرى للاستغناء عن تمرير المعاملات، وهي أن يوفّر المطوِّر قيمًا افتراضية لها جميعًا: class D { int i; int j; public: // من الممكن أيضا استدعاء منشئ افتراضي بدون معاملات D(int i = 0, int j = 42): i(i), j(j) {} }; D d; // مع القيم الافتراضية للوسائط D استدعاء منشئ يوفر المصرِّف منشئًا افتراضيًا فارغًا بشكل ضمني في بعض الحالات، كأن يكون المطور لم يوفر منشئًا ولم تكن ثمة شروط مانعة أخرى. class C { std::string s; // تحتاج الأعضاء إلى أن تكون قابلة للإنشاء }; C c1; // لها منشئ افتراضي معرّف ضمنيا C وجود نوع آخر من المنشئ هو أحد الشروط المانعة المذكورة سابقًا: class C { int i; public: C(int i): i(i) {} }; C c1; // ليس لها منشئ افتراضي ضمني C ،خطأ في التصريف الإصدار <C++ 11 إحدى الطرق الشائعة لمنع إنشاء مُنشئ افتراضي ضمني هي جعله خاصًّا private (بلا تعريف)، والهدف من ذلك هو إطلاق خطأ تصريفي في حال حاول شخص ما استخدام المنشئ، وينتج عن هذا إمّا خطأ في الوصول (Access to private error) أو خطأ ربط (linker error)، حسب نوع المصرّف. وللتأكد من أنّ مُنشئًا افتراضيًّا (مشابه وظيفيًا للمنشئ الضمني) قد تمّ تعريفه، يكتب المُطوِّر منشئًا فارغًا بشكل صريح. الإصدار C++ 11 في الإصدار C++ 11، يستطيع المطوّر أيضًا استخدام الكلمة المفتاحية delete لمنع المصرّف من توفير مُنشئ افتراضي. class C { int i; public: // يُحذف المنشئ الافتراضي بشكل صريح C() = delete; }; C c1; // C خطأ تصريفي: حذف منشئ أيضًا، إن أردت أن يوفرّ المصرّف مُنشئًا افتراضيًّا، فذلك يكون على النحو التالي: class C { int i; public: // توفير منشئ افتراضي تلقائيا C() = default; C(int i): i(i) {} }; C c1; // مُنشأة افتراضيا C c2(1); // int مُنشأة عبر المنشئ الإصدار C++ 14 يمكنك تحديد ما إذا كان لنوعٍ ما مُنشئ افتراضي (أو أنه نوع أولي - primitive type) باستخدام std::is_default_constructible from <type_traits> إليك الشيفرة: class C1 {}; class C2 { public: C2() {} }; class C3 { public: C3(int) {} }; using std::cout; using std::boolalpha; using std::endl; using std::is_default_constructible; cout << boolalpha << is_default_constructible < int > () << endl; // true cout << boolalpha << is_default_constructible < C1 > () << endl; // true cout << boolalpha << is_default_constructible < C2 > () << endl; // true cout << boolalpha << is_default_constructible < C3 > () << endl; // false الإصدار = C++ 11 في الإصدار C++ 11، من الممكن استخدام الإصدار غير الدّالي (non-functor) لـ std::is_default_constructible: cout << boolalpha << is_default_constructible<C1>::value << endl; // true المُدمِّر (Destructor) المُدمّر هو دالّة بدون وسائط تُستدعى قُبيْل تدمير كائن مُعرّف من المستخدم (user-defined object)، ويُسمّى باسم النوع الذي يدمِّره مسبوقًا بـ ~ . class C { int* is; string s; public: C(): is(new int[10]) {}~C() { // تعريف المُدمّر delete[] is; } }; class C_child: public C { string s_ch; public: C_child() {}~C_child() {} // مدمّر الصنف الفرعي }; void f() { C c1; C c2[2]; C* c3 = new C[2]; C_child c_ch; delete[] c3; } // يتم تدمير المتغيرات التلقائية هنا تحليل الشيفرة السابقة: C c1: استدعاء المدمر الافتراضي. [C c2[2: استدعاء المُدمّر الافتراضي على العنصرين. [C* c3 = new C[2: استدعاء المُدمّر الافتراضي على عنصرَي المصفوفة C_child c_ch: عند تدميره يستدعي مدمر s_ch من قاعدة C (ومن ثم، s) delete[] c3: يستدعي المدمرات على [c3[0 و [c3[1. يوفر المصرِّف مدمرًا افتراضيًا بشكل ضمني في بعض الحالات، كأن يكون المطور لم يوفر مدمرًا ولم تكن ثمة شروط مانعة أخرى. class C { int i; string s; }; void f() { C* c1 = new C; delete c1; // له مُدمّر C } class C { int m; private: ~C() {} // لا يوجد مُدمّر عام }; class C_container { C c; }; void f() { C_container* c_cont = new C_container; delete c_cont; // Compile ERROR: C has no accessible destructor } الإصدار > C++ 11 في C++ 11، يستطيع المُطوِّر تغيير هذا السلوك عن طريق منع المُصرّف من توفير مدمِّر افتراضي. class C { int m; public: ~C() = delete; // لا يوجد مُدمّر ضمني }; void f { C c1; } // Compile ERROR: C has no destructor يمكنك أن تجعل المصرّف يوفّر مدمّرًا افتراضيًّا، انظر: class C { int m; public: ~C() = default; }; void f() { C c1; } // بنجاح c1 لها مُدمّر وقد تم تدمير C الإصدار> C++ 11 يمكنك تحديد ما إذا كان لنوعٍ ما مدمّرٌ ما (أو أنّه نوع أولي) باستخدام std::is_destructible من <type_traits>: class C1 {}; class C2 { public: ~C2() = delete }; class C3: public C2 {}; using std::cout; using std::boolalpha; using std::endl; using std::is_destructible; cout << boolalpha << is_destructible < int > () << endl; // true cout << boolalpha << is_destructible < C1 > () << endl; // true cout << boolalpha << is_destructible < C2 > () << endl; // false cout << boolalpha << is_destructible < C3 > () << endl; // false النسخ والمبادلة (Copy and swap) إذا أردت كتابة صنف لإدارة الموارد فستحتاج إلى تنفيذ جميع الدوال التابعة الخاصّة (انظر قاعدة الثلاثة/خمسة/صفر). والطريقة الأبسط لكتابة مُنشئ النسخ (copy constructor) وعامل الإسناد (assignment operator) هي: person(const person &other): name(new char[std::strlen(other.name) + 1]), age(other.age) { std::strcpy(name, other.name); } person& operator = (person const& rhs) { if (this != &other) { delete[] name; name = new char[std::strlen(other.name) + 1]; std::strcpy(name, other.name); age = other.age; } return *this; } لكنّ هذه المقاربة تعتريها بعض العيوب، فهي تفشل في ضمان الاعتراض القوي (strong exception guarantee) إذا أطلق new[] فسنكون قد محونا الموارد التي يملكها this سلفًا، ولن نستطيع استردادها. وهناك الكثير من التكرار في شيفرتي إنشاء النسخ (copy construction) وإسناد النسخ (copy assignment)، ولا ننسى كذلك التحقّق من الإسناد الذاتي (self-assignment) الذي يضيف حِملًا زائدًا إلى عملية النسخ عادة، لكنّه يبقى ضروريًّا. يمكننا استخدام أسلوب النسخ والمبادلة (copy-and-swap idiom) لتلبية ضمان الاستثناء القوي (strong exception guarantee) وتجنّب تكرار الشيفرة: class person { char* name; int age; public: /* كل الدوالّ الأخرى */ friend void swap(person & lhs, person & rhs) { using std::swap; // enable ADL swap(lhs.name, rhs.name); swap(lhs.age, rhs.age); } person & operator = (person rhs) { swap( *this, rhs); return *this; } }; إن عجبت من سبب نجاح الشيفرة السابقة، فانظر ما سيحدث حين يكون لدينا ما يلي: person p1 = ...; person p2 = ...; p1 = p2; أولاً، ننسخ rhs من p2 (والذي لم يكن علينا تكراره هنا). لن يكون علينا فعل أيّ شيء في operator= في حال رفع اعتراض (throwing an exception) وسيبقى p1 دون تغيير. بعد ذلك، سنُبادل الأعضاء بين *this و rhs، ثم سيخرُج rhs عن النطاق. ستُنظّف المواردَ الأصلية الخاصّة بالمؤشّر this ضمنيًا داخل العامل operator=من خلال المدمّر، والذي لم يكن علينا أن نكرره. حتى الإسناد الذاتي (self-assignment) سيعمل بلا مشاكل - لكنّه سيكون أقل كفاءة باستخدام النسخ والمبادلة (إذ يتطلّب تخصيصًا - allocation - وإلغاء تخصيص - deallocation - إضافي)، ولكن إذا كان هذا الأمر مستبعدًا، فلن يؤثّر على الأداء العامّ. الإصدار ≥ C++ 11 تعمل الصيغة أعلاه كما هي بالنسبة لإسناد النقل (move assignment). p1 = std::move(p2); هنا، ننقل/ننشئ rhs من p2، وتبقى الشيفرة الأخرى صالحة، فإذا كان صنفٌ ما قابلًا للنقل لكن غير قابل للنسخ، فلا داعي لحذف إسناد النسخ، لأنّ عامل الإسناد سيكون معطوبَ الصياغة بسبب مُنشئ النسخ المحذوف. النقل والنسخ الضمني اعلم أنّ التصريح عن مدمِّر يمنع المصرّف من إنشاء مُنشئات النقل (move constructors) وعوامل إسناد النقل (move assignment operators) ضمنيًّا، وتذكر في حال صرّحت بمدمّر أن تضيف التعريفات المناسبة لعمليات النقل. وسيؤدّي التصريح عن عمليات النقل إلى منع إنشاء عمليات النسخ، لذا يجب إضافتها هي أيضًا (إذا كانت كائنات هذا الصنف تحتاج إلى عمليات النسخ). class Movable { public: virtual~Movable() noexcept = default; // المنشئ لن ينشئ هذا التعبير الخاطئ لأننا عرّفنا مُدمّرا Movable(Movable && ) noexcept = default; Movable & operator = (Movable && ) noexcept = default; // التصريح عن عمليات النقل سيمنع إنشاء عمليات النسخ إلا إن قمنا بتمكين ذلك صراحة Movable(const Movable& ) = default; Movable & operator = (const Movable& ) = default; }; الدوال التابعة غير الساكنة (Non-static member functions) من الممكن أن يكون للأصناف (class) والبنيات (struct) دوال تابعة أو متغيرات عضوية، وصياغة الدوالّ التابعة تشبه صياغة الدوالّ المستقلة، ويمكن تعريفها إمّا داخل الصنف أو خارجه، فإذا عُرِّفت خارج تعريف الصنف فإنّ اسم الدالّة سيُسبَق باسمِ الصنف ومعامل النطاق (::). class CL { public: void definedInside() {} void definedOutside(); }; void CL::definedOutside() {} تُستدعى هذه الدوالّ على نسخة (أو مرجع إلى نسخة) من الصنف باستخدام العامل النُّقَطي (dot operator - (.))، أو مؤشّر إلى نسخة باستخدام عامل السهم (arrow operator - (->))، ويرتبط كل استدعاء بالنسخة التي استُدعِيت عليها الدالّة. وعندما يُستدعى تابع على نسخة فسيكون له حقّ الوصول إلى كافّة حقول تلك النسخة (عبر المؤشّر this)، ولكن لا يمكنه الوصول إلى حقول النسخ الأخرى إلا إذا مُرِّرت كمعاملات. struct ST { ST(const std::string & ss = "Wolf", int ii = 359): s(ss), i(ii) {} int get_i() const { return i; } bool compare_i(const ST & other) const { return (i == other.i); } private: std::string s; int i; }; ST st1; ST st2("Species", 8472); int i = st1.get_i(); // st2.i ولكن ليس إلى st1.i يمكن الوصول إلى bool b = st1.compare_i(st2); // st1 و st2 يمكن الوصول إلى يُسمح لهذه الدوالّ بالوصول إلى الحقول و/أو التوابع الأخرى بغضّ النظر عن مُحدِّدات الوصول (access modifiers) الخاصّة بالمتغيرات والتوابع. كذلك يمكن كتابتها والوصول إلى الحقول و/أو استدعاء التوابع المُصرّحة قبلها، إذ يجب تحليل تعريف الصنف بأكمله قبل أن يبدأ المصرّف في تصريفه. class Access { public: Access(int i_ = 8088, int j_ = 8086, int k_ = 6502): i(i_), j(j_), k(k_) {} int i; int get_k() const { return k; } bool private_no_more() const { return i_be_private(); } protected: int j; int get_i() const { return i; } private: int k; int get_j() const { return j; } bool i_be_private() const { return ((i > j) && (k < j)); } }; التغليف (Encapsulation) هُنالك عدّة استعمالات للتوابع، من ذلك أنها تُستخدم للتغليف (encapsulation)، وذلك باستخدام جالِب (getter) ومُعيِّن (setter) بدلاً من السماح بالوصول إلى الحقول مباشرة. class Encapsulator { int encapsulated; public: int get_encapsulated() const { return encapsulated; } void set_encapsulated(int e) { encapsulated = e; } void some_func() { do_something_with(encapsulated); } }; يمكن الوصول إلى الحقل encapsulated داخل الصنف من قِبل أيّ دالة تابعة غير ساكنة، أمّا خارج الصنف، فيُنظّم حق الوصول إليه بواسطة الدوال التابعة إذ يُستخدم get_encapsulated() لقراءته و set_encapsulated() لتعديله، هذا يمنع التعديلات غير المقصودة على المتغيّر (هناك العديد من النقاشات حول ما إذا كانت الجوالب والمعيّنات تدعم التغليف أم تكسره، ولكلٍّ من الطرفين وجهة نظرة وجيهة). إخفاء الأسماء واستيرادها عندما يوفّر صنف أساسي مجموعة من الدوالّ زائدة التحميل (overloaded functions)، ثم يضيف صنفٌ مشتق تحميلًا زائدًا آخر إلى المجموعة، فإنّ ذلك سيخفي كل التحميلات الزائدة الخاصّة بالصنف الأساسي. struct HiddenBase { void f(int) { std::cout << "int" << std::endl; } void f(bool) { std::cout << "bool" << std::endl; } void f(std::string) { std::cout << "std::string" << std::endl; } }; struct HidingDerived: HiddenBase { void f(float) { std::cout << "float" << std::endl; } }; // ... HiddenBase hb; HidingDerived hd; std::string s; hb.f(1); // الخرج: int hb.f(true); // الخرج: bool hb.f(s); // الخرج: std::string; hd.f(1. f); // الخرج: float hd.f(3); // الخرج: float hd.f(true); // الخرج: float hd.f(s); // Error: Can't convert from std::string to float. هذا السلوك ناتج عن قواعد تحليل الاسم (name resolution rules): فأثناء البحث عن الاسم، يتوقف البحث بمجّرد العثور على الاسم الصحيح حتى لو لم يُعثَر على الإصدار الصحيح للكيان الذي يحمل ذلك الاسم (مثل hd.f(s))، ونتيجة لهذا، تؤدي زيادة تحميل دالّة في الصنف المشتق إلى منع الوصول إلى التحميل الزائد الموجود في الصنف الأساسي. ولكي لتجنّب ذلك، يمكن استخدام using لأجل "استيراد" الأسماء من الصنف الأساسي إلى الصنف المشتق حتى تكون متاحة أثناء البحث عن الاسم. انظر المثال التالي حيث يجب أن تُعد جميع الأعضاء المسمّاة HiddenBase::f أعضاءً من HidingDerived أثناء البحث: struct HidingDerived: HiddenBase { using HiddenBase::f; void f(float) { std::cout << "float" << std::endl; } }; // ... HidingDerived hd; hd.f(1. f); // الخرج: float hd.f(3); // الخرج: int hd.f(true); // الخرج: bool hd.f(s); // الخرج: std::string إذ كان الصنف المشتق يستورد الأسماء باستخدام using ولكن يُصرّح كذلك عن دوالّ لها نفس بصمات الدوالّ في الصنف الأساسي، فسيُعاد تعريف دوالّ الصنف الأساسي بصمت أو تُخفى. struct NamesHidden { virtual void hide_me() {} virtual void hide_me(float) {} void hide_me(int) {} void hide_me(bool) {} }; struct NameHider: NamesHidden { using NamesHidden::hide_me; void hide_me() {} // NamesHidden::hide_me() إعادة تعريف void hide_me(int) {} // NamesHidden::hide_me(int) إخفاء }; يمكن أيضًا استخدام تصريح using لتغيير معدِّلات الوصول (Access Modifiers)، بشرط أن يكون الكيان المستورد إمّا عامًّا (public) أو محميًا (protected) في الصنف الأساسي. struct ProMem { protected: void func() {} }; struct BecomesPub: ProMem { using ProMem::func; }; // ... ProMem pm; BecomesPub bp; pm.func(); // خطأ: محميّ bp.func(); // جيد وبالمثل إذا أردنا استدعاء دالة تابعة من صنف محدد في التسلسل الهرمي الوراثي (inheritance hierarchy) بشكل صريح، فيمكننا تأهيل اسم الدالّة عند استدعائها، وتحديد ذلك الصنف بالاسم. struct One { virtual void f() { std::cout << "One." << std::endl; } }; struct Two: One { void f() override { One::f(); // this->One::f(); std::cout << "Two." << std::endl; } }; struct Three: Two { void f() override { Two::f(); // this->Two::f(); std::cout << "Three." << std::endl; } }; // ... Three t; t.f(); t.Two::f(); t.One::f(); } تحليل الشيفرة السابقة: ()t.f: الصيغة العادية. ()t.Two::f: استدعاء إصدار ()f المعرَّف في Two. ()t.One::f: استدعاء إصدار ()f المعرَّف في One. الدوال التابعة الوهميّة الدوال التوابع يمكن أن تكون وهمية (virtual)، وإن استُدعِيت على مؤشّر أو مرجع إلى نسخة فلن يتم الوصول إليها مباشرة، بل سيُبحَث عن الدالّة في جدول الدوالّ الوهمية (قائمة من المؤشّرات-إلى-الدوال التابعة التي تشير إلى الدوالّ الوهمية، والمعروفة باسم vtable أو vftable)، ثم تُستخدَم لاستدعاء الإصدار المناسب للنوع (الفعلي) الديناميكي للنسخة. ولن يتم أي بحث إذا استُدعيت الدالّة مباشرة من متغير داخل صنف ما. struct Base { virtual void func() { std::cout << "In Base." << std::endl; } }; struct Derived: Base { void func() override { std::cout << "In Derived." << std::endl; } }; void slicer(Base x) { x.func(); } // ... Base b; Derived d; Base * pb = & b, * pd = & d; // مؤشّرات Base & rb = b, & rd = d; // مراجع int main() { b.func(); // الخرج: In Base. d.func(); // الخرج: In Derived. pb -> func(); // الخرج: In Base. pd -> func(); // الخرج: In Derived. rb.func(); // الخرج: In Base. rd.func(); // الخرج: In Derived. slicer(b); // الخرج: In Base. slicer(d); // الخرج: In Base. } ورغم أن pd من النوع Base* وrd من النّوع Base&، إلا أن استدعاء func() على أيّ منهما سيؤدّي إلى استدعاء Derived::func() بدلاً من Base::func()؛ وذلك لأنّ جدول vtable الخاصّ بالبنية Derived يُحدِّث المدخل Base::func() بدلاً من الإشارة إلى Derived::func(). ومن ناحية أخرى، لاحظ كيف يؤدّي تمرير نسخة إلى slicer() إلى استدعاء Base::func() حتى عندما تكون النسخة المُمرَّرة من نوع Derived، وهذا بسبب مفهوم يُعرف بتشريح البيانات (Data Slicing)، وفيه يؤدّي تمرير نسخة من Derived إلى مُعامل من النوع Base بالقيمة (by value) إلى عرض ذلك الجزء من Derived الذي يمكنه الوصول إلى نسخة Base. حين تُعرَّف دالة تابعة على أنّها وهمية فإنّ جميع دوال الصنف التابعة المشتقة التي تحمل نفس البصمة ستعيد تعريفه (override) بغضّ النظر عمّا إذا كانت الدالّة التي أعادت تعريفه وهمية أم لا، هذا سيُصعِّب عملية تحليل الأصناف المشتقّة على المبرمجين لعدم وجود أيّ إشارة تحدّد أيٌّ من تلك دوالّ وهمية. struct B { virtual void f() {} }; struct D: B { void f() {} // B::f وهمية بشكل ضمني، إعادة تعريف // B لكن عليك التحقق من }; لاحظ أنّ الدالّة مشتقة (Derived Function) لا يمكنها أن تعيد تعريف دالّة أساسية (Base Function) إلا إذا تطابقت بصماتهما، حتى لو صُرِّح بأنّ الدالّة المشتقة وهمية (virtual)، فستنشئ دالّةً وهمية جديدة إن لم لتطابق البصمات. struct BadB { virtual void f() {} }; struct BadD: BadB { virtual void f(int i) {} // BadB::f لا تُعِد تعريف }; الإصدار ≥ C++ 11 اعتبارًا من الإصدار C++ 11، يمكن التصريح بنِيَّة إعادة التعريف (override) باستخدام الكلمة المفتاحية override، واعلم أن هذه الكلمة حساسة للسياق، وسيخبر ذلك المصرّف أنّ المبرمج يتوقع منه أن يعيد تعريف دالّة الصنف الأساسي، وعليه يطلق المصرِّف خطأً إذا لم تحدث عملية إعادة التعريف. struct CPP11B { virtual void f() {} }; struct CPP11D: CPP11B { void f() override {} void f(int i) override {} // Error: Doesn't actually override anything. }; وفائدة هذا أنه يخبر المبرمجين بأنّ الدالّة وهمية، وأنها كذلك مُصرَّحة في صنف أساسي واحد على الأقل، مما يسهّل تحليل الأصناف المعقّدة. يجب تضمين المُحدِّد virtual في تصريح الدالّة وعدم تكراره في التعريف عند التصريح بأنّ دالّة ما وهمية virtual وتكون معرَّفة خارج تعريف الصنف. الإصدار ≥ C++ 11 ينطبق هذا أيضًا على الكلمة المفتاحية override. struct VB { virtual void f(); // هنا "virtual" ضع void g(); }; /* virtual */ void VB::f() {} // لكن ليس هنا virtual void VB::g() {} // خطأ وإن نفذ الصنف الأساسي زيادة تحميل على دالّة وهمية، فإن التحميلات التي حُدِّدَت على أنها وهمية بشكل صريح هي وحدها التي ستكون وهمية. struct BOverload { virtual void func() {} void func(int) {} }; struct DOverload: BOverload { void func() override {} void func(int) {} }; // ... BOverload* bo = new DOverload; bo - > func(); // DOverload::func() استدعاء bo - > func(1); // BOverload::func(int) استدعاء الثباتية (Const Correctness) أحد الاستخدامات الرئيسية لمؤهلات المؤشّر this هو الصحة الثباتية، أو الثباتية باختصار (const correctness). تضمن هذه الممارسة عدم تعديل الكائن إلا عند الحاجة لهذا، وأنّ أيّ دالّة (عضوة أو غير عضوة) لا تحتاج إلى تعديل كائن ما لن تملك حق الكتابة في ذلك الكائن (سواء بشكل مباشر أو غير مباشر). هذا يمنع التعديلات غير المقصودة مما يجعل الشيفرة أكثر متانة، كما يسمح لأيّ دالّة لا تحتاج إلى تعديل الحالة لأن تقبل الكائنات سواء كانت ثابتة (const) أو غير ثابتة دون الحاجة إلى إعادة كتابة أو تحميل الدالّة. تبدأ الثباتية من أسفل إلى أعلى، وذلك بسبب طبيعتها، إذ تُصرَّح أيّ دالة تابعة في الصنف لا تحتاج إلى تغيير الحالة على أنها ثابتة (const)، وذلك كي يمكنَ استدعاؤها على النسخ الثابتة. هذا يسمح بدوره بالتصريح أنّ المُعاملات المُمرّرة بالمرجع (passed-by-reference) ثابتة عندما لا تكون هناك حاجة إلى تعديلها، ممّا يسمح للدوالّ بأخذ كائنات ثابتة أو غير ثابتة دون مشاكل، كما يمكن للثباتيّة أن تنتشر للخارج بهذه الطريقة. وتكون الجالبات (Getters) ثابتة كأي دالّة أخرى لا تحتاج إلى تعديل حالة الكائن المنطقية. class ConstIncorrect { Field fld; public: ConstIncorrect(const Field & f): fld(f) {} // تعديل const Field & get_field() { return fld; } // لا يوجد تعديل، ينبغي أن تكون ثابتة void set_field(const Field & f) { fld = f; } // تعديل void do_something(int i) { // تعديل fld.insert_value(i); } void do_nothing() {} // لا يوجد تعديل، ينبغي أن تكون ثابتة }; class ConstCorrect { Field fld; public: ConstCorrect(const Field & f): fld(f) {} // غير ثابتة: يمكن التعديل const Field & get_field() const { return fld; } // ثابتة: لا يمكن التعديل void set_field(const Field & f) { fld = f; } // غير ثابتة: يمكن التعديل void do_something(int i) { // غير ثابتة: يمكن التعديل fld.insert_value(i); } void do_nothing() const {} // ثابتة: لا يمكن التعديل }; // ... const ConstIncorrect i_cant_do_anything(make_me_a_field()); Field f = i_cant_do_anything.get_field(); // ليست ثابتة get_field() ،خطأ i_cant_do_anything.do_nothing(); // خطأ كالأعلى const ConstCorrect but_i_can(make_me_a_field()); Field f = but_i_can.get_field(); //جيد but_i_can.do_nothing(); // جيد كما هو موضّح في تعليقات ConstIncorrect و ConstCorrect، فإنّ تأهيل الدوالّ يمكن أن يُستخدم في التوثيق. يمكن افتراض أن أي دالة غير ثابتة ستغير الحالة إذا كان صنفُ ما صحيحًا وفق مفهوم الثبات،وكذلك أيّ دالّة ثابتة لن تغيّر الحالة. الدوال التابعة الثابتة للأصناف يوضّح المثال التالي مفهوم الدوال التابعة الثابتة: #include <iostream> #include <map> #include <string> using namespace std; class A { public: map<string, string> * mapOfStrings; public: A() { mapOfStrings = new map<string, string>(); } void insertEntry(string const & key, string const & value) const { (*mapOfStrings)[key] = value; // هذا يعمل بنجاح. delete mapOfStrings; // وهذا أيضًا. mapOfStrings = new map<string, string>(); // أما هذا فلا يعمل. } void refresh() { delete mapOfStrings; mapOfStrings = new map<string, string>(); // ليست دالة ثابتة refresh يعمل لأن. } void getEntry(string const & key) const { cout << mapOfStrings->at(key); } }; int main(int argc, char* argv[]) { A var; var.insertEntry("abc", "abcValue"); var.getEntry("abc"); getchar(); return 0; } هذا الدرس جزء من سلسلة دروس عن C++. ترجمة -بتصرّف- للفصول: Chapter 39: Inline functions Chapter 40: Special Member Functions Chapter 41: Non-Static Member Functions Chapter 42: Constant class member functions من كتاب C++ Notes for Professionals
-
الدوال الوهمية النهائية (Final virtual functions) قدّمت C++ 11 المُحدِّد final الذي يمنع إعادة تعريف (overriding) تابع في حال ظهر في بصمته (signature): class Base { public: virtual void foo() { std::cout << "Base::Foo\n"; } }; class Derived1: public Base { public: // Base::foo تخطي void foo() final { std::cout << "Derived1::Foo\n"; } }; class Derived2: public Derived1 { public: // Compilation error: cannot override final method virtual void foo() { std::cout << "Derived2::Foo\n"; } }; لا يمكن استخدام المُحدّد final إلا مع دالة تابعة وهمية (virtual)، ولا يمكن تطبيقه على الدوال التابعة غير الوهمية. وكما في حال final، فهناك أيضًا مُحدِّدٌ يُسمّى override، وهو يمنع تخطي الدوالّ الوهمية في الصنف المشتق. كذلك يمكن دمج المُحدِّدين override و final معًا على النحو التالي: class Derived1: public Base { public: void foo() final override { std::cout << "Derived1::Foo\n"; } }; استخدام override و virtual معًا في C++ 11 والإصدارات الأحدث يكون للمحدّد override معنى خاصًّا في الإصدار C++ 11 وما بعده عندما يُلحَق بنهاية بصمة الدالّة، فهو يشير إلى أن الدّالة: تتخطى الدالّةَ الحالية في الصنف الأساسي (base class)، وأنّ … دالّة الصنف الأساسي وهمية virtual. الهدف الأساسي من هذا المُحدّد هو توجيه المُصرّف، يوضّح المثال أدناه التغيّر في السلوك في حال استخدام override وفي حال عدم استخدامها: عند عدم استخدام override #include <iostream> struct X { virtual void f() { std::cout << "X::f()\n"; } }; ()Y::f لن تتخطى ()x::f لأن لها بصمة مختلفة، لكن سيقبل المصرِّفُ الشيفرة ويتجاهل ()Y::f بصمت. انظر: struct Y: X { virtual void f(int a) { std::cout << a << "\n"; } }; مع override: #include <iostream> struct X { virtual void f() { std::cout << "X::f()\n"; } }; سينبهك المصرِّف إلى حقيقة أن ()Y::f لا تتخطى شيئًا. struct Y: X { virtual void f(int a) override { std::cout << a << "\n"; } }; لاحظ أنّ override ليست كلمة مفتاحية، بل مُعرِّف خاص لا يظهر إلا في بصمات الدوالّ، ويمكن استخدام ذلك المعرِّف في جميع السياقات الأخرى: void foo() { int override = 1; // حسنا int virtual = 2; // Compilation error: keywords can't be used as identifiers. } الدوال التابعة الوهمية وغير الوهمية مع الدوال التابعة الوهمية: #include <iostream> struct X { virtual void f() { std::cout << "X::f()\n"; } }; سيكون تحديد virtual هنا اختياريًا لأنها يمكن أن تُستنتج من ()X::f، انظر: struct Y: X { virtual void f() { std::cout << "Y::f()\n"; } }; void call(X & a) { a.f(); } int main() { X x; Y y; call(x); // يكون خرجها: "X::f()" call(y); // يكون خرجها: "Y::f()" } بدون الدوال التابعة الوهمية: #include <iostream> struct X { void f() { std::cout << "X::f()\n"; } }; struct Y: X { void f() { std::cout << "Y::f()\n"; } }; void call(X & a) { a.f(); } int main() { X x; Y y; call(x); // يكون خرجها: "X::f()" call(y); // يكون خرجها: "X::f()" } سلوك الدوال الوهمية في المنشئات والمدمرات قد يكون سلوك الدّوالّ الوهمية في المنشِئات (constructors) والمدمّرات (destructors) مربكًا للوهلة الأولى. #include <iostream> using namespace std; class base { public: base() { f("base constructor"); }~base() { f("base destructor"); } virtual const char * v() { return "base::v()"; } void f(const char * caller) { cout << "When called from " << caller << ", " << v() << " gets called.\n"; } }; class derived: public base { public: derived() { f("derived constructor"); }~derived() { f("derived destructor"); } const char * v() override { return "derived::v()"; } }; int main() { derived d; } الناتج: عندما تُستدعى من مُنشئ أساسي (base constructor)، ستُستدعى base::v(). عندما تُستدعى من مُنشئ مشتق (derived constructor)، سيُستدعى مشتقّ derived::v(). عندما تُستدعى من مدمِّر مشتق، سيُستدعى derived::v(). عندما تُستدعى من مدمِّر أساسي، سيُستدعى base::v(). السبب في هذا هو أنّ الصنف المشتق ربّما يُعرِّف أعضاءً إضافيين لم تتم تهيئتهم بعد (في حالة المُنشئ) أو سبق حذفهم (في حالة المُدمّر)، ما يجعل استدعاء توابعه غير آمن. لذا يكون النوع الديناميكي لـ this* أثناء إنشاء وتدمير كائنات C++ هو صنف المنشئ أو المدمِّر، وليس الصنف المشتق. انظر المثال التالي: #include <iostream> #include <memory> using namespace std; class base { public: base() { std::cout << "foo is " << foo() << std::endl; } virtual int foo() { return 42; } }; class derived: public base { unique_ptr < int > ptr_; public: derived(int i): ptr_(new int(i * i)) {} لا يمكن استدعاء ما يلي قبل derived::derived بسبب طريقة عمل ++C: int foo() override { return *ptr_; } }; int main() { derived d(4); } الدوال الوهمية الخالصة (Pure virtual functions) تستطيع جعل دالّةً "دالّةً وهمية خالصة" (أو مُجرّدة) عبر إلحاق = 0 بالتصريح، وتعدُّ الأصناف التي تحتوي على دالّة وهمية خالصة واحدة على الأقل أصنافًا مجرّدة، ولا يمكن إنشاء نسخ منها؛ ولا يمكن كذلك إنشاء نسخ من الأصناف المشتقة منها إلا إن كانت تعرّف، أو ترث تعريفات، كل الدوالّ الوهمية الخالصة. struct Abstract { virtual void f() = 0; }; struct Concrete { void f() override {} }; Abstract a; // خطأ Concrete c; //جيد حتى لو عُرِّفت دالّة على أنّها وهمية خالصة، فمن الممكن أن تُعطى تطبيقًا (implementation) افتراضيًّا، لكنّ هذا لن يغيّر حقيقة أنها مجرّدة، وسيكون على الأصناف المشتقّة أن تعرّفها قبل أن تنشئ منها نسخًا، وفي هذه الحالة يُسمح لإصدار الصنف المشتق من الدالّة باستدعاء إصدار الصنف الأساسي. struct DefaultAbstract { virtual void f() = 0; }; void DefaultAbstract::f() {} struct WhyWouldWeDoThis: DefaultAbstract { void f() override { DefaultAbstract::f(); } }; هناك بعض الأسباب التي تجعلنا نرغب في فعل ذلك: إذا أردنا إنشاء صنف لا يمكن استنساخه لكنّه لا يمنع الأصناف المشتقة منه من أن تُستنسَخ، نستطيع أن نعلن عن المدمّر على أنه تابع وهمي خالص، فهو على أي حال لازم التعريف إذا أردنا أن نكون قادرين على حذف النسخة من الذاكرة. ولكن لمّا كان المدمّر وهميًا، على الأرجح لمنع تسرّب الذاكرة أثناء الاستخدام متعدد الأشكال، فلن يتأثر الأداء بالسلب في حال إعلان دالّة وهمية أخرى، قد يكون هذا مفيدًا عند صنع الواجهات. struct Interface { virtual~Interface() = 0; }; Interface::~Interface() = default; struct Implementation: Interface {}; في الشيفرة السابقة، لاحظ أن ()Implementation~ تُعرَّف تلقائيًا من قبل المصرِّف إن لم تُحدد بشكل صريح. إذا احتوت معظم أو كلّ تطبيقات الدالّة الوهمية الخالصة على شيفرة مكررة، فيمكن نقل تلك الشيفرة إلى إصدار الدالّة الموجود في الصنف الأساسي، من أجل تسهيل الصيانة. class SharedBase { State my_state; std::unique_ptr < Helper > my_helper; // ... public: virtual void config(const Context & cont) = 0; // ... }; /* virtual */ void SharedBase::config(const Context & cont) { my_helper = new Helper(my_state, cont.relevant_field); do_this(); and_that(); } class OneImplementation: public SharedBase { int i; // ... public: void config(const Context & cont) override; // ... }; void OneImplementation::config(const Context & cont) /* override */ { my_state = { cont.some_field, cont.another_field, i }; SharedBase::config(cont); my_unique_setup(); }; // SharedBase وهكذا بالنسبة للأصناف الأخرى المشتقة من هذا الدرس جزء من سلسلة دروس عن C++. ترجمة -بتصرّف- للفصل Chapter 38: Virtual Member Functions من كتاب C++ Notes for Professionals
-
يمكن تعريف عوامل مثل + و -> في ++C من أجل استخدامها مع الأنواع المُعرّفة من قِبل المستخدم. فمثلًا، تعرِّف الترويسة العامل + لضمّ (concatenate) السلاسل النصية، وهذا ممكن عن طريق تعريف عامِل باستخدام الكلمة المفتاحية operator. العوامل الحسابية (Arithmetic operators) من الممكن زيادة تحميل جميع العوامل الحسابية الأساسية: + ، += - ، -= * ، *= / ، /= & ، &= | ، |= ^ ، ^= >> ، >>= << ، <<= يتشابه التحميل الزائد في كل العوامل كما سترى فيما يأتي من الشرح، ولزيادة التحميل خارج الأصناف (class) والبنيات (struct)، يجب تطبيق العامل +operator وفق العامل =+operator. انظر المثال التالي: T operator+(T lhs, const T& rhs) { lhs += rhs; return lhs; } T& operator+=(T& lhs, const T& rhs) { // إجراء عملية الجمع return lhs; } التحميل الزائد داخل الأصناف والبنيات: انظر المثال التالي حيث يجب تطبيق العامل +operator وفق العامل =+operator. T operator + (const T & rhs) { * this += rhs; return *this; } T & operator += (const T & rhs) { // إجراء عملية الجمع return *this; } ملاحظة: يجب أن يعيد operator+ قيمة غير ثابتة، إذ أنّ إعادة مرجع لن يكون له معنى -إذ يُرجع كائنًا جديدًا- ولا إعادة قيمة ثابتة const كذلك إذ يجب أن تتجنّب عمومًا الإعادة بقيمة ثابتة، ويُمرّر الوسيط الأول بالقيمة (by value)، للسببين التاليين: نظرًا لأنّك لا تستطيع تعديل الكائن الأصلي، ذلك أن Object foobar = foo + bar; لا ينبغي أن يعدّل foo على أيّ حال لأنه لا فائدة من ذلك. لا يمكنك جعله ثابتًا لأنّك ستحتاج إلى تعديل الكائن لما أن operator+ تُنفَّذ بواسطة operator+= الذي يعدّل الكائن التمرير بمرجع ثابت &const هو أحد الخيارات المتاحة، لكن سيتعيّن عليك حينها إنشاء نسخة مؤقّتة من الكائن المُمرّر، أما إن مرّرت الوسيط بقيمته (by value) فسيتكفّل المُصرّف بذلك نيابة عنك. كذلك فإن operator+= يعيد مرجعًا إلى نفسه، وهكذا يمكن سَلْسَلَته، لكن لا تستخدم المتغيّر نفسه، إذ أنّ ذلك سيؤدي إلى سلوك غير محدّد. الوسيط الأوّل هو مرجع نريد تعديله لكنه ليس ثابتًا، لأنك لن تستطيع تعديله عندئذ، ولا ينبغي تعديل الوسيط الثاني، ويُمرَّر بمرجِع ثابت const& لأسباب تتعلق بالأداء، إذ أن تمرير الوسيط بمرجع ثابت أسرع من تمريرِه بالقيمة. عامل فهرسة المصفوفات (Array subscript operator) يمكن زيادة تحميل عامل فهرسة المصفوفات []، ويجب عليك دائمًا تطبيق نسختين، إحداهما ثابتة (const)، والأخرى غير ثابتة، لأنّه إن كان الكائن ثابتًا فلن يستطيع تعديل الكائن المُعاد من قِبل عامل الفهرسة []. تُمرَّر الوسائط بمرجع ثابت (const&) بدلاً من قيَمها لأنّ التمرير بالمرجع أسرع من التمرير بالقيمة، كما أنها تكون ثابتة حتى لا يُغيِّرَ العامِل الفهرسَ عن طريق الخطأ، وتعيد العوامل القيمة بالمرجع، لأنّها مصممة بطريقة تمكِّنك من تعديل الكائن [] المُعاد، انظر المثال التالي حيث نغير القيمة من 1 إلى 2، إذ لم يكن ذلك ممكنًا إن لم يُعَد بالمرجع. std::vector<int> v{ 1 }; v[0] = 2; لا يمكنك زيادة التحميل إلا داخل صنف أو بنية، انظر المثال التالي حيث يكون I هو نوع الفهرس، ويكون غالبًا عددًا صحيحًا: T& operator[](const I& index) // افعل شيئا ما // أعِد شيئًا ما } const T& operator[](const I& index) const // افعل شيئا ما // إعادة شيء ما } يمكن إنشاء عدّة عوامل فهرسة [][]... عبر الكائنات الوكيلة (proxy objects). انظر المثال التالي: template < class T > class matrix { // [][] يسمح الصنف بتحميل template < class C > class proxy_row_vector { using reference = decltype(std::declval < C > ()[0]); using const_reference = decltype(std::declval < C const > ()[0]); public: proxy_row_vector(C& _vec, std::size_t _r_ind, std::size_t _cols): vec(_vec), row_index(_r_ind), cols(_cols) {} const_reference operator[](std::size_t _col_index) const { return vec[row_index * cols + _col_index]; } reference operator[](std::size_t _col_index) { return vec[row_index * cols + _col_index]; } private: C& vec; std::size_t row_index; // فهرس الصفوف std::size_t cols; // عدد الأعمدة في المصفوفة }; using const_proxy = proxy_row_vector < const std::vector < T >> ; using proxy = proxy_row_vector < std::vector < T >> ; public: matrix(): mtx(), rows(0), cols(0) {} matrix(std::size_t _rows, std::size_t _cols): mtx(_rows*_cols), rows(_rows), cols(_cols) {} // [] متبوعا باستدعاء آخر لـ operator[] استدعاء const_proxy operator[](std::size_t _row_index) const { return const_proxy(mtx, _row_index, cols); } proxy operator[](std::size_t _row_index) { return proxy(mtx, _row_index, cols); } private: std::vector < T > mtx; std::size_t rows; std::size_t cols; }; عوامل التحويل يمكنك زيادة تحميل عوامل النوع (type operators) بحيث يمكن تحويل النوع ضمنيًا إلى نوع آخر، ويجب تعريف عامل التحويل في صنف (class) أو بنية (struct): operator T() const { /* إعادة شيء ما */ } *ملاحظة: يكون العامل ثابتًا حتى يسمح بتحويل الكائنات الثابتة. انظر المثال التالي حيث نحول Text ضمنيًا إلى *const char: struct Text { std::string text; // هنا نحوله ضمنيًا: /*explicit*/ operator const char*() const { return text.data(); } // ^^^^^^^ // لتعطيل التحويل الضمني }; Text t; t.text = "Hello world!"; // OK const char* copyoftext = t; نظرة أخرى على الأعداد المركبة تستخدم الشيفرة التالية نوعًا يمثل الأعداد المركّبة، حيث يُحوَّل الحقل الأساسي (underlying field) تلقائيًا وفقًا لقواعد تحويل الأنواع وبتطبيق العوامل الأساسية الأربعة (+ و - و * و /) مع عضو من حقل آخر (سواء كان من النوع complex<T>، أو من نوع عددي آخر). لنرى الآن المثال التالي الذي يوضّح مفهوم زيادة تحميل العوامل وكيفية استخدام القوالب: #include <type_traits> namespace not_std{ using std::decay_t; //---------------------------------------------------------------- // complex< value_t > //---------------------------------------------------------------- template<typename value_t> struct complex { value_t x; value_t y; complex &operator += (const value_t &x) { this->x += x; return *this; } complex &operator += (const complex &other) { this->x += other.x; this->y += other.y; return *this; } complex &operator -= (const value_t &x) { this->x -= x; return *this; } complex &operator -= (const complex &other) { this->x -= other.x; this->y -= other.y; return *this; } complex &operator *= (const value_t &s) { this->x *= s; this->y *= s; return *this; } complex &operator *= (const complex &other) { (*this) = (*this) * other; return *this; } complex &operator /= (const value_t &s) { this->x /= s; this->y /= s; return *this; } complex &operator /= (const complex &other) { (*this) = (*this) / other; return *this; } complex(const value_t &x, const value_t &y) : x{x} , y{y} {} template<typename other_value_t> explicit complex(const complex<other_value_t> &other) : x{static_cast<const value_t &>(other.x)} , y{static_cast<const value_t &>(other.y)} {} complex &operator = (const complex &) = default; complex &operator = (complex &&) = default; complex(const complex &) = default; complex(complex &&) = default; complex() = default; }; // تربيع القيمة المطلقة template<typename value_t> value_t absqr(const complex<value_t> &z) { return z.x*z.x + z.y*z.y; } //---------------------------------------------------------------- // operator - (negation) - عامل النفي //---------------------------------------------------------------- template<typename value_t> complex<value_t> operator - (const complex<value_t> &z) { return {-z.x, -z.y}; } //---------------------------------------------------------------- // + عامل //---------------------------------------------------------------- template<typename left_t,typename right_t> auto operator + (const complex<left_t> &a, const complex<right_t> &b) -> complex<decay_t<decltype(a.x + b.x)>> { return{a.x + b.x, a.y + b.y}; } template<typename left_t,typename right_t> auto operator + (const left_t &a, const complex<right_t> &b) -> complex<decay_t<decltype(a + b.x)>> { return{a + b.x, b.y}; } template<typename left_t,typename right_t> auto operator + (const complex<left_t> &a, const right_t &b) -> complex<decay_t<decltype(a.x + b)>> { return{a.x + b, a.y}; } //---------------------------------------------------------------- // - عامل //---------------------------------------------------------------- template<typename left_t,typename right_t> auto operator - (const complex<left_t> &a, const complex<right_t> &b) -> complex<decay_t<decltype(a.x - b.x)>> { return{a.x - b.x, a.y - b.y}; } template<typename left_t,typename right_t> auto operator - (const left_t &a, const complex<right_t> &b) -> complex<decay_t<decltype(a - b.x)>> { return{a - b.x, - b.y}; } template<typename left_t,typename right_t> auto operator - (const complex<left_t> &a, const right_t &b) -> complex<decay_t<decltype(a.x - b)>> { return{a.x - b, a.y}; } //---------------------------------------------------------------- // * عامل //---------------------------------------------------------------- template<typename left_t, typename right_t> auto operator * (const complex<left_t> &a, const complex<right_t> &b) -> complex<decay_t<decltype(a.x * b.x)>> { return { a.x*b.x - a.y*b.y, a.x*b.y + a.y*b.x }; } template<typename left_t, typename right_t> auto operator * (const left_t &a, const complex<right_t> &b) -> complex<decay_t<decltype(a * b.x)>> { return {a * b.x, a * b.y}; } template<typename left_t, typename right_t> auto operator * (const complex<left_t> &a, const right_t &b) -> complex<decay_t<decltype(a.x * b)>> { return {a.x * b, a.y * b}; } //---------------------------------------------------------------- // / عامل //---------------------------------------------------------------- template<typename left_t, typename right_t> auto operator / (const complex<left_t> &a, const complex<right_t> &b) -> complex<decay_t<decltype(a.x / b.x)>> { const auto r = absqr(b); return { ( a.x*b.x + a.y*b.y) / r, (-a.x*b.y + a.y*b.x) / r }; } template<typename left_t, typename right_t> auto operator / (const left_t &a, const complex<right_t> &b) -> complex<decay_t<decltype(a / b.x)>> { const auto s = a/absqr(b); return { b.x * s, -b.y * s }; } template<typename left_t, typename right_t> auto operator / (const complex<left_t> &a, const right_t &b) -> complex<decay_t<decltype(a.x / b)>> { return {a.x / b, a.y / b}; } } // not_std فضاء الاسم int main(int argc, char **argv) { using namespace not_std; complex<float> fz{4.0f, 1.0f}; // complex<double> إنشاء auto dz = fz * 1.0; // complex<double> ما يزال auto idz = 1.0f/dz; // complex<double> ما يزال auto one = dz * idz; // complex<double> أيضا auto one_again = fz * idz; // اختبار العامل للتحقق من أن كل شيء سيُصرّف بلا مشاكل complex<float> a{1.0f, -2.0f}; complex<double> b{3.0, -4.0}; // complex<double> كل هذه من النوع auto c0 = a + b; auto c1 = a - b; auto c2 = a * b; auto c3 = a / b; // complex<float> كل هذه من النوع auto d0 = a + 1; auto d1 = 1 + a; auto d2 = a - 1; auto d3 = 1 - a; auto d4 = a * 1; auto d5 = 1 * a; auto d6 = a / 1; auto d7 = 1 / a; // complex<double> كل هذه من النوعauto e0 = b + 1; auto e1 = 1 + b; auto e2 = b - 1; auto e3 = 1 - b; auto e4 = b * 1; auto e5 = 1 * b; auto e6 = b / 1; auto e7 = 1 / b; return 0; } العوامل المسماة (Named operators) يمكنك توسيع C++ بالعوامل المسمّاة المحاطة بعوامل ++C القياسية. سنبدأ أولًا بكتابة شيفرة تشكل مكتبة من بضعة أسطر: namespace named_operator { template < class D > struct make_operator { constexpr make_operator() {} }; template < class T, char, class O > struct half_apply { T && lhs; }; template < class Lhs, class Op > half_apply < Lhs, '*', Op > operator * (Lhs && lhs, make_operator < Op > ) { return { std::forward < Lhs > (lhs) }; } template < class Lhs, class Op, class Rhs > auto operator*( half_apply<Lhs, '*', Op>&& lhs, Rhs&& rhs ) -> decltype(named_invoke(std::forward < Lhs > (lhs.lhs), Op {}, std::forward < Rhs > (rhs))) { return named_invoke(std::forward < Lhs > (lhs.lhs), Op {}, std::forward < Rhs > (rhs)); } } هذه الشيفرة لا تفعل أي شيء حتى الآن. سنضيف الآن المتجهات: namespace my_ns { struct append_t : named_operator::make_operator<append_t> {}; constexpr append_t append{}; template<class T, class A0, class A1> std::vector<T, A0> named_invoke( std::vector<T, A0> lhs, append_t, std::vector<T, A1> const& rhs ) { lhs.insert( lhs.end(), rhs.begin(), rhs.end() ); return std::move(lhs); } } using my_ns::append; std::vector<int> a {1,2,3}; std::vector<int> b {4,5,6}; auto c = a *append* b; لقد عرّفنا كائنًا append من النوع append_t:named_operator::make_operator<append_t>، ثم زدنا بعد ذلك تحميل append_t:named_operator::make_operator<append_t> للأنواع التي نريدها على اليمين واليسار. ستزيد المكتبةُ تحميل lhs*append_t لإعادة كائن half_apply مؤقّت، كما أنها ستزيد تحميل half_apply*rhs لاستدعاء named_invoke( lhs, append_t, rhs ). ويجب أن ننشئ مفتاح append_t ليستدعي التوقيع المناسب عبر named_invoke متوافق مع البحث القائم على العامل (ADL-friendly) . لنفترض الآن أنّك تريد إجراء عملية ضرب عنصرًا بعنصر العناصر المصفوفة: template<class=void, std::size_t...Is> auto indexer( std::index_sequence<Is...> ) { return [](auto&& f) { return f( std::integral_constant<std::size_t, Is>{}... ); }; } template<std::size_t N> auto indexer() { return indexer( std::make_index_sequence<N>{} ); } namespace my_ns { struct e_times_t : named_operator::make_operator<e_times_t> {}; constexpr e_times_t e_times{}; template<class L, class R, std::size_t N, class Out=std::decay_t<decltype( std::declval<L const&>()*std::declval<R const&>() )> > std::array<Out, N> named_invoke( std::array<L, N> const& lhs, e_times_t, std::array<R, N> const& rhs ) { using result_type = std::array<Out, N>; auto index_over_N = indexer<N>(); return index_over_N([&](auto...is)->result_type { return {{ (lhs[is] * rhs[is])... }}; }); } } هذا مثال حيّ على ذلك. يمكن توسيع هذه الشيفرة لتعمل على الصفوف (tuples) أو الأزواج أو المصفوفات الشبيهة بـ C، أو حتى الحاويات متغيّرة الطول. كذلك تستطيع استخدام عامل نوعي عنصري (element-wise operator type) والحصول منه على lhs *element_wise<'+'>* rhs. أيضًا من الممكن استخدام عامِلا الضرب *dot* و *cross*. يمكن توسيع استخدام * ليدعم مُحدّدات (delimiters) أخرى مثل +، وتُحدّد أسبقيةُ المُحدّد أسبقيةَ العامِل المسمَّى، وذلك مفيد عند استخدام معادلات الفيزياء في C++ إذ لن تحتاج إلّا إلى الحد الأدنى من أقواس () . نستطيع دعم عوامل ->*then* بتغيير طفيف على المكتبة أعلاه، وكذلك توسيع std::function قبل المعيار الذي يتم تحديثه، أو كتابة ->*bind* أُحاديّ، بل يمكننا الحصول على عامل مُسمّى إذ نمرِّر العامل Op إلى دالّة الاستدعاء النهائية، مما يسمح بما يلي: named_operator < '*' > append = [](auto lhs, auto && rhs) { using std::begin; using std::end; lhs.insert(end(lhs), begin(rhs), end(rhs)); return std::move(lhs); }; مما ينتج عنه إنشاء عامل مسمّى ومضيف للحاويات في C++ 17. العوامل الأحادية (Unary Operators) العاملان الأحاديان التاليان يمكن زيادة تحميلهما: ++foo و foo++ --foo و foo-- ويكون التحميل مُتماثل بالنسبة لكلا النوعين (++ و --)، ولزيادة التحميل خارج الصنف (class) أو البنية (struct): // ++foo العامل المسبق T & operator++(T & lhs) { // إجراء عملية الجمع return lhs; } يجب أن يُستخدم العامل المسبق ++foo -يُستخدم وسيط int لفصل المُسبِق pre عن اللاحق postfix- بواسطة العامل المسبق foo++، انظر: T operator++(T & lhs, int) { T t(lhs); ++lhs; return t; } التحميل الزائد داخل الصنف (class) أو البنية (struct): // ++foo العامل المسبق T & operator++() { // إجراء عملية الجمع return *this; } كما فعلنا قبل قليل، يجب أن يُستخدم ++foo -يُستخدم وسيط int لفصل المُسبِق pre عن اللاحق postfix- بواسطة foo++، انظر: T operator++(int) { T t( * this); ++( * this); return t; } ملاحظة: يعيد العامل المسبق مرجعًا إلى نفسه حتى يمكنك متابعة العمليات عليه، ويكون الوسيط الأول مرجعًا إذ أنّ العامل المسبق يغيّر الكائن، وهذا سبب في كونه غير ثابت const، إذ لم تكن لتستطيع تعديله إن كان غير ذلك. يُعيد العامل المُلحق (postfix operator) قيمة مؤقتة -القيمة السابقة-، وعليه لا يمكن أن يكون مرجعًا لأنه سيكون مرجعًا إلى قيمة مؤقتة، والتي ستكون قيمة مُهملة (garbage value) في نهاية الدالّة لأنّ المتغيرات المؤقتة تخرج عن النطاق، كذلك لا يمكن أن تكون ثابتة إذ يُفترض أن تستطيع تعديلها مباشرةً. واعلم أن الوسيط الأوّل هو مرجع غير ثابت للكائن المُستدعِي، لأنه لو كان ثابتًا فلن تتمكّن من تعديله، وإذا لم يكن مرجعًا، فلن يكون بمقدورك تغيير القيمة الأصلية. يُفضل استخدام السابقة ++ بدلاً من اللاحقة ++ في [حلقات](رابط الفصل 11) for بسبب عملية النسخ (copying) الضرورية في تحميلات العامل المُلحق. ورغم تكافئ الاثنتين وظيفيًا من منظور [حلقة](رابط الفصل 11) for، إلا أنه قد تكون هناك ميزة طفيفة في الأداء عند استخدام السابقة ++، وخاصة في الأصناف "الكبيرة" التي تحتوي الكثير من الأعضاء الواجب نسخها. انظر المثال التالي على استخدام السابقة ++ في حلقة for: for (list < string > ::const_iterator it = tokens.begin(); it != tokens.end(); ++it) { // it++ لا تستخدم ... } عوامل الموازنة (Comparison operators) يمكنك زيادة تحميل جميع عوامل المقارنة: == و != > و < >= و <= الطريقة الموصى بها لزيادة تحميل كل هذه العوامل هي باستخدام العامليْن (== و <) فقط، ثمّ استخدَامِهما لتعريف الباقي. انظر المثال التالي للتحميل خارج الصنف أو البنية: // لا تستخدم إلا هذين العامليْن bool operator == (const T & lhs, const T & rhs) { /* Compare */ } bool operator < (const T & lhs, const T & rhs) { /* Compare */ } // الآن يمكنك تعريف الباقي bool operator != (const T & lhs, const T & rhs) { return !(lhs == rhs); } bool operator > (const T & lhs, const T & rhs) { return rhs < lhs; } bool operator <= (const T& lhs, const T& rhs) { return !(lhs > rhs); } bool operator >= (const T& lhs, const T& rhs) { return !(lhs < rhs); } زيادة التحميل داخل الصنف أو البنية، لاحظ أن الدوال ثابتة، ذلك أنه إن لم تكن كذلك فلن تستطيع استدعاءها إن كان الكائن ثابتًا: // لا تستخدم إلا هذين العاملين bool operator == (const T& rhs) const { /* Compare */ } bool operator < (const T& rhs) const { /* Compare */ } // الآن يمكنك تعريف الباقي bool operator != (const T& rhs) const { return !( *this == rhs); } bool operator > (const T& rhs) const { return rhs < *this; } bool operator <= (const T& rhs) const { return !( *this > rhs); } bool operator >= (const T& rhs) const { return !( *this < rhs); } من الواضح أنّ العوامل ستعيد قيمة منطقية (bool)، أي إمّا true أو false. تأخذ جميع العوامل وسائطها كمراجع ثابتة (const&) لأنّ الشيء الوحيد الذي تفعله هذه العوامل هو المقارنة، لذلك لا ينبغي لها تعديل الكائنات. لاحظ أن التمرير بالمرجع (&) أسرع من التمرير بالقيمة (by value)، ويكون مرجعًا ثابتًا const للتأكد من أنّ العوامل لن تعدّله. كذلك، لاحظ أنّ العوامل داخل الأصناف والبنيات تكون ثابتة، لأن الدوالّ إن لم تكن ثابتة فلن يمكننا مقارنة الكائنات الثابتة، لأنّ المصرّف لا يعلم أنّ العوامل لا تغيّر شيئًا. عامل الإسناد (Assignment Operator) تكمن أهمية عامل الإسناد في أنه يتيح لك تغيير حالة المتغير، وإذا لم تزد تحميل عامل الإسناد في الصنف أو البنية فسيُنشئه المصرِّف تلقائيًا، ويُجري عامل الإسناد المُنشأ تلقائيًا عملية "الإسناد عضوًا بعضو" (memberwise assignment)، أي عن طريق استدعاء عوامل الإسناد على جميع الأعضاء، بحيث يُنسخ كائن إلى آخر، عضوًا بعضو. يجب زيادة تحميل عامل الإسناد عندما لا تكون عملية الإسناد عضوًا بعضو مناسبة للصنف أو البنية خاصتك، كأن تكون بحاجة إلى تنفيذ نسخ عميق (deep copy) لكائن ما. زيادة تحميل عامل الإسناد = سهل، لكن عليك اتباع بعض الخطوات البسيطة. اختبار الإسناد الذاتي (Test for self-assignment). هذا الاختبار مهم لسببين: الإسناد الذاتي عملية نسخ لا داعي لها، لذلك ليس من المنطقي إجراؤها. لن تنجح الخطوة التالية في حال استخدام الإسناد الذاتي. تنظيف البيانات القديمة. يجب استبدال البيانات الجديدة بالبيانات القديمة. ربما تفهم الآن السبب الثاني في الخطوة السابقة إذ أنه في حال حذف محتوى الكائن فستفشل عملية الإسناد الذاتي في تنفيذ عملية النسخ. نسخ جميع الأعضاء. إذا زدت تحميل عامل الإسناد في صنفك أو بنيتك فلن يُنشأ تلقائيًا بواسطة المُصرّف، لذلك سيقع على عاتقك مسؤولية نسخ جميع الأعضاء من الكائن الآخر. إعادة *this. يُعيد العامل مرجعًا إليه لأجل السماح بالعمليات المتسلسلة (أي int b = (a = 6) + 4;). // يمثل نوعا ما T T & operator = (const T & other) { // افعل شيئا ما return *this; } ملاحظة: يُمرّر other بمرجع ثابت (const&) لأنه لا ينبغي تغيير الكائن الذي يتم تعيينه، كما أنّ التمرير بالمرجع أسرع من التمرير بالقيمة، وينبغي جعل operator= ثابتًا const للتأكد أنه لن يعدّلها عن طريق الخطأ. لا يمكن زيادة تحميل عامل الإسناد إلا في الأصناف والبنيات، لأنّ القيمة اليسرى لـ = تكون دائمًا هي الصنفَ أو البنيةَ نفسها، ولا يضمن تعريف العامل كدالّة حرّة (free function) ذلك، لهذا لا يُسمح به. تكون القيمة اليسرى هي الصنف أو البُنية بنفسها بشكل ضمني عند التصريح عنها في صنف أو بُنية، لذا لا توجد مشكلة في ذلك. عامل استدعاء الدالّة (Function call operator) يمكنك زيادة تحميل عامل استدعاء الدالّة ()، ويجب أن تحدث زيادة التحميل داخل صنف أو بنية: //R <- نوع القيمة المعادة R operator()(Type name, Type2 name2, ...) { // افعل شيئا ما // إعادة قيمة } // استخدمه هكذا R foo = object(a, b, ...); مثلا: struct Sum { int operator()(int a, int b) { return a + b; } }; // انشاء نسخة من البنية Sum sum; int result = sum(1, 1); // 2 عامل NOT الثنائي (Bitwise NOT operator) زيادة تحميل عامل NOT الثنائي (~) بسيط إلى حد ما. انظر المثال التالي لزيادة التحميل خارج الصنف أو البنية: T operator~(T lhs) { // نفذ العملية return lhs; } التحميل الزائد داخل الصنف أو البنية class / struct: T operator~() { T t( *this); // نفذ العملية return t; } ملاحظة: يعيد عامل operator~ بالقيمة (by value) لأنّ عليه أن يُعيد قيمة جديدة (القيمة المُعدّلة) وليس مرجعًا إلى القيمة -سيكون مرجعًا إلى الكائن المؤقّت الذي ستصبح قيمته مُهملة [محذوفة] بمجرد تنفيذ العامل-، كما أنها لا ينبغي أن تكون ثابتة لأنّ شيفرة الاستدعاء يجب أن تكون قادرة على تعديله بعد ذلك (أي يجب أن تكون العبارة int a = ~a+ 1; ممكنة). يجب عليك إنشاء كائن مؤقت داخل الصنف أو البنية، إذ لا يمكنك تعديل this، لأنه سيؤدي إلى تعديل الكائن الأصلي، وهو ما لا ينبغي أن يحدث. عوامل الإزاحة البِتيّة للدخل/الخرج (Bit shift operators for I/O) يشيع استخدام العامليْن << و >> كعوامل للكتابة والقراءة، على الترتيب. std::ostream تزيد تحميل << لكتابة المتغيرات في [المجرى](رابط الفصل 13) الأساسي (على سبيل المثال: std::cout) std::istream تزيد تحميل >> للقراءة من [المجرى](رابط الفصل 13) الأساسي إلى متغير (على سبيل المثال: std::cin) ويتماثل أسلوبهما في حال أردت زيادة تحميلهما "بشكل طبيعي" خارج الصنف أو البنية، باستثناء أنّ الوسائط ليست من نفس النوع: نوع القيمة المُعادة هو [المجرى](رابط الفصل 13) الذي تريد زيادة التحميل منه - overload from - (على سبيل المثال، std::ostream)، والذي يُمرّر بالمرجع (by reference) للسماح بالعمليات المتسلسلة (التسلسل: std::cout << a << b;). مثال: std::ostream& lhs سيكون من نفس نوع القيمة المعادة. rhs تمثّل النوع الذي تريد السماح بالتحميل منه (على سبيل المثال T)، والذي يُمرَّر بمرجع ثابت بدلاً من تمريره بالقيمة لأسباب تتعلّق بالأداء (لا ينبغي تغيير rhs على أيّ حال). مثال: const Vector&. انظر المثال التالي حيث نزيد تحميل >>std::ostream operator للسماح بالخرج من المتجه: std::ostream& operator<<(std::ostream& lhs, const Vector& rhs) { lhs << "x: " << rhs.x << " y: " << rhs.y << " z: " << rhs.z << '\n'; return lhs; } Vector v = { 1, 2, 3 }; // الآن يمكنك فعل ما يلي std::cout << v; هذا الدرس جزء من سلسلة مقالات عن C++. ترجمة -بتصرّف- للفصل Chapter 36: Operator Overloading من كتاب C++ Notes for Professionals
-
تصنيف كلفة تمرير وسيط إلى معامل يقسم تحليل التحميل الزائد تكلفة تمرير وسيط (argument) إلى مُعامل (parameter) إلى 4 تصنيفات مختلفة، تُسمّى "تسلسلات" (sequences)، وقد يتضمن كل تسلسل صفرًا أو واحدًا أو عدّة تحويلات. تسلسل التحويل القياسي Standard conversion sequence void f(int a); f(42); تسلسل التحويل المُعرّف من قبل المستخدم User defined conversion sequence void f(std::string s); f("hello"); تسلسل تحويل علامة الحذف Ellipsis conversion sequence: void f(...); f(42); تسلسل قائمة التهيئة: void f(std::vector<int> v); f({ 1, 2, 3 }); المبدأ العام هو أنّ تسلسلات التحويل القياسية هي الأقل كلفة، يليها تسلسل التحويل المُعرَّف من المستخدم، يليها تسلسل تحويل علامة الحذف. أما تسلسل قائمة التهيئة فهي حالة خاصة، إذ أنّها ليست تحويلًا -قائمة المهيئ ليست تعبيرًا ذا نوع- تُحسب كُلفتها عن طريق تعريفها على أنّها تكافئ إحدى تسلسلات التحويل الثلاثة الأخرى، وذلك اعتمادًا على نوع المُعامل وشكل قائمة المهيئ. الترقيات والتحويلات الحسابية Arithmetic promotions and conversions يُعدّ تحويل نوع عددي صحيح إلى نوع مُرقّى (promoted type) مقابل له أفضل من تحويله إلى نوع عددِي صحيح آخر. void f(int x); void f(short x); signed char c = 42; f(c); // short أفضل من التحويل إلى int الترقية إلى short s = 42; f(s); // int التطابق التام أفضل من الترقية إلى كذلك ترقية float إلى double أفضل من تحويله إلى نوع عددِي عشري آخر. void f(double x); void f(long double x); f(3.14 f); // calls f(double); long double أفضل من التحويل إلى double الترقية إلى تتكافأ التحويلات الحسابية الأخرى، بخلاف الترقيات. void f(float x); void f(long double x); f(3.14); // غامض void g(long x); void g(long double x); g(42); // غامض g(3.14); // غامض لذلك، من أجل ضمان رفع أيّ لبس عند استدعاء دالة f سواء مع وسائط عددية صحيحة أو عشرية من أيّ نوع قياسي، فستحتاج لكتابة ثمانية تحميلات زائدة، بحيث يحدث تطابق تام مع التحميل الزائد أو يُختار التحميل الزائد ذو نوع الوسيط المُرقّى (promoted argument type)، مهما كان نوع الوسيط. void f(int x); void f(unsigned int x); void f(long x); void f(unsigned long x); void f(long long x); void f(unsigned long long x); void f(double x); void f(long double x); التحميل الزائد على مرجع إعادة توجيه Forwarding Reference يجب أن تكون حذرًا عند توفير تحميل زائد لمرجع إعادة توجيه (forwarding reference) لأنّه قد يكون تامَّ التطابق: struct A { A() = default; // #1 A(A const& ) = default; // #2 template <class T> A(T&& ); // #3 }; والقصد هنا هو أنّ A قابلة للنسخ، وأنّ لدينا منشئًا آخر يمكنه أن يهيّئ عضوًا آخر. انظر: A a; // #1 استدعاء A b(a); // #3! استدعاء هناك تطابُقان صالحان لاستدعاء الإنشاء: A(A const& ); // #2 A(A& ); // #3, مع T = A& كلاهما مُطابِقان بشكل تام، لكن #3 تأخذ مرجعًا إلى كائن تأهيله أقل من #2، لذا فهو أكثر امتثالًا لتسلسل التحويل القياسي، وأكثر تطابقًا، والحل هنا هو تقييد هذه المنشآت دائمًا (على سبيل المثال باستخدام قاعدة SFINAE): template <class T, class = std::enable_if_t<!std::is_convertible<std::decay_t<T>*, A*>::value> > A(T&& ); وما نريده هنا هو استبعاد أيّ نوع A أو صنف مُشتقّ علنًا (publicly) من A من نظرنا، ونتيجة لذلك تصبح صيغة هذا المنشئ غير صحيحة في المثال المُوضّح سابقًا (وعليه يُزال من مجموعة التحميل الزائد). لذا سيُستدعى مُنشئ النسخ، وهو الذي أردنا. التطابق التام يفضَّل التحميل الزائد الذي لا يحتاج إلى تحويل أنواع المعاملات أو الذي يحتاج فقط إلى التحويلات بين الأنواع التي تُعدُّ مُتطابقة بشكل تام، على التحميل الزائد الذي يتطّلب تحويلات أخرى قبل إجراء الاستدعاء. void f(int x); void f(double x); f(42); // f(int) استدعاء عندما يرتبط وسيط بمرجع من نفس النوع، فإنّ المطابقة لن تتطّلب تحويلًا حتى لو كان المرجع ذا تأهيل ثباتي أعلى. انظر المثال التالي حيث يكون نوع الوسيط في (f(x هو int، وهو تطابق تام مع &int: void f(int& x); void f(double x); int x = 42; f(x); void g(const int& x); void g(int x); g(x); // غامض، كلا التحميلين الزائدين يعطيان تطابقا تاما يعد النوعان "مصفوفة تحتوي عناصر من النوع T" و "مؤشّر إلى T" متطابقين بشكل تام لغرض تحليل التحميل الزائد، كما يُعدُّ نوع الدالّة T مطابقًا تمامًا لنوع مؤشّر الدالّة T*، رغم أنّ كليهما يتطّلبان إجراء تحويلات . void f(int* p); void f(void* p); void g(int* p); void g(int (&p)[100]); int a[100]; f(a); // f(int*); تطابق تام، مع تحويل من مصفوفة إلى مؤشّر g(a); // غامض، كلا التحويلين الزائدين يعطيان تطابقا تاما التحميل الزائد للثباتية constness والتغير volatility يُعدّ تمرير وسيط مؤشّر (pointer argument) إلى مُعامل T* -إن أمكن- أفضل من تمريره إلى مُعامل const T*. struct Base {}; struct Derived : Base {}; void f(Base* pb); void f(const Base* pb); void f(const Derived* pd); void f(bool b); Base b; f(&b); Derived d; f(&d); تفسير الشيفرة السابقة: في (f(&b: تٌفضَّل (*f(base على (*f(const base. في (f(&d: تٌفضَّل (*f(const Derived على (*f(base، رغم أن دور الثباتية ينحصر في كسر التعادل (tie-breaker). وبالمثل، يُعدّ تمرير وسيط إلى مُعامل T& أفضل من تمريره إلى مُعامل const T&، حتى لو كان لكليهما نفس التطابق (match rank). void f(int& r); void f(const int& r); int x; f(x); // أفضل f(int&) التحميلان مطابقان، لكنّ const int y = 42; f(y); // هي المرشح الصالح f(const int&) تنطبق هذه القاعدة أيضًا على الدوال التابعة المُؤهّلة ثباتيًا (const-qualified member functions)، التي تحتاج إلى السماح بالوصول المتغيّر (mutable access) إلى الكائنات غير الثابتة، والوصول الثابت (immutable access) إلى الكائنات الثابتة. class IntVector { public: // ... int* data() { return m_data; } const int* data() const { return m_data; } private: // ... int* m_data; }; IntVector v1; int* data1 = v1.data(); const IntVector v2; const int* data2 = v2.data(); تفسير الشيفرة السابقة: ()Vector::data أفضل من Vector::data() const، ويمكن استخدام data1 لتعديل المتجه. Vector::data() const هي المرشح الصالح الوحيد، ولايمكن استخدام data2 لتعديل المتجه. كذلك فإن التحميل الزائد غير المتغيّر (non-volatile overload) أفضل من التحميل الزائد المتغيّر: class AtomicInt { public: // ... int load(); int load() volatile; private: // ... }; AtomicInt a1; a1.load(); // يُفضَّل التحميل الزائد غير المتغير، لايوجد آثار جانبية volatile AtomicInt a2; a2.load(); // التحويل الزائد المتغير هو الوحيد المرشّح، مع وجود آثار جانبية static_cast< volatile AtomicInt &> (a1).load(); البحث عن الاسماء والتحقق من الوصول يحدث تحليل التحميل الزائد بعد البحث عن الاسم، هذا يعني أنّ تحليل التحميل الزائد قد لا يختار الدالّة الأكثر تطابقا في حال عدم العثور على اسمها. انظر المثال التالي حيث نستدعي S::f لأن f غير مرئية هنا، رغم أنها ستكون أكثر تطابقًا. void f(int x); struct S { void f(double x); void g() { f(42); } }; يحدث تحليل التحميل الزائد قبل التحقّق من الوصول، وقد تُختار دالّة غير مُتاحة للوصول من قبل تحليل التحميل الزائد بدلًا من دالة أخرى أقلّ تطابقًا ولو كانت متاحة للوصول. class C { public: static void f(double x); private: static void f(int x); }; C::f(42); في الشيفرة السابقة، تعطي (C::f(42 خطأً، لأنها تستدعي (private C::f(int رغم أن (public C::f(double` صالحة. وبالمثل، لا يتحقّق تحليل التحميل الزائد ممّا إذا كانت صيغة explicit صحيحة في الاستدعاء الناتج: struct X { explicit X(int); X(char); }; void foo(X); foo({ 4 }); // أكثر تطابقا، لكن التعبير سيء الصياغة، لأنّ المنشئ صريح X(int) التحميل الزائد داخل التسلسل الهرمي للصنف الأمثلة التالية سوف تستخدم هذا التسلسل الهرمي: struct A { int m; }; struct B: A {}; struct C: B {}; يُفضل التحويل من نوع صنف مشتق (derived class type) إلى نوع صنف أساسي (base class type) في التحويلات المُعرَّفة من قبل المستخدم، ينطبق هذا عند التمرير بالقيمة (by value) أو بالمرجع (by reference)، وكذلك عند تحويل مؤشّر يشير إلى صنف مشتق إلى مؤشّر آخر يشير إلى صنف أساسي. struct Unrelated { Unrelated(B b); }; void f(A a); void f(Unrelated u); B b; f(b); // f(A) استدعاء يُعدّ تحويل المؤشّر من صنف مشتق إلى صنف أساسي أفضل من تحويله إلى void*. void f(A* p); void f(void* p); B b; f(&b); // f(A*) استدعاء إذا كانت هناك عدّة تحميلات زائدة داخل نفس التسلسل الوراثي، فستكون الأولوية للتحميل الزائد الخاص بالصنف الأساسي الأكثر اشتقاقا (most derived base class). هذا المبدأ مماثل للإرسال الوهمي (virtual dispatch)، إذ يُختار التنفيذ "الأكثر تخصّصًا"، لكن يحدث تحليل التحميل الزائد دائمًا في وقت التصريف، ولن يُنزّل (down-cast) ضمنيًا أبدًا. void f(const A& a); void f(const B& b); C c; f(c); // f(const B&) استدعاء B b; A& r = b; f(r); // f(const A&) استدعاء // غير صالح f(const B&) التحميل الزائد لـ بالنسبة للمؤشّرات العضوية (pointers to members)، تُطبّق قاعدة مماثلة ولكن في الاتجاه المعاكس، إذ يُفضَّل الصنف المشتق الأقل اشتقاقا (least derived derived class). void f(int B::*p); void f(int C::*p); int A::*p = &A::m; f(p); // f(int B::*) استدعاء خطوات تحليل التحميل الزائد خطوات تحليل التحميل الزائد هي: البحث عن الدوالّ المرشّحة عبر البحث بالاسم. ستُجري الاستدعاءات غير المُؤهّلة كلّا من البحث العادي غير المُؤهّل - regular unqualified lookup - وكذلك البحث القائم على الوسيط - argument-dependent lookup - (إن أمكن). غربلة مجموعة الدوالّ المرشّحة لاستخلاص مجموعة من الدوالّ القابلة للتطبيق. الدوالّ القابلة للتطبيق هي الدوال التي يوجد تسلسل تحويل ضمني بين الوسائط المُمرّرة إليها والمعاملات التي تقبلها. في المثال التالي، تكون الدالتان 1 و2 صالحتين رغم حذفنا للدالة 2، والدالة 3 غير صالحة لعدم تطابق قائمة الوسائط، أما 4 فغير صالحة لأننا لا نستطيع ربط عنصر مؤقت بمرجع يساري غير ثابت. void f(char); // (1) void f(int) = delete; // (2) void f(); // (3) void f(int&); // (4) f(4); اختيار أفضل دالّة مرشحة قابلة للتطبيق. تكون دالّة قابلة للتطبيق F1 أفضل من دالّة أخرى أخرى قابلة للتطبيق F2 إذا لم يكن تسلسل التحويل الضمني لكل وسيط في F1 أسوأ من تسلسل التحويل الضمني المقابل في F2، وبالنسبذة لوسيط ما، فإنّ تسلسل التحويل الضمني لذلك الوسيط في F1 أفضل من تسلسل تحويل ذلك الوسيط في F2، void f(int); // (1) void f(char); // (2) f(4); // استدعاء 1 لأنّ تسلسل التحويل فيها أفضل أو في التحويلات المُعرَّفة من قبل المستخدم، فإنّ تسلسل التحويل القياسي (standard conversion sequence) من القيمة المُعادة من F1 إلى النوع المقصود أفضل منه لدى نوع القيمة المُعادة من F2، struct A { operator int(); operator double(); } a; int i = a; float f = a; // غامض تفسير int i = a في الشيفرة السابقة: يفضَّل ()a.operator int على ()a.operator double أو يكون لـ F1 نفس نوع المرجع في ارتباط مرجعي مباشر (direct reference binding)، على خلاف F2، struct A { operator X&(); // #1 operator X&&(); // #2 }; A a; X& lx = a; // #1 استدعاء X&& rx = a; // #2 استدعاء أو F1 ليست تخصيصًا لقالب دالة، على خلاف F2، template < class T > void f(T); // #1 void f(int); // #2 f(42); // #2 استدعاء أو F1 و F2 كلاهما تخصيصان لقالب الدالّة، بيْد أنّ F1 أكثر تخصيصًا من F2. template < class T > void f(T); // #1 template < class T > void f(T*); // #2 int* p; f(p); // #2 استدعاء الترتيب هنا مهم، فالتحقّق من تسلسل التحويل قبل القالب أفضل مقارنة بعدم التحقق من القالب (non-template check)، إذ يؤدّي هذا إلى خطأ شائع في التحميل الزائد لمراجع إعادة التوجيه (forwarding reference): struct A { A(A const& ); // #1 template < class T> A(T&&); // #2 ليست مقيدة }; A a; A b(a); // #2 استدعاء تفسير الشيفرة السابقة: الحالة 1 ليست قالبًا، والحالة 2 تحلَّل إلى (&A(A التي تكون مرجعًا أقل تأهيلًا مقارنة بحالة 1، وذلك يجعلها الخيار الأنسب لتسلسل التحويل الضمني. إذا لم يكن هناك مرشّح قابل للتطبيق أفضل من غيره، فسيُعدُّ الاستدعاء غامضًا: void f(double) {} void f(float) {} f(42); // خطأ: غامض التحميل الزائد للدوال (Function Overloading) ما هو التحميل الزائد للدوال؟ التحميل الزائد للدوالّ هو أن يُصرَّح عن عدة دوّال تحمل نفس الاسم بالضبط ولها نفس النطاق، وتختلف فقط في بصمتها (signature)، أي في الوسائط التي تقبلها. لنفترض أنّك تريد كتابة سلسلة من الدوالّ المتخصّصة في الطباعة بشكل عام، بدءًا بالسّلاسل النصية std::string: void print(const std::string & str) { std::cout << "This is a string: " << str << std::endl; } سيعمل هذا بكفاءة، لنفترض الآن أنّك تريد دالّة تقبل عددًا صحيحًا (int) وتطبعه. يمكنك كتابة: void print_int(int num) { std::cout << "This is an int: " << num << std::endl; } ولكن بما أن الدالتان تقبلان معاملات مختلفة، فيمكنك ببساطة كتابة: void print(int num) { std::cout << "This is an int: " << num << std::endl; } صار لدينا الآن دالتان، كلتاهما تحمل الاسم print ولكن مع بصْمتين مختلفين، وتقبل إحداهما سلسلة نصية، والأخرى تقبل عددًا صحيحًا (int). يمكنك الآن استدعَاؤهما على النحو التالي: print("Hello world!"); // "This is a string: Hello world!" print(1337); // "This is an int: 1337" بدلًا من: print("Hello world!"); print_int(1337); عندما تكون لديك عدة دوالّ زائدة التحميل (overloaded)، سيستنتج المُصرّف أيًّا من تلك الدوالّ يجب عليه استدعاؤها عبر تحليل المعاملات المُمرّرة. كذلك يجب الحذر عند تحميل الدوالّ. انظر المثال التالي مع تحويلات النوع الضمنية (implicit type conversions): void print(int num) { std::cout << "This is an int: " << num << std::endl; } void print(double num) { std::cout << "This is a double: " << num << std::endl; } كما ترى فليس من الواضح أيُّ تحميلٍ زائد للدالّة print سيُستدعى عند كتابة: print(5); وقد تحتاج إلى إعطاء المُصرّف بعض التلميحات، مثل: print(static_cast<double>(5)); print(static_cast<int>(5)); print(5.0); يجب توخّي الحذر أيضًا عند كتابة تحميلات زائدة تقبل معامِلات اختيارية: // انتبه! شيفرة خاطئة void print(int num1, int num2 = 0) // هي 0 num2 القيمة الافتراضية لـ { std::cout << "These are ints: << num1 << " and " << num2 << std::endl; } void print(int num) { std::cout << "This is an int: " << num << std::endl; } سيعجز المُصرّف عن معرفة إن كان الاستدعاء print(17) مخصّصًا للدالّة الأولى أو الثانية، وذلك بسبب المعامل الثاني الاختياري، لهذا لن تُصرّف هذه الشيفرة. نوع القيمة المعادة في الدوالّ زائدة التحميل لاحظ أنك لا تستطيع زيادة التحميل على دالّة بناءً على نوع قيمتها المعادة. مثلا: // شيفرة خاطئة std::string getValue() { return "hello"; } int getValue() { return 0; } int x = getValue(); سيؤدي هذا إلى حدوث خطأ في التصريف نظرًا لأنّ المصرّف سيعجز عن تحديد نسخة getValue التي يجب استدعاؤها، رغم اختلاف نوع القيمة المعادة في التصريحين. زيادة تحميل الدوال التابعة المؤهلة يمكن زيادة تحميل الدوال التي داخل الأصناف عند الوصول إليها عبر مراجع مؤهَّلة (cv-qualified reference) لذلك الصنف، ويُستخدم هذا غالبًا لزيادة تحميل الثوابت (const)، ولكن يمكن استخدامها أيضًا لزيادة تحميل القيم ذات التأهيل volatile و const volatile أيضًا، ذلك لأنّ جميع الدوال التابعة غير الساكنة تأخذ this كمعامِل خفي، ذلك المعامِل تُطبَّقُ عليه المؤهّلات الثباتية (cv- qualifiers). هذا ضروري لأنه لا يمكن استدعاء تابع إلا إذا كان تأهيله مكافئًا على الأقل لتأهيل النسخة التي استُدعِي عليها، وصحيح أنّ النسخة غير ثابتة تستطيع استدعاء كل من الأعضاء الثابتة وغير الثابتة، إلا أنه لا يمكن لنسخة ثابتة أن تستدعي إلا الأعضاء الثابتة، وهذا يسمح لدالّة بأن يكون لها سلوكيات مختلفة بحسب مؤهّلات النُّسخة التي استُدعِيت عليها، ويسمح للمبرمج بمنع الدوالّ بالنسبة لمؤهِّل معيّن من خلال عدم توفير إصدار من تلك الدالّة لذلك المؤهل. يمكن لصنف ذي تابع print أن يُزاد تحميله ثباتيًا (const overloaded) على النحو التالي: #include <iostream> class Integer { public: Integer(int i_): i{i_}{} void print() { std::cout << "int: " << i << std::endl; } void print() const { std::cout << "const int: " << i << std::endl; } protected: int i; }; int main() { Integer i{5}; const Integer &ic = i; i.print(); // يطبع "int: 5" ic.print(); // يطبع "const int: 5" } هذا مبدأ أساسي لصحة الثباتية (const)، إذ يُسمح باستدعاء الدوال التابعة على نُسخ const من خلال جعلها هي ثابتة، وهذا يتيح للدوالّ أن تأخذ النسخ كمؤشرات/مراجع ثابتة إذا لم تكن بحاجة إلى تعديلها، ويسمح هذا للشيفرة بتحديد ما إذا كانت الحالة ستُعدّل عن طريق أخذ معامِلات غير مُعدّلة كمعامِلات ذات تأهيل ثابت const ومعاملات مُعدّلة بدون تأهيل، مما يجعل الشيفرة أكثر أمانًا وأسهل في القراءة. class ConstCorrect { public: void good_func() const { std::cout << "I care not whether the instance is const." << std::endl; } void bad_func() { std::cout << "I can only be called on non-const, non-volatile instances." << std::endl; } }; void i_change_no_state(const ConstCorrect & cc) { std::cout << "I can take either a const or a non-const ConstCorrect." << std::endl; cc.good_func(); // جيد، يمكن استدعاؤها من النسخ الثابتة أو غير الثابتة cc.bad_func(); // خطأ، لا يمكن استدعاؤها إلا من النسخ غير الثابتة } void const_incorrect_func(ConstCorrect & cc) { cc.good_func(); // جيد، يمكن استدعاؤها من النسخ الثابتة أو غير الثابتة cc.bad_func(); // جيد، لا يمكن استدعاؤها إلا من النسخ غير الثابتة } أحد الاستخدامات الشائعة لهذا هو إعلان توابع الوصول (accessors) كثوابت، وتوابع التغيير (mutators) على أنها غير ثابتة، ولا يمكن تعديل عضو من الصنف داخل تابع ثابت. أما إذا كان هناك عضو تحتاج إلى تعديله، كأن تريد قفل std::mutex مثلًا فيمكنك إعلانه كـ mutable: class Integer { public: Integer(int i_): i { i_ } {} int get() const { std::lock_guard < std::mutex > lock { mut }; return i; } void set(int i_) { std::lock_guard < std::mutex > lock { mut }; i = i_; } protected: int i; mutable std::mutex mut; }; زيادة تحميل قوالب الدوال (Function Template Overloading) يمكن زيادة تحميل قوالب الدوالّ وفق نفس القواعد التي نزيد بها تحميل الدوالّ العادية، أي يجب أن يكون للدوالّ المُحمّلة نفس الاسم، ولكن مع أنواع معاملات مختلفة، وتكون زيادة التحميل صالحة إذا كان: نوع القيمة المعادة مختلفًا، أو … إذا كانت قائمة معاملات القالب مختلفة، باستثناء أسماء المعاملات والوسائط الافتراضية (فهُما ليسا جزءًا من التوقيع). تبدو مقارنة نوعا معامِلات بالنسبة لدالة عادية عمليةً سهلة على المُصرّف لأنه يملك كل المعلومات اللازمة، ولكن ربما لا تكون الأنواع داخل القالب قد حُدّدت بعد، لذا فإن القاعدة التي تحكم على مدى تطابق نوعي معامِلات تقريبية هنا، وتقضي بأن الأنواع والقيم المستقلة (non depependend) يجب أن تطابق تهجِئة الأنواع والعبارات التابعة (dependent)، أي يجب أن يتوافقًا مع قاعدة التعريف الواحد [ODR]،مع استثناء أن معامِلات القالب يمكن إعادة تسميتها. لكن إذا كانت التهجئة مختلفة، واختلفت قيمتان داخل النوعين لكنهما يستنسخان دائمًا إلى نفس القيم، فتكون زيادة التحميل غير صالحة (invalid)، لكن لن يكون التشخيص مطلوبًا من المصرِّف. template < typename T > void f(T*) { } template < typename T > void f(T) {} هذا تحميل صالح (valid)، لأنّ "T" و "T *" لهما تهجئتان مختلفتان، لكنّ زيادة التحميل التالية غير صالحة، كما أنّ التشخيص غير مطلوب. template < typename T > void f(T (*x)[sizeof(T) + sizeof(T)]) { } template < typename T > void f(T (*x)[2 * sizeof(T)]) { } هذا الدرس جزء من سلسلة دروس عن C++. ترجمة -بتصرّف- للفصول: Chapter 35: Function Overloading Chapter 37: Function Template Overloading Chapter 105: Overload resolution من كتاب C++ Notes for Professionals
-
أساسيات الأصناف الصنف (class) هو نوع يعرّفه المستخدم، ويُسبق بالكلمة المفتاحية class أو struct أو union، ويشير المصطلح "class" بشكل عام إلى الأصناف غير الاتحاديّة (non-union classes). والصنف مؤلّف من أعضاء يمكن أن تكون أيًا مما يلي: متغيرات أعضاء، وتسمى كذلك متغيِّرات عضوية أو حقولًا (member variables)، توابع، وتسمى كذلك دوالًا تابعة أو دوالًا أعضاءً (member functions)، أنواع عضوية أو أنواع تعريفية (typedefs أو member types) قوالب عضوية أو قوالب أعضاء (member templates) من أيّ نوع: متغير، دالّة، صنف أو قالب. الكلمتان المفتاحيّتان class و struct واللّتان تُسمّيان مفاتيح الأصناف (class keys) متشابهتان إلى حدّ كبير، باستثناء أنّ محدد الوصول الافتراضي للأعضاء والأصناف الأساسية (bases) تكون خاصّة (private) في الأصناف التي صُرِّح عنها باستخدام المفتاح class، وعامّة (public) بالنسبة للأصناف التي صُرِّح عنها باستخدام أحد المفتاحين struct أو union. على سبيل المثال، المُقتطفان التاليان متطابقان: struct Vector { int x; int y; int z; }; // تكافئ class Vector { public: int x; int y; int z; }; بعد التصريح عن صنف، يُضاف نوع جديد إلى برنامجك، ومن الممكن استنساخ كائنات من هذا الصنف على النحو التالي: Vector my_vector; ويمكن الوصول إلى أعضاء الصّنف باستخدام الصياغة النقطيّة. my_vector.x = 10; my_vector.y = my_vector.x + 1; // my_vector.y = 11; my_vector.z = my_vector.y - 4; // my:vector.z = 7; الأصناف والبِنيات النهائية الإصدار ≥ C++ 11 يُمكن حظر اشتقاق صنف بواسطة الكلمة المفتاحية final، وفق الصياغة التالية: class A final { }; ستؤدّي أيُّ محاولة لاشتقاقه إلى خطأ تصريفي: // Compilation error: cannot derive from final class class B : public A {}; يمكن أن تظهر الأصناف النهائية في أيّ مكان في هرميّة الأصناف (class hierarchy): class A {}; // OK. class B final: public A {}; // Compilation error: cannot derive from final class B. class C: public B {}; محددات الوصول (Access specifiers) الكلمة المفتاحية الوصف public الجميع لديهم صلاحية الوصول protected الصنف والأصناف المشتقة منه وأصدقاء الصنف هم من لهم صلاحية الوصول private الصنف وأصدقاء الصنف فقط لديهم حق الوصول 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; } إذا عُرِّف النوع باستخدام الكلمة المفتاحية class، سيكون مُحدِّد الوصول الافتراضي هو private، ولكن إذا عُرِّف باستخدام struct، فإنّ محدِّد الوصول الافتراضي الخاص به سيكون public: struct MyStruct { int x; }; class MyClass { int x; }; MyStruct s; s.x = 9; // عام x خلل في الصياغة، لأنّ MyClass c; c.x = 9; // خاصّ x خلل في الصياغة، لأنّ غالبًا ما تُستخدم مُحدِّدات الوصول لتقييد إمكانية الوصول إلى الحقول والتوابع الداخلية، إذ تجبر تلك المحدِّدات المُبرمج على استخدام واجهة برمجية محدّدة، فمثلًا لفرض استخدام الجالِبات (توابع الجلب - getters) والمُعيِّنات (توابع التعيين - setters) بدلاً من الرجوع مباشرة إلى المتغيرات: class MyClass { public: /* Methods: */ int x() const noexcept { return m_x; } void setX(int const x) noexcept { m_x = x; } private: /* Fields: */ int m_x; }; يُعدُّ استخدام الكلمة المفتاحيّة protected مفيدًا لقصر حق الوصول إلى بعض الوظائف على الأصناف المشتقّة، فمثلًا في الشيفرة التالية، يكون الوصول إلى التابع calculateValue() مقصورًا على الأصناف المشتقّة من الصنف Plus2Base. انظر: struct Plus2Base { int value() noexcept { return calculateValue() + 2; } protected: /* Methods: */ virtual int calculateValue() noexcept = 0; }; struct FortyTwo: Plus2Base { protected: /* Methods: */ int calculateValue() noexcept final override { return 40; } }; لاحظ أنّه يمكن استخدام الكلمة المفتاحية friend لمنح تراخيص استثنائية إلى بعض الدوالّ أو الأنواع من أجل الوصول إلى الأعضاء المحمييّن والخواص. كذلك يمكن استخدام الكلمات المفتاحية public و protected و private لمنح أو تقييد حقّ الوصول إلى الكائنات الفرعية للصنف الأساسي. الوراثة (Inheritance) يمكن للأصناف والبُنى أن تكون بينها علاقات وراثة، فإذا ورث صنف أو بنية B من صنف أو بنية A، فإنّ هذا يعني أنّ B هو أب لـ A. ونقول أنّ B هو صنف أو بنية مشتقة من A، وأنّ A هو الصنف أو البنية الأساسية (base class/struct)، أو نقول اختصارًا "أساس". struct A { public: int p1; protected: int p2; private: int p3; }; // A يرث من B اجعل struct B: A {}; هناك ثلاثة أشكال من الوراثة: public private protected لاحظ أنّ الوراثة الافتراضية تماثل الرؤية الافتراضية للأعضاء: أي أنها تكون عامّة (public) في حال استخدام الكلمة المفتاحية struct، أو خاصّة (private) في حال استخدام الكلمة المفتاحية class. من الممكن اشتقاق صنف من بِنية (أو العكس). ويُتحكَّم في الوراثة الافتراضية هنا بواسطة الصنف الابن/الفرعي (child)، وعليه فإنّ بنيةً مشتقة من صنف ستكون وراثتها الافتراضية عامّة (public)، وصنفٌ (class) مشتق من بنية (struct) ستكون وراثته الافتراضية خاصة (private). الوراثة العامة (public): struct B: public A // `struct B : A` أو { void foo() { p1 = 0; // B عام في p1 p2 = 0; // B محمي p2 p3 = 0; // B خاص في p3 صيغة غير صحيحة، لأن } }; B b; b.p1 = 1; // عام p1 b.p2 = 1; // محمي p2 خطأ b.p3 = 1; // غير قابل للوصول p2 خطأ لأن الوراثة الخاصة (private): struct B: private A { void foo() { p1 = 0; // B خاص في p1 p2 = 0; // B خاص في p2 p3 = 0; // A خاص في p3 خطأ،لأنّ } }; B b; b.p1 = 1; // خاص p3 خطأ،لأنّ b.p2 = 1; // خاص p2 خطأ،لأنّ b.p3 = 1; // غير قابل للوصول p3 خطأ، لأنّ الوراثة المحميّة (protected): struct B: protected A { void foo() { p1 = 0; // B محمي في p1 p2 = 0; // B محمي في p2 p3 = 0; // A خاص في p2 خطأ، لأنّ } }; B b; b.p1 = 1; // محمي p1 خطأ، لأن b.p2 = 1; // محمي p2 خطأ لأنّ b.p3 = 1; // غير قابل للوصول p3 خطأ لأنّ لاحظ أنه على الرغم من أنّ الوراثة المحميّة protected مسموح بها إلا أنّ استخدامها ناد، فمن أمثلة استخدامها في التخصيص الجزئي لصنف أساسي (يشار إليه عادةً باسم "التعددية الشكلية المحكومة". كان يُنظَر في بدايات البرمجة الكائنية (OOP) إلى الوراثة (العامة) كعلاقة انتماء ("IS-A")، وهذا يعني أنّ الوراثة العامة لا تكون صحيحة إلا إذا كانت نُسخ الصنف المشتق أيضًا نُسخًا من الصنف الأساسي، وقد استُبدِلَ بهذا المبدأ مبدأ آخر، وهو مبدأ Liskov للتعويض: يقال عادة إنّ الوراثة الخاصة (Private inheritance) تجسّد علاقة مختلفة تمامًا، إذ يُعبَّر عنها عادة بالصيغة "منفَّذة وفق" (تُدعى أحيانًا علاقة "HAS-A"). على سبيل المثال، يمكن أن يرث صنف Stack بشكل خاص (privately) من صنف Vector، وتشبه الوراثة الخاصة علاقة التجميع (aggregation) أكثر من شبهها بعلاقة الوراثة العامة. ولا تكادُ تُستخدم الوراثة المحمية (Protected inheritance) على الإطلاق، ولا يوجد اتفاق عام على نوع العلاقة التي تجسّدها. الصداقة (Friendship) تُستخدم الكلمة المفتاحية friend لإعطاء الأصناف والدوالّ الأخرى حق الوصول إلى أعضاء الصنف الخواص والمحميّين، حتى لو كانت مُعرّفة خارج نطاق الصنف. class Animal { private: double weight; double height; public: friend void printWeight(Animal animal); friend class AnimalPrinter; // << من الاستخدامات الشائعة للدوالّ الصديقة هو زيادة تحميل المعامل friend std::ostream & operator << (std::ostream & os, Animal animal); }; void printWeight(Animal animal) { std::cout << animal.weight << "\n"; } class AnimalPrinter { public: void print(const Animal & animal) { // يُسمح بالدخول إلى الأعضاء الخاصة الآن std::cout << animal.weight << ", " << animal.height << std::endl; } } std::ostream& operator<<(std::ostream& os, Animal animal) { os << "Animal height: " << animal.height << "\n"; return os; } int main() { Animal animal = { 10, 5 }; printWeight(animal); AnimalPrinter aPrinter; aPrinter.print(animal); std::cout << animal; } الناتج: 10 10, 5 Animal height: 5 الوراثة الوهمية (Virtual Inheritance) يمكنك استخدام الكلمة المفتاحية virtual وفق الصياغة التالية: struct A{}; struct B: public virtual A{}; إذا كان لصنف B صنفٌ أساسيّ وهمي هو A، فهذا يعني أنّ A سوف يكون في أكثر صنف مشتق، وعليه فإنّ الصنف الأكثر اشتقاقًا (most derived class) سيكون مسؤولًا عن تهيئة ذلك الصنف الأساسي الوهمي (virtual base): struct A { int member; A(int param) { member = param; } }; struct B: virtual A { B(): A(5) {} }; struct C: B { C(): /*A(88)*/ {} }; void f() { C object; // `A` لم يهيّئ الصنف الأساسي الوهمي C خطأ، لأنّ } إذا ألغينا تعليق /* A (88) */، فلن يحدث خطأ، لأنّ C سيكون قد هيّأ أساسه الوهمي (أو صنفه الأب) A. كذلك، لاحظ أنه عند إنشاء object، فإنّ الصنف الأكثر اشتقاقا (most derived class) هو C، وعليه فإنّ C هو المسؤول عن إنشاء (استدعاء مُنشئ) A، ومن ثم فإنّ قيمة A::member ستكون 88، وليس 5 (كما سيكون الأمر لو أنشأنا كائنًا من النوع B). هذا مفيد لحل مشكلة الماسّة: A A A / \ | | B C B C \ / \ / D D الوراثة الوهمية vs الوراثة العادية يرث كلٌّ من B و C من A، ويرث D من B و C، لذا فهناك نُسختان من A في D! قد ينتج عن هذا بعض الغموض عند محاولة الوصول إلى عضو من A عبر D، ذلك أنّه ليس للمُصرّف أي طريقة لتَخمين الصنف الذي تريد الوصول من خلاله إلى العضو (أهو الصنف الذي يرثه B، أو الصنف الذي ورثه C؟) . تحل الوراثة الوهميّة (Virtual inheritance) هذه المشكلة على النحو التالي: نظرًا لأنّ الأصناف الأساسية الوهميّة تقبع في الكائنات الأكثر اشتقاقًا (most derived object)، فستكون هناك نُسخة واحدة فقط من A في D. struct A { void foo() {} }; struct B: public /*virtual*/ A {}; struct C: public /*virtual*/ A {}; struct D: public B, public C { void bar() { foo(); // نستدعي؟ foo خطأ، فأي // B::foo() أم C::foo() هل } }; إزالة التعليقات سيُجلِّي هذا الغموض. الوراثة الخاصة: تقييد واجهة الصنف الأساسي الوراثة الخاصة مفيدة لتقييد الواجهة العامة للصنف: class A { public: int move(); int turn(); }; class B: private A { public: using A::turn; }; B b; b.move(); // خطأ في التصريف b.turn(); // OK يمنع هذا المنظور الوصول إلى التوابع العامة لـ A عبر مؤشّرٍ أو مرجع يشير إلى A: B b; A& a = static_cast<A&>(b); // خطأ في التصريف في حالة الوراثة العامة، سيوفّر مثل هذا التحويل إمكانية الوصول إلى جميع التوابع العامة للصنف A، رغم وجود طرق بديلة لمنع ذلك من داخل الصنف المشتق B، مثل الإخفاء: class B: public A { private: int move(); }; أو استخدام الكلمة المفتاحية private: class B: public A { private: using A::move; }; وفي كلا الحالتين، سيكون من الممكن القيام بما يلي: B b; A& a = static_cast<A&>(b); // جائز في الوراثة العامة a.move(); // OK الوصول إلى أعضاء الصنف للوصول إلى متغيرات الأعضاء أو الدوالّ العضوية لكائن من صنف ما، فإننا نستخدم العامل .: struct SomeStruct { int a; int b; void foo() {} }; SomeStruct var; // var في a الدخول إلى الحقل std::cout << var.a << std::endl; // var في b تعيين الحقل var.b = 1; // استدعاء تابع var.foo(); يُستخدم العامل -> عادة عند محاولة الوصول إلى أعضاء الصنف عبر مؤشّر. كخيار بديل، يمكن تحصيل النُسخة واستخدام العامل .، لكنّ هذا أقل شيوعًا: struct SomeStruct { int a; int b; void foo() {} }; SomeStruct var; SomeStruct *p = &var; // عبر مؤشّر a الوصول إلى المتغير العضو std::cout << p -> a << std::endl; std::cout << (*p).a << std::endl; // عبر مؤشّر b تعيين متغير العضو p -> b = 1; (*p).b = 1; // استدعاء دالة تابعة عبر مؤشّر p -> foo(); (*p).foo(); للوصول إلى أعضاء الصنف الساكنة (static class members)، فإنّنا نستخدم العامل ::، ولكن مع اسم الصنف وليس نسخته، وكخيار بديل، يمكن الوصول إلى الأعضاء الساكنة من نُسخة أو مُؤشّر--إلى-نُسخة باستخدام العامل . أو -> على الترتيب، وبنفس الصياغة المُستخدمة للوصول إلى الأعضاء غير الساكنة. struct SomeStruct { int a; int b; void foo() {} static int c; static void bar() {} }; int SomeStruct::c; SomeStruct var; SomeStruct* p = &var; // SomeStruct في البنية c تعيين الحقل الساكن SomeStruct::c = 5; // p و var عبر SomeStruct في البنية c تعيين متغير العضو الساكن var.a = var.c; var.b = p->c; // استدعاء دالة تابعة ساكنة SomeStruct::bar(); var.bar(); p->bar(); تفصيل العامل -> ضروريّ لأنّ عامل الوصول العضوي . له الأسبقية على عامل التحصيل *، وقد يتوقع المرء أنّ *p.a سيحصل p (لينتج عنه مرجع إلى الكائن المشار إليه من قِبل p) ثم يصل إلى العضو a. ولكن في الواقع، فإنّه يحاول الوصول إلى العضو a من p قبل أن يحصله، أي أن *p.a تكافئ *(p.a). في المثال أعلاه، قد ينتج عن هذا خطأ في التصريف لسببين: أولاً، p مؤشّر وليس له عضو a. ثانياً، a عدد صحيح، وبالتالي لا يمكن تحصيله. أحد الحلول الممكنة -رغم عدم شيوعه- لهذه المشكلة هو التحكّم بشكل صريح (explicitly) في الأسبقية عبر الأقواس: (*p).a. وهناك حل أكثر شيوعًا، وهو استخدام العامل ->، وهو حل مختصر لتحصيل المؤشّر ثم الوصول إليه. بمعنى آخر (*p).a تكافئ p->a. العامل :: هو عامل النطاق (scope operator)، ويُستخدم بنفس طريقة الوصول إلى عضو في فضاء الاسم (namespace). ذلك أنّ الأعضاء الساكنة في الصنف تعدٌّ في نطاق ذلك الصنف، لكنها لا تُعدُّ أعضاءً في نُسخ ذلك الصنف. يُسمح أيضًا باستخدام . و -> المعتادتان مع الأعضاء الساكنة، على الرغم من أنهما ليستا عضوتين في النُسخ لأسباب تاريخية، وهذا مفيد لكتابةِ الشيفرات العامة في القوالب لأنّ المُستدعِي لا يعنيه إن كان التابع ساكنًا أو غير ساكن. أنواع الأعضاء، والأسماء البديلة / الكُنى يمكن لصنف class أو بنية struct تعريف الكُنى/الأسماء البديلة (aliases) لنوع عضوي (member type)، وهي أسماء بديلة للنّوع توجد داخل الصنف نفسه وتُعامل كأعضاء منه. struct IHaveATypedef { typedef int MyTypedef; }; struct IHaveATemplateTypedef { template < typename T > using MyTemplateTypedef = std::vector < T > ; }; يمكن الوصول إلى هذه التعريفات النوعية (typedefs)، مثل الأعضاء الساكنة، باستخدام عامل النطاق ::: IHaveATypedef::MyTypedef i = 5; // عدد صحيح i IHaveATemplateTypedef::MyTemplateTypedef<int> v; // std::vector<int> من النوع v يُسمح لكُنى الأنواع العضوية -كما هو الحال مع أسماء النوع البديلة (type aliases) العادية- أن تشير إلى أيّ نوع سبق تعريفه أو تكنِيته (aliased) من قبل، لكن ليس بعد تعريفه. وبالمثل، يمكن أن يشير التعريف النوعي (typedef) الموجود خارج الصنف إلى أيّ تعريف نوعي يمكن الوصول إليه داخل الصنف، بشرط أن يأتي بعد تعريف الصنف. struct IHaveATypedef { typedef int MyTypedef; }; struct IHaveATemplateTypedef { template < typename T > using MyTemplateTypedef = std::vector < T > ; }; template < typename T > struct Helper { T get() const { return static_cast < T > (42); } }; struct IHaveTypedefs { // typedef MyTypedef NonLinearTypedef; // سيحدث خطأ في حال إلغاء التعليق typedef int MyTypedef; typedef Helper < MyTypedef > MyTypedefHelper; }; IHaveTypedefs::MyTypedef i; // x_i is an int. IHaveTypedefs::MyTypedefHelper hi; // x_hi is a Helper<int>. typedef IHaveTypedefs::MyTypedef TypedefBeFree; TypedefBeFree ii; // ii is an int. يمكن التصريح عن أسماء النوع العضوي البديلة عبر مستوى وصول (access level)، وستحترم معدَّل الوصول المناسب: class TypedefAccessLevels { typedef int PrvInt; protected: typedef int ProInt; public: typedef int PubInt; }; TypedefAccessLevels::PrvInt prv_i; // خاص TypedefAccessLevels::PrvInt خطأ، لأن TypedefAccessLevels::ProInt pro_i; // محمي TypedefAccessLevels::ProInt خطأ، لأن TypedefAccessLevels::PubInt pub_i; // حسنا class Derived: public TypedefAccessLevels { PrvInt prv_i; // خاص TypedefAccessLevels::PrvInt خطأ، لأنّ ProInt pro_i; // حسنا PubInt pub_i; // حسنا }; يساعد هذا على توفير قدر من التجريد، مما يسمح لمصمّم الصنف بتغيير عمله الداخلي دون تعطيل الشّيفرات التي تعتمد عليه. class Something { friend class SomeComplexType; short s; // ... public: typedef SomeComplexType MyHelper; MyHelper get_helper() const { return MyHelper(8, s, 19.5, "shoe", false); } // ... }; // ... Something s; Something::MyHelper hlp = s.get_helper(); في هذه الحالة، إذا تغير الصنف المساعد من SomeComplexType إلى نوع آخر فلن تحتاج إلّا إلى تعديل تصريح typedef و friend طالما يوفّر الصنف المساعد نفس الوظيفة، كما أنّ أيّ شيفرة تستخدمه كـ Something::MyHelper بدلاً من تحديده باسمه لن تحتاج إلى أيّ تعديلات. وهكذا نقلّل مقدار الشيفرة التي يجب تعديلها عند تغييرالتنفيذ إذ لن نحتاج إلى تغيير اسم النوع إلا في مكان واحد. يمكنك أيضًا دمج هذا مع decltype، إذا رغبت في ذلك. class SomethingElse { AnotherComplexType < bool, int, SomeThirdClass > helper; public: typedef decltype(helper) MyHelper; private: InternalVariable < MyHelper > ivh; // ... public: MyHelper& get_helper() const { return helper; } // ... }; في هذه الحالة، سيؤدي تغيير تقديم SomethingElse::helper إلى تغيير التعريف النوعي (typedef) تلقائيًا بفضل decltype. هذا يقلّل من عدد التعديلات اللازمة عندما نريد تغيير الصنف المساعد helper، كما يقلّل من الأخطاء البشرية. إذا لم يكن اسم النوع يُستخدَم إلّا مرّة واحدة أو مرتين داخليًا، ولم يكن يُستخدَم أبدًا في الخارج على سبيل المثال، فليست هناك حاجة لتوفير كُنية له. أمّا إذا كان يُستخدم مئات أو آلاف المرّات داخل المشروع أو كان له اسم طويل، فقد يكون من المفيد توفيره كتعريف نوعي (typedef) بدلًا من استخدامه باسمه الأساسي. يجب عليك أن تدرس الأمر قبل اختيار الطريقة التي تريد العمل بها. يمكن أيضًا استخدام هذا مع أصناف القوالب لتوفير حقّ الوصول إلى معاملات القالب من خارج الصنف. template < typename T > class SomeClass { // ... public: typedef T MyParam; MyParam getParam() { return static_cast < T > (42); } }; template < typename T > typename T::MyParam some_func(T & t) { return t.getParam(); } SomeClass < int > si; int i = some_func(si); يَشيع استخدام هذا مع الحاويات التي عادة ما توفر نوع العنصر الخاص بها (والأنواع المساعِدة الأخرى) كأسماء بديلة لنوع عضوي (member type aliases). وتوفّر معظم الحاويات الموجودة في المكتبة القياسية للغة C++ الأنواع المساعدة الاثنتي عشر التالية، إضافة إلى أنواع خاصة أخرى. template < typename T > class SomeContainer { // ... public: // توفير نفس الأنواع المساعدة للحاويات القياسية typedef T value_type; typedef std::allocator<value_type> allocator_type; typedef value_type& reference; typedef const value_type& const_reference; typedef value_type* pointer; typedef const value_type* const_pointer; typedef MyIterator<value_type> iterator; typedef MyConstIterator<value_type> const_iterator; typedef std::reverse_iterator<iterator> reverse_iterator; typedef std::reverse_iterator<const_iterator> const_reverse_iterator; typedef size_t size_type; typedef ptrdiff_t difference_type; }; كان توفير "قالب typedef" شائعًا قبل الإصدار C++ 11، وقد أصبحت هذه الطريقة أقل شيوعًا مع إضافة ميزة كُنَى القوالب، لكنّها ما تزال مفيدة في بعض المواقف، وتُدمج مع كُنى القوالب في مواقف أخرى، قد يكون هذا مفيدًا جدًا في الحصول على مكوِّنات فردية من نوع معقّد، مثل مؤشّر دالّة ما. كذلك يكثر استخدام الاسم type للاسم البديل للنوع. انظر: template < typename T > struct TemplateTypedef { typedef T type; } TemplateTypedef < int > ::type i; // هو رقم صحيح i. يُستخدم هذا غالبًا مع الأنواع التي لها عدّة معاملات قوالب، لتوفير اسم بديل يُعرّف معاملًا واحدًا أو أكثر. template < typename T, size_t SZ, size_t D > class Array { /* ... */ }; template < typename T, size_t SZ > struct OneDArray { typedef Array < T, SZ, 1 > type; }; template < typename T, size_t SZ > struct TwoDArray { typedef Array < T, SZ, 2 > type; }; template < typename T > struct MonoDisplayLine { typedef Array < T, 80, 1 > type; }; OneDArray < int, 3 > ::type arr1i; // Array<int, 3, 1> مصفوفة من النوع arr1i TwoDArray < short, 5 > ::type arr2s; // Array<short, 5, 2> مصفوفة من النوع arr2s MonoDisplayLine < char > ::type arr3c; // Array<char, 80, 1> مصفوفة من النوع arr3c الأصناف والبِنيات المُتشعِّبة (Nested Classes/Structures) يمكن أن يحتوي صنف class أو بنية struct على تعريف صنف أو بنية أخرى داخله، وسيُسمى "صنفًا متشعبًا" (nested class)، بينما يُشار إلى الصنف الذي يحتوي التعريف "بالصنف المُحيط" (enclosing class)، ويعدُّ الصنف المتشعِّب عضوًا في الصنف المُحيط لكنه منفصل عنه. struct Outer { struct Inner {}; }; يمكن الوصول إلى الأصناف المتشعِّبة من خارج الصنف المُحيط باستخدام عامل النطاق، أمّا من داخل الصنف المُحيط، فيمكن استخدام الأصناف المتشعِّبة بدون مؤهلات: struct Outer { struct Inner {}; Inner in ; }; // ... Outer o; Outer::Inner i = o.in; وكما هو الحال مع الأصناف والبنيات غير المُتشعِّبة، يمكن تعريف التوابع والمتغيّرات الساكنة إما داخل الصنف المتشعِّب، أو في فضاء الاسم (namespace) المُحيط، لكن لا يمكن تعريفها داخل الصنف المُحيط لأنه يُعتبر صنفًا مختلفًا عن الصنف المتشعِّب. // خطأ struct Outer { struct Inner { void do_something(); }; void Inner::do_something() {} }; // ممتاز struct Outer { struct Inner { void do_something(); }; }; void Outer::Inner::do_something() {} بالمثل، وكما هو الحال مع الأصناف غير المتشعِّبة، يمكن التصريح مُسبقًا (forward declared) عن الأصناف المتشعِّبة ثم تعريفها لاحقًا، شريطة أن تُعرّف قبل استخدامها. class Outer { class Inner1; class Inner2; class Inner1 {}; Inner1 in1; Inner2* in2p; public: Outer(); ~Outer(); }; class Outer::Inner2 {}; Outer::Outer(): in1(Inner1()), in2p(new Inner2) {} Outer::~Outer() { if (in2p) { delete in2p; } } الإصدار <C++ 11 لم يكن للأصناف المتشعِّبة قبل الإصدار C++ 11 حق الوصول إلّا إلى أسماء الأنواع (type names) والأعضاء الساكنة (static) والعدّادات (enumerators) من الصنف المُحيط، ولم تكن الأعضاء الأخرى المعرَّفة في الصنف المحيط متاحة. الإصدار ≥ C++ 11 وبدءًا من الإصدار C++ 11، تُعامل الأصناف المتشعِّبة وأعضاؤها كما لو كانت صديقة (friend) للصنف المُحيط، وصار بإمكانها الوصول إلى جميع أعضائه وفقًا لقواعد الوصول المعتادة، وإذا كان أعضاء الصنف المتشعِّب يحتاجون إلى تقييم عضو ساكن أو أكثر من الصنف المُحيط، فيجب أن تُمرّر نُسخة إليها: class Outer { struct Inner { int get_sizeof_x() { return sizeof(x); // غير مُقيّم، لذلك هناك حاجة إلى نسخة x } int get_x() { return x; // لا يمكن الوصول إلى الأعضاء غير الساكنين بدون نسخة } int get_x(Outer& o) { return o.x; // الوصول إلى الأعضاء الخاصة Inner بإمكان Outer كعضو من } }; int x; }; بالمقابل، لا يُعامَل الصنف المُحيط كصديق للصنف المتشعِّب، وعليه لا يمكنه الوصول إلى أعضائه دون الحصول على إذن صريح. class Outer { class Inner { // friend class Outer; int x; }; Inner in ; public: int get_x() { return in.x; // Error: int Outer::Inner::x is private. // لإصلاح المشكلة "friend" الغ تعليق } }; لا يُعدّ أصدقاء الصنف المتشعِّب تلقائيًا أصدقاءً للصنف المحيط؛ فالصداقة مع الصنف المحيط ينبغي التصريح عنها بشكل منفصل. بالمقابل، بما أن الصنف المُحيط لا يُعدُّ صديقًا للصنف المتشعِّب تلقائيًا ، فلن يكون أصدقاء الصنف المُحيط أصدقاءً للصنف المتشعِّب. class Outer { friend void barge_out(Outer& out, Inner& in); class Inner { friend void barge_in(Outer& out, Inner& in); int i; }; int o; }; void barge_in(Outer & out, Outer::Inner & in ) { int i = in .i; // جيد int o = out.o; // خاص int Outer::o خطأ: لأن } void barge_in(Outer& out, Outer::Inner& in) { int i = in .i; // خاص int Outer::Inner::i خطأ: لأن int o = out.o; // جيد } كما هو الحال مع جميع أعضاء الصنف الآخرين، لا يمكن تسمية الأصناف المتشعِّبة من خارج الصنف إلا إذا كانت عامّة (public). لكن يُسمح لك بالوصول إليها بغض النظر عن مُعدِّل الوصول طالما أنك لا تُسمّيها صراحة. class Outer { struct Inner { void func() { std::cout << "I have no private taboo.\n"; } }; public: static Inner make_Inner() { return Inner(); } }; // ... Outer::Inner oi; // خاص Outer::Inner خطأ: لأن auto oi = Outer::make_Inner(); // جيد oi.func(); // جيد Outer::make_Inner().func(); // جيد يمكنك أيضًا إنشاء كُنية نوع للصنف المتشعِّب، وإذا كانت كُنية النوع موجودة في الصنف المحيط فيمكن أن يكون للنوع المتشعِّب وكُنية النوع مُحدِّدا وصول مختلفّين. وإذا كانت كُنية النوع موجودة خارج الصنف المُحيط، فإن ذلك يتطلب إمّا أن يكون الصنف المتشعِّب عامًّا (public)، أو نوع التعريف (typedef)، أيهما. class Outer { class Inner_ {}; public: typedef Inner_ Inner; }; typedef Outer::Inner ImOut; // جيد typedef Outer::Inner_ ImBad; // خطأ // ... Outer::Inner oi; // جيد Outer::Inner_ oi; // خطأ ImOut oi; // جيد كما هو الحال مع الأصناف الأخرى، يمكن الاشتقاق من الأصناف المتشعِّبة، ويمكنها أن تشتق من أصناف أخرى. struct Base {}; struct Outer { struct Inner: Base {}; }; struct Derived: Outer::Inner {}; هذا مفيد في الحالات التي يكون هناك صنف مشتقّ من الصنف المُحيط، إذ يسمح ذلك للمُبرمج بتحديث الصنف المتشعِّب عند الضرورة، ويمكن دمج ذلك مع تعريف نوعيٍّ (typedef) لتوفير اسم ثابت لكل صنف متشعِّب من الصنف المُحيط: class BaseOuter { struct BaseInner_ { virtual void do_something() {} virtual void do_something_else(); } b_in; public: typedef BaseInner_ Inner; virtual ~BaseOuter() = default; virtual Inner & getInner() { return b_in; } }; void BaseOuter::BaseInner_::do_something_else() {} // --- class DerivedOuter: public BaseOuter { // BaseOuter::BaseInner_ is private لاحظ استخدام النوع التعريفي المؤهل struct DerivedInner_: BaseOuter::Inner { void do_something() override {} void do_something_else() override; } d_in; public: typedef DerivedInner_ Inner; BaseOuter::Inner & getInner() override { return d_in; } }; void DerivedOuter::DerivedInner_::do_something_else() {} // ... // BaseOuter::BaseInner_::do_something(); استدعاء BaseOuter * b = new BaseOuter; BaseOuter::Inner & bin = b -> getInner(); bin.do_something(); b -> getInner().do_something(); // DerivedOuter::DerivedInner_::do_something(); استدعاء BaseOuter * d = new DerivedOuter; BaseOuter::Inner & din = d -> getInner(); din.do_something(); d -> getInner().do_something(); في الحالة أعلاه، يقدّم كل من الصِّنفين BaseOuter و DerivedOuter النوع العُضوي Inner كـ BaseInner_ و DerivedInner_ على الترتيب، ويسمح هذا بااشتقاق الأنواع المتشعِّبة دون تعطيل واجهة الصِّنف المُحيط، كما يسمح باستخدام النوع المتشعِّب بأشكال متعددة. البِنيات والأصناف غير المُسمّاة (Unnamed struct/class) يُسمح باستخدام البِنيات غير المُسمّاة (نوع ليس له اسم): void foo() { struct /* No name */ { float x; float y; } point; point.x = 42; } أو struct Circle { struct /* بلا اسم */ { float x; float y; } center; // لكن مع أعضاء لها اسم float radius; }; يمكن أن نكتب الآن: Circle circle; circle.center.x = 42.f; لكن لا يُسمح بالبنيات المجهولة - anonymous struct - (نوع غير مسمّى وكائن غير مسمّى) struct InvalidCircle { struct /* بلا اسم */ { float centerX; float centerY; }; // لا أعضاء كذلك float radius; }; ملاحظة: تسمح بعض المُصرِّفات بالبُنى المجهولة كملحقات. الإصدار ≥ C++ 11 يمكن النظر إلى تعبيرات لامدا كبنية خاصة غير مُسمّاة. تسمح decltype بالحصول على نوع بنية غير مسمّاة: decltype(circle.point) otherPoint; يمكن أن تكون نُسخ البنيات غير المسمّاة معاملات لتابع قالب (template method): void print_square_coordinates() { const struct {float x; float y;} points[] = { {-1, -1}, {-1, 1}, {1, -1}, {1, 1} }; // template <class T, std::size_t N> std::begin(T (&)[N]) بالنسبة لمجال يعتمد على for (const auto& point : points) { std::cout << "{" << point.x << ", " << point.y << "}\n"; } decltype(points[0]) topRightCorner{1, 1}; auto it = std::find(points, points + 4, topRightCorner); std::cout << "top right corner is the " << 1 + std::distance(points, it) << "th\n"; } أعضاء الصنف الساكنة يُسمح للصنف أن يكون له أعضاء ساكنة (static) قد تكون متغيرات أو دوالًا، وتعدُّ هذه الأعضاء في نطاق الصنف لكنها لا تُعامل كأعضاء عاديّة، إذ أنها تتميّز بمدة تخزين ثابتة (تستمرّ في الوجود من بداية البرنامج وحتى نهايته)، ولا ترتبط بنُسخة معيّنة من الصنف، ولا يوجد منها سوى نسخة واحدة للصنف بأكمله. class Example { static int num_instances; // حقل ساكن int i; // حقل غير ساكن public: static std::string static_str; // حقل ساكن static int static_func(); // تابع ساكن // يجوز للتوابع غير الساكنة تعديل الحقول الساكنة Example() { ++num_instances; } void set_str(const std::string & str); }; int Example::num_instances; std::string Example::static_str = "Hello."; // ... Example one, two, three // الخاصة به، مثل ما يلي “i” كل مثال لديه : // (&one.i != &two.i) // (&one.i != &three.i) // (&two.i != &three.i). // انظر ما يلي ، “num_instances” تتشارك الأمثلة الثلاثة: // (&one.num_instances == &two.num_instances) // (&one.num_instances == &three.num_instances) // (&two.num_instances == &three.num_instances) لا تُعدُّ الحقول الساكنة مُعرَّفة داخل الصنف ولكنّها تُعدّ مُصرّحة فقط، وعليه سيكون تعريفها خارج تعريف الصنف. يُسمح للمبرمج بتهيئة المتغيّرات الساكنة في التعريف لكنه غير ملزَم بهذا، وعند تعريف المتغيرات العضوية تُحذف الكلمة المفتاحية static. class Example { static int num_instances; // تصريح public: static std::string static_str; // تصريح // ... }; int Example::num_instances; // تعريف مُهيّأ صفريًا. std::string Example::static_str = "Hello."; // تعريف لهذا السبب، يمكن أن تكون المتغيرات الساكنة أنواعًا غير مكتملة (بخلاف void)، طالما عُرِّفت لاحقًا كنوع كامل. struct ForwardDeclared; class ExIncomplete { static ForwardDeclared fd; static ExIncomplete i_contain_myself; static int an_array[]; }; struct ForwardDeclared {}; ForwardDeclared ExIncomplete::fd; ExIncomplete ExIncomplete::i_contain_myself; int ExIncomplete::an_array[5]; يمكن تعريف الدوال التابعة الساكنة داخل أو خارج تعريف الصنف كما هو حال الدوال التابعة العادية، وكما هو الحال مع المتغيرات العضوية الساكنة، تُحذَف الكلمة المفتاحية static في حال تعريف العضو الساكن خارج تعريف الصنف. // بالنسبة للمثال أعلاه، إما class Example { // ... public: static int static_func() { return num_instances; } // ... void set_str(const std::string& str) { static_str = str; } }; // أو class Example { /* ... */ }; int Example::static_func() { return num_instances; } void Example::set_str(const std::string& str) { static_str = str; } في حال التصريح عن حقل على أنه ثابت (const) وليس متغيّرًا (volatile)، وكان من نوع عددي صحيح (integral) أو تِعدادي (enumeration)، فيمكن تهيئته عند التصريح عنه داخل تعريف الصنف. enum E { VAL = 5 }; struct ExConst { const static int ci = 5; // جيد. static const E ce = VAL; // جيد. const static double cd = 5; // خطأ. static const volatile int cvi = 5; // خطأ. const static double good_cd; static const volatile int good_cvi; }; const double ExConst::good_cd = 5; // جيد. const volatile int ExConst::good_cvi = 5; // جيد. الإصدار ≥ C++ 11 بدءًا من الإصدار C++ 11، يمكن تعريف المتغيرات العضوية الساكنة من الأنواع LiteralType (أنواع يمكن إنشاؤها في وقت التصريف وفقًا لقواعد التعبيرات الثابتة constexpr) كتعبيرات ثابتة؛ لكن يجب أن تُهيّأ داخل تعريف الصنف. struct ExConstexpr { constexpr static int ci = 5; // جيد. static constexpr double cd = 5; // جيد. constexpr static int carr[] = { 1, 1, 2 }; // جيد. static constexpr ConstexprConstructibleClass c{}; // جيد. constexpr static int bad_ci; // Error. }; constexpr int ExConstexpr::bad_ci = 5; // خطأ كذلك. في حال تمّ أخذ عنوان (odr-used) لمتغير عضوي ساكن من النوع الثابت (const) أو constexpr، أو عُيَّن إلى مرجع فينبغي أن يكون له تعريف منفصل خارج تعريف الصنف، ولا يُسمح لهذا التعريف باحتواء مُهيّئ. struct ExODR { static const int odr_used = 5; }; // const int ExODR::odr_used; const int* odr_user = & ExODR::odr_used; // خطأ، ألغ تعليق السطر أعلاه لحل المشكلة يمكن الوصول إلى الأعضاء الساكنة باستخدام عامل النطاق :: بما أنها لأنّها غير مرتبطة بنُسخة معيّنة. std::string str = Example::static_str; يمكن أيضًا الوصول إليها كما لو كانت أعضاءً عادية وغير ساكنة، هذا له دلالة تاريخية ولكنه يُستخدم بشكل أقل من عامل النطاق لأنه يمنع أيّ لبس بخصوص ما إذا كان العضو ساكنًا أم لا. Example ex; std::string rts = ex.static_str; يمكن لأعضاء الصنف الوصول إلى الأعضاء الساكنة دون الحاجة إلى تأهيل نطاقهم (qualifying their scope)، كما هو الحال مع أعضاء الصنف غير الساكنة. class ExTwo { static int num_instances; int my_num; public: ExTwo() : my_num(num_instances++) {} static int get_total_instances() { return num_instances; } int get_instance_number() const { return my_num; } }; int ExTwo::num_instances; لا يمكن أن تكون الأعضاء الساكنة قابلة للتغيير (mutable)، وليست في حاجة لهذا لأنها غير مرتبطة بأيّ نُسخة، أمّا مسـالة أنّ النُسخة ثابتة أم لا فليس لها أيّ تأثير على الأعضاء الساكنة. struct ExDontNeedMutable { int immuta; mutable int muta; static int i; ExDontNeedMutable(): immuta(-5), muta(-5) {} }; int ExDontNeedMutable::i; // ... const ExDontNeedMutable dnm; dnm.immuta = 5; // Error: Can't modify read-only object. dnm.muta = 5; // يمكن إعادة كتابة الحقول القابلة للتغيير من كائن ثابت dnm.i = 5; // يمكن إعادة كتابة الأعضاء الساكنة بغض النظر عن ثباتيّة النسخة تحترم الأعضاء الساكنة مُحدِّدات الوصول، تمامًا مثل الأعضاء غير الساكنة. class ExAccess { static int prv_int; protected: static int pro_int; public: static int pub_int; }; int ExAccess::prv_int; int ExAccess::pro_int; int ExAccess::pub_int; // ... int x1 = ExAccess::prv_int; // خاص int ExAccess::prv_int خطأ، لأنّ int x2 = ExAccess::pro_int; // محمي int ExAccess::pro_int خطأ، لأنّ int x3 = ExAccess::pub_int; // جيّد بحُكم أنّها غير مرتبطة بنُسخة معيّنة، فلا تمتلك التوابع الساكنة المؤشّرُ this؛ وعليه لا تستطيع الوصول إلى الحقول غير الساكنة إلا في حال تمرير نُسخة إليها. class ExInstanceRequired { int i; public: ExInstanceRequired() : i(0) {} static void bad_mutate() { ++i *= 5; } // خطأ static void good_mutate(ExInstanceRequired& e) { ++e.i *= 5; } // جيد }; ونظرًا لعدم امتلاكها للمؤشّر this، فلا يمكن تخزين عناوينها في مؤشّرات الدوال التابعة (pointers-to-member-functions)، وتُخزّن بدلاً من ذلك في مؤشّرات الدّوال (pointers-to-functions) العادية. struct ExPointer { void nsfunc() {} static void sfunc() {} }; typedef void (ExPointer::* mem_f_ptr)(); typedef void( * f_ptr)(); mem_f_ptr p_sf = &ExPointer::sfunc; // خطأ f_ptr p_sf = &ExPointer::sfunc; // جيد نظرًا لعدم امتلاكها لمؤشّر this، فهي لا ثابتة (const) ولا متغيّرة (volatile)، ولا يمكن أن يكون لها مؤهّلات مرجعية (ref-qualifiers). كما لا يمكنها أن تكون وهميّة كذلك. struct ExCVQualifiersAndVirtual { static void func() {} // جيد static void cfunc() const {} // خطأ static void vfunc() volatile {} // خطأ static void cvfunc() const volatile {} // خطأ static void rfunc() & {} //خطأ static void rvfunc() && {} // خطأ virtual static void vsfunc() {} // خطأ static virtual void svfunc() {} // خطأ }; وبما أن المتغيرات العضوية الساكنة لا ترتبط بنُسخة معينة، فيُتعَامل معها كمتغيرات عامّة استثنائيّة (special global variables)؛ فتُنشأ عند بدء تشغيل البرنامج ولا تُحذف حتى إنهائه، بغض النظر عمّا إذا كانت هناك أيّ نُسخ موجودة بالفعل من الصنف. ولا توجد إلا نسخة (copy) واحدة فقط من كل حقل ساكن (ما لم يُصرَّح بالمتغير كـ thread_local - الإصدار C++ 11 أو أحدث - وفي هذه الحالة تكون هناك نُسخة واحدة لكل خيط thread). والمتغيرات العضوية الساكنة لها نفس ارتباط (linkage) الصِّنف، سواء كان للصنف ارتباط خارجي أو داخلي، ولا يُسمح للأصناف المحلية (Local classes) والأصناف غير المُسمّاة (unnamed classes) أن يكون لها أعضاء ساكنة. الوراثة المتعددة (Multiple Inheritance) إلى جانب الوراثة الفردية (single inheritance): class A {}; class B : public A {}; فهناك مفهوم الوراثة المتعددة في C++: class A {}; class B {}; class C : public A, public B {}; سوف يرث C الآن من A و B في الوقت ذاته. ملاحظة: كن حذرًا إذ قد يؤدّي هذا إلى بعض الغموض في حال استخدام نفس الأسماء في عدّة أصناف أو بنيات موروثة. الغموض في الوراثة المتعددة قد تكون الوراثة المتعدّدة مفيدة في بعض الحالات ولكنها قد تخلق بعض المشاكل، فعلى سبيل المثال: لنفترض أنّ صنفين أساسيَن يحتويان على دوال بنفس الاسم، وأنّ الصنف المشتق منهما لم يُعِدْ تعريف تلك الدالة، فإن حاولت الوصول إلى تلك الدالة عبر كائن من الصنف المُشتق، فسيعرِض المُصرّف خطأً لأنه لا يستطيع تحديد الدالة التي يجب استدعاؤها. إليك مثالًا على ذلك: class base1 { public: void funtion() { //شيفرة الدالّة } }; class base2 { void function () { // شيفرة الدالّة } }; class derived: public base1, public base2 { }; int main() { derived obj; // خطأ، لأنّ المصرف لا يستطيع أن يحدد التابع الذي يجب استدعاؤه obj.function() } ولكن يمكن حلّ هذه المشكلة باستخدام دالّة حلّ النطاق (scope resolution function) لتحديد الصنف الذي سيُستدعى تابعه: int main() { obj.base1:: function (); // base1 تُستدعى دالة الصنف obj.base2:: function (); // base2 تُستدعى دالة الصنف } الدوال التابعة غير الساكنة (Non-static member functions) يمكن أن يكون للصنف دوال تابعة غير ساكنة تعمل على نُسخ الصنف المستقلة. class CL { public: void member_function() {} }; تُستدعى هذه التوابع على نُسَخ الصنف، على النحو التالي: CL instance; instance.member_function(); ويمكن تعريفها إمّا داخل أو خارج تعريف الصنف، فإذا عُرِّفت في الخارج ستُعدُّ داخل نطاق الصنف. struct ST { void defined_inside() {} void defined_outside(); }; void ST::defined_outside() {} ويمكن أن تُؤهّل (CV-qualified) أو تُؤهّل مرجعيًا (ref-qualified)، وينعكس ذلك على كيفية رؤيتها للنُّسخة التي استُدعيت عليها، ويعتمد الإصدار الذي سيُستدعى على المؤهّلات الثباتية للنُّسخة، فإذا لم يكن هناك إصدار له نفس المؤهِّلات الثباتيّة للنُّسخة، فسيُستدعى إصدار أكثر تأهيلًا ثباتيًا (more-cv-qualified) إذا كان متاحًا. struct CVQualifiers { void func() {} // 1: غير مؤهلة ثباتيا void func() const {} // 2:غير ثابتة void cv_only() const volatile {} }; CVQualifiers non_cv_instance; const CVQualifiers c_instance; non_cv_instance.func(); // #1 استدعاء c_instance.func(); // #2 استدعاء non_cv_instance.cv_only(); // const volatile استدعاء الإصدار c_instance.cv_only(); // const volatile استدعاء الإصدار الإصدار ≥ C++ 11 تحدّد الدوال التابعة المؤهّلة مرجِعيًّا (Member function ref-qualifiers) ما إذا كان يجوز أن يُستدعَى التابع على نُسخ يمينِيَّة (rvalue) أم لا، وتستخدم نفسَ صياغةِ التوابع المؤهّلة ثباتيًّا. struct RefQualifiers { void func() & {} // 1: تُستدعى على النسخ العادية void func() && {} // 2: تُستدعي على النسخ اليمينية }; RefQualifiers rf; rf.func(); // #1 استدعاء RefQualifiers {}.func(); // #2 استدعاء يجوز دمج المؤهّلات الثباتية والمؤهّلات المرجعية إذا لزم الأمر. struct BothCVAndRef { void func() const & {} // تُستدعى على النسخ العادية void func() && {} // تُستدعى على النسخ المؤقتة }; يمكن أيضًا أن تكون وهميّة، وهذا ضروري في التعددية الشكلية، ويسمح للأصناف المشتقة بأن يكون لها نفس واجهة الصنف الأب مع إضافة وظائفها الخاصّة. struct Base { virtual void func() {} }; struct Derived { virtual void func() {} }; Base * bp = new Base; Base * dp = new Derived; bp.func(); // Base::func() استدعاء dp.func(); // Derived::func() استدعاء هذا الدرس جزء من سلسلة دروس عن C++. ترجمة -وبتصرّف- للفصل Chapter 34: Classes/Structures من الكتاب C++ Notes for Professionals
-
يُقدر أنّ عدد لغات البرمجة الإجمالي يتجاوز 9000 لغة برمجة، منها حوالي 50 لغة تُستخدم على نطاق واسع من قبل المبرمجين [1]. هذا العدد الهائل قد يربك المبتدئ الذي يريد دخول عالم البرمجة، بل وحتى المبرمجين الذين يرغبون في تعلم لغات برمجة أخرى. 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; } إنّ أسئلة من قبيل: أيّ لغة برمجة ينبغي أن أتعلم؟ أو ما هي أفضل لغة برمجة؟ أو هل اللغة الفلانية خير من اللغة الفلانية؟ هي من الأسئلة الجدلية. يكفي أن تبحث على جوجل على مثل هذه الأسئلة وستجد ما يشبه حربا ضروسا على شبكة الإنترنت. فهذا يقول إنّ لغة البرمجة الفلانية هي أفضل اللغات، والآخر يقول بل اللغة الفلانية الأخرى خير. هناك سبب وجيه لهذا الاختلاف، ذلك أنّه لا توجد لغة برمجة تناسب الجميع، أو تتفوق على غيرها في كل المجالات. لكل لغة برمجة نقاط قوة ونقاط ضعف. سنحاول في هذه المقالة أن نساعد المبتدئ، وحتى من له خبرة سابقة في البرمجة ويريد تعلم لغات برمجة إضافية، على اختيار لغة البرمجة المناسبة ليتعلمها ويبدأ بها. سنركز في هذه المقالة على ثلاث من أشهر لغات البرمجة وأكثرها شعبية، وهي بايثون وروبي و PHP. سنوازن بين هذه اللغات ونستعرض أهم مميزات كل منها من الجوانب التالية: استخدامات اللغة وتطبيقاتها سهولة التعلم الشعبية الدعم والاستقرَار الأمان المكتبة وإطارات العمل الطلب في سوق العمل محاسن ومساوئ كل لغة باختصار سنختم هذه المقالة بخلاصة لنجيب فيها عن سؤال أيّ هذه لغات البرمجة أنسب لكي تتعلمها وتبدأ بها. استخدامات اللغة وتطبيقاتها بايثون بايثون هي لغة برمجة متعددة الأغراض، أي أنّه يمكن استخدامها لتطوير كافة أنواع التطبيقات، من تطبيقات الويب، وحتى الألعاب وتطبيقات سطح المكتب. تطوير الويب: يمكن استخدام بايثون لتطوير المواقع وتطبيقات الويب، إذ توفّر عددا من إطارات العمل المتقدمة، مثل Django و Flask. لكن شعبيتها لدى مطوري الويب أقل عموما من روبي و PHP. تطوير تطبيقات سطح المكتب: بايثون مثالية لتطبيقات سطح المكتب، إذ أنّها لغة مستقلة عن المنصات وأنظمة التشغيل، فبرامج بايثون يمكن أن تعمل دون جهد إضافي على ويندوز وأنظمة يونيكس. الذكاء الاصطناعي: نحن نعيش ثورة جديدة ستغير كل شيء من حولنا، وهي ثورة الذكاء الاصطناعي وتعلم الآلة التي أصبحت تطبيقاتها في كل مكان، في السيارات ذاتية السياقة وأجهزة التلفاز الذكية وروبوتات الدردشة والتعرف على الوجوه وغيرها من التطبيقات. تُعد بايثون أنسب لغات البرمجة للذكاء الاصطناعي كما يراها 57% من المطورين [2]. إذ توفر للباحثين في هذا المجال مكتبات متقدمة في مختلف مجالات البحث العلمي، مثل Pybrain لتعلم الآلة، وكذلك TensorFlow، وهي مكتبة يمكن استخدامها في الشبكات العصبونية والتعرف على الصور ومعالجة اللغات الطبيعية وغيرها . البحث العلمي: توفر بايثون للباحثين مكتبات عملية متقدمة تساعدهم على أبحاثهم، مثل مكتبة Numpy الخاصة بالحوسبة العلمية، و Scipy الخاصة بالتحليل العددي. بايثون تُستخدم بكثافة في القطاعات الأكاديمية والتجارية، هذه بعض المشاريع والشركات التي طُوِّرت باستخدامها: جوجل: لدى جوجل قاعدة عامة تقول: "استخدم بايثون ما أمكن، واستخدم C++ عند الضرورة". انستغرام: إحدى أكبر الشبكات الاجتماعية Netflix: أكبر منصة لبث الأفلام والمسلسلات على الشبكة Uber: أكبر تطبيق للتوصيل روبي تشبه روبي بايثون في العديد من الجوانب، من ذلك أنّها لغة برمجة متعددة الأغراض، إذ يمكن استخدامها في كافة أنواع التطبيقات. روبي هي لغة كائنية خالصة، أي أنّ كل شيء في روبي هو كائن له توابعه وخاصياته. يجعلها هذا مثالية للبرامج التي تعتمد بكثافة على نمط البرمجة الكائنية. تطبيقات الويب: روبي مثالية لتطوير تطبيقات الويب، إذ توفر إحدى أشهر وأفضل منصات تطوير الويب، وهي Ruby on Rails. والدليل على ذلك أنّ بعض أكبر المنصات والمواقع تستخدم Ruby on Rails، مثل منصة التجارة الإلكترونية Shopify. المشاريع الكبيرة: تُستخدم روبي في المشاريع الكبيرة والمعقدة والتي تستغرق مدة طويلة، وتتطلب تغييرات مستمرة. لغة نمذجة: تُستخدم روبي في تطوير نماذج أولية للبرامج ( prototypes) قبل البدء بتطويرها الفعلي. لغة سكريبتات: تُستخدم روبي (وبايثون كذلك) لبرمجة السكربتات، وهي ملفات تحتوي مجموعة من الأوامر التي يمكن تنفيذها دون الحاجة إلى تصريفها. من أكبر المشاريع والمواقع التي طُوِّرت باستخدام روبي نذكر على سبيل المثال لا الحصر: Sass: أحد أفضل امتدادات لغة CSS. Hulu: منصة لبث الأفلام والمسلسلات والوثائقيات Github : أكبر منصة لاستضافة المشاريع البرمجية PHP على خلاف روبي وبايثون، PHP ليست متعددة الأغراض، وإنما هي لغة متخصصة في برمجة الخوادم. الاستخدام الأساسي للغة PHP هو تطوير الواجهات الخلفية للمواقع وتطبيقات الويب، سواء الساكنة أو الديناميكية. تطبيقات سطح المكتب: صحيح أنّ PHP لغة متخصصة في برمجة الخوادم، إلا أنّه يمكن استخدامها لتطوير تطبيقات سطح المكتب باستخدام مكتبة PHP-GTK. لغة PHP لغة قوية، وقد طُوِّرت بها بعض أكبر المواقع على شبكة الإنترنت، مثل: فيسبوك: أكبر شبكة اجتماعية ياهو: محرك بحث ويكيبيديا: تستخدم هذه الموسوعة الضخمة PHP ضمن مجموعة من اللغات الأخرى ووردبريس: أكبر منصة لإدَارة المحتوى سهولة التعلم إحدى أهم عوامل المفاضلة بين لغات البرمجة هي سهولة تعلمها، خصوصا لدى المبتدئين. تعد بايثون على العموم أبسط وأسهل للتعلم موازنة بلغة روبي أو PHP. بايثون لغة مختصرة وبعيدة عن الإسهاب، في الحقيقة يُقدّر أنّ بايثون تختصر في المتوسط حوالي 80% من الشفرات المكتوبة موازنة بلغات البرمجة الكائنية الأخرى [3]. أضف إلى ذلك أن كتابة شيفرة برمجية بلغة بايثون أشبه بكتابة قصيدة أو قصة باللغة الإنجليزية الأمر الذي لا يجعل كتابة شيفرات بايثون عملية سهلة وممتعة، بل حتى قراءتها أيضًا. تعلم PHP أصعب عمومًا من تعلم بايثون، ذلك أنّ بايثون لغة متعددة الأغراض، أما PHP فهي لغة متخصصة تتطلب معرفة أولية بلغات أخرى، مثل HTML و CSS و Javascript. لكن إن كنت تريد تعلم PHP، فأتوقع أنك تريد أن تتعلم تطوير المواقع، ما يعني أنّك غالبا تعرف أساسيات هذه اللغات. فيما يخص روبي، فهي أصعب قليلا، وقد تحتاج إلى معرفة أولية بأساسيات البرمجة قبل تعلمها. الشعبية تحل بايثون في المرتبة الرابعة كأكثر لغات البرمجة شعبية أثناء تحديث هذا المقال، كما تتربع على عرش لغات البرمجة متعددة الأغراض، إذ يستخدمها حوالي 44% من المبرمجين. ثمّ تأتي لغة PHP في المرتبة الثامنة في قائمة أكثر لغات البرمجة شعبية، إذ يستخدمها حوالي 26% من المطورين، أما روبي فتأتي في المرتبة الرابع عشرة بنسبة استخدام تقارب 7%. لا تتمتع بايثون بالشعبية وحسب، ولكنها محبوبة أيضا لدى مجتمع المبرمجين، ففي الاستطلاع نفسه لعام 2020، جاءت بايثون في المرتبة الثالثة كأحب لغات البرمجة إلى المبرمجين، إذ أنّ أكثر من ثلثي المبرمجين المُستطلَعين قالوا أنّهم يحبونها. بالمقابل أتت كل من روبي و PHP في المرتبتين 19 و 20 على التوالي في هذه القائمة، حيث أنّ 43% من المبرمجين قالوا أنّهم يحبون روبي، و37% منهم قالوا أنهم يحبون PHP. هناك فرق واضح بين بايثون وبين PHP وروبي من حيث الشعبية وحب المبرمجين. بايثون بلا شك تتفوق في هذا الجانب تفوقا واضحا. لكن تجدر الإشارة إلى أنّ لغة PHP متخصصة، فهي تكاد تُستخدم حصرا في برمجة الخوادم، على خلاف بايثون وروبي متعدّدتي الأغراض، واللتان تُستخدمان في كل المجالات تقريبا. لذا رغم أنّ شعبية PHP أقل من بايثون، إلا أنّ هذا لا يقلل من قيمتها، ولا يعني أنّها غير مفيدة أو أنّه ليس لها مستقبل. بل على العكس، فهذا دليل على قوتها، لتتأكد من هذا حاول مقارنة شعبية PHP بلغة البرمجة ASP المتخصصة في المجال نفسه (برمجة الخوادم). لغة ASP ليست موجودة حتى في قائمة أكثر 25 لغة البرمجة استخدامًا. وهذا يعطيك فكرة عن قوة PHP وشعبيتها رغم أنّها لغة متخصصة في مجال واحد فقط. من جهة أخرى، لغتا PHP وروبي ليست محبوبتين للمبرمجين، إذ احتلّتا مرتبتين متأخرتين في قائمة أكثر اللغات المحبوبة. الدعم والاستقرار لغات بايثون وروبي PHP لغات مفتوحة المصدر، وتتمتع بمجتمع كبير من المبرمجين، وتُستخدم على نطاق واسع في المشاريع البحثية والتجارية. ظهرت هذه اللغات الثلاث في أوقات متقاربة: PHP: ظهرت سنة 1995، وهي في الإصدار 7.3 حاليا. بايثون: ظهرت سنة 1991، وهي في الإصدار 3.8 حاليا روبي: ظهرت سنة 1995، وهي في الإصدار 2.7 حاليا ما فتئت هذه اللغات تتطور منذ تأسيسها، خصوصا بايثون و PHP اللتان تُحدَّثان بوتيرة سريعة. كما تتمتع هذه اللغات بمجتمعات كبيرة وحيوية تدعمها، سواء عبر المكتبات أو المقالات أو الدروس والشروح. هناك مسألة يجدر الانتباه لها، وهي أّنه يوجد من بايثون إصداران: الإصدار 2.x والإصدار 3.x. وهما إصداران غير متوافقين، فالبرامج المكتوبة ببايثون 2.x، لن تعمل على بايثون 3.x، والعكس صحيح. هذا الأمر يمكن أن يكون مزعجا، خصوصا للمبتدئين. ولكن لا ينبغي أن تقلق من هذا، إذ أنّ دعم بايثون 2.x توقف سنة 2020، وسيبقى الإصدار بايثون 3.x وحسب. هناك ملاحظة أخرى مهمة، وهي أنّ لغة PHP انتقلت من الإصدار 5 إلى الإصدار 7 مباشرة، إذ أنّه ليس هناك إصدار سادس من هذه اللغة. السبب في ذلك هو أنّه كانت هناك خلافات كثيرة عليها، لذلك انتقل المطورون إلى الإصدار السابع مباشرة، والذي جاء بتعديلات كثيرة وجذرية على اللغة. يُفضل على العموم العمل بهذا الإصدار، لأنه الأحدث، كما أنّ بعض أنظمة إدارة المحتوى، مثل ووردبريس، تتطلب استخدام الإصدار السابع. هذه اللغات الثلاث على العموم مستقرة وتتمتع بدعم كبير وتُحدَّث باستمرار. وستبقى كذلك على الأرجح لمدة طويلة. الأمن لقد أصبح موضوع الأمن الرقمي والخصوصية من المواضيع المهمّة في الوقت الحالي. فكل يوم نسمع عن حالات اختراق وسرقة للبيانات الحساسة، حتى لدى الشركات الكبيرة مثل فيسبوك وجوجل. لهذا السبب ينبغي أن يحرص المبرمج على تأمين تطبيقاته وبرامجه وحماية خصوصيات المستخدمين وبياناتهم الحساسة. لا توجد عمومًا لغة برمجة آمنة تماما، فالأمر لا يعود إلى اللغة أو المنصة المُستخدمة، ولكن يعود إلى مدى احترام المبرمج لمعايير الأمن وكتابة شيفرات نظيفة وخالية من الثغرات الأمنية. قد تجد البعض يقول أنّ PHP أقل أمانا من بايثون وروبي، أو أنها لغة غير آمنة، وهذا أمر مردود. فلو كانت PHP غير آمنة، أنظنّ أنّ أكبر شبكة اجتماعية في العالم، وهي فيسبوك التي تخزن أكبر قاعدة بيانات للبيانات الشخصية للمستخدمين ستستخدم PHP؟ هذا غير ممكن. PHP مثلها مثل بايثون أو روبي، هي لغة مستقرة ويسهر عليها آلاف المطورين الذين يحدثونها باستمرار ويحرصون على سد أيّ ثغرة تظهر فيها. ربما كان السبب الذي يجعل البعض يقول هذا هو أنّ صياغة بايثون البسيطة تقلل من احتمال وجود ثغرات في الشفرة، وذلك على خلاف PHP التي تُعد أعقد من بايثون. قد يكون هذا الأمر صحيحا نسبيا، لكنّ الأمر يعود في النهاية إلى المبرمج، إن كان المبرمج يرتكب أخطاء ولا يحترم معايير الأمن، فلن تكون برامجه آمنة مهما كانت اللغة التي يكتب بها. الأداء والسرعة سرعة التنفيذ هي إحدى العوامل الأساسية لاختيار لغات البرمجة، خصوصا في المجالات التي تحتاج إلى إجراء حسابات مكثّفة، مثل الرسوميات وتطوير الألعاب. هناك نوعان من لغات البرمجة: لغات البرمجة المُفسّرة (interpreted): هي لغات برمجة يتم تنفيذ الشفرات المكتوبة بها مباشرة. لغات البرمجة الُمصرّفة (compiled): هي لغات برمجة تُصرّف (تُترجم) شفراتها إلى لغة المُجمّع أو أيّ لغة وسيطة قبل تنفيذها. على العموم، لغات البرمجة المصرّفة أسرع من لغات البرمجة المفسّرة. تُعد كل من بايثون وروبي لغتين مفسرتين، أما PHP فرغم أنّها مفسرة على العموم، إلا أنّ أنّ البرنامج الذي يسمح لك بتفسير تعليمات PHP مُصرَّف إلى رُقامة (bytecode) وسيطة. لهذا السبب فإنّ PHP عموما أسرع من بايثون، كما أنّ بايثون عموما أسرع من روبي. المكتبات وإطارات العمل تُقاس قوة كل لغة برمجة بالمكتبات التي توفرها. المكتبات هي حُزم من الشفرات الجاهزة والمنظمة التي تقدم دوالا وأصنافًا جاهزة لحل مشاكل معينة، أو إنشاء تطبيقات في مجال معين. أما إطارات العمل فهي منصات للبرمجة والتطوير، وعادة ما توفر أدوات تساعد على إنشاء المشاريع وإدارتها، وتنفيذ الشفرات وتنقيح الأخطاء وغيرها من المهام اليومية التي تسهل عمل المبرمجين. سوف نستعرض في هذه الفقرة بعض المكتبات وإطارات العمل الشهيرة للغات بايثون وروبي و PHP. بايثون Django: هو إطار عمل مجاني ومفتوح المصدر لتطوير المواقع. يوفر Django العديد من المزايا، مثل إدارة قواعد البيانات والمصادقة (authentication) وإدارة المستخدمين وغيرها. pycharm: هو إطار عمل لكتابة البرامج بلغة بايثون، يتولى pycharm التفاصيل الروتينية، ويتيح لك أن تركز على المهام الكبيرة والمعقدة. pycharm هو بيئة تطوير متكاملة، ويوفر العديد من المزايا، مثل الإكمال التلقائي للشفرات وفحص الأخطاء وإدارة المشاريع وغيرها. TensorFlow: هي مكتبة مجانية ومفتوحة المصدر للذكاء الاصطناعي من تطوير شركة جوجل. تُستخدم TensorFlow لكتابة وتقديم خوارزميات الذكاء الاصطناعي والتعلم الآلي والعصبونات. تُستخدم TensorFlow في العديد من مشاريع الذكاء الاصطناعي، مثل البحث الصوتي في جوجل. PyGame: مكتبة لتطوير ألعاب الفيديو، وتوفر العديد من المكتبات لمعالجة الصوت والصورة وكل الجوانب الضرورية لتطوير الألعاب. روبي Ruby on Rails: هو إطار عمل لتطوير تطبيقات الويب، ويوفر كل المزايا والوظائف التي تحتاجها لتطوير تطبيقات ومواقع ويب متقدمة. هذا الإطار مفتوح المصدر ومجاني. Bundler: هي بيئة متكاملة لإدارة مشاريع روبي تمكن من تثبيت المكتبات ومعالجة الإصدارات بسهولة. Better_errors: مكتبة لاختبار الشفرات المكتوبة بلغة روبي وتنقيح الأخطاء. PHP Laravel: أحد أشهر إطارات العمل الخاصة بلغة PHP. يُسرّع Laravel وتيرة العمل على المشاريع الكبيرة، إذ يوفر الكثير من المزايا الجاهزة، مثل المصادقة على المستخدمين وإدارة الجلسات والتخزين المؤقت وغيرها من المهام الأساسية لتطوير تطبيقات الويب. ووردبريس: ووردبريس هو أشهر نظام لإدارة المحتوى، ويُشغِّل ملايين المواقع على الشبكة. هذه المنصة مبنية على PHP. Ratchet: تمكّن هذه المكتبة من إنشاء تطبيقات ثنائية الاتجاه بين الخادم والعميل. تتوفر بايثون وروبي و PHP على المئات إن لم أقل الآلاف من المكتبات وإطارات العمل، وكل سنة تظهر مكتبات وإطارات عمل جديدة تستبدل القديمة أو تنافسها. مهما كانت اللغة التي اخترتها، ومهما كان المجال الذي تعمل فيه، فستجد حتمًا مكتبات جاهزة لمساعدتك على كتابة برامجك. الطلب في سوق العمل الطلب في سوق العمل هو أحد المؤشرات الأساسية للموازنة بين لغات البرمجة، خصوصا لمن كان يبحث عن وظيفة. بحسب استطلاع stackoverflow، فإنّ مطوري روبي يحصلون على أعلى أجر موازنة بمطوري بايثون و PHP. إذ يحصل مطور روبي في المتوسط على 71 ألف دولار سنويا، أما مطور بايثون فيحصل على 59 ألف دولار سنويا، بالمقابل لا يحصل مطور PHP إلا على 39 ألف دولار سنويا. من الواضح أنّ روبي هي الأفضل من حيث الأجور وفرص العمل، وقد يعود ذلك إلى قلة من يتقنون روبي، فقد رأينا من قبل أنّ شعبيتها بين المبرمجين قليلة موازنة ببايثون أو حتى PHP. هذه الأرقام تُحسب على صعيد عالمي، لكن قد يختلف الواقع من دولة إلى أخرى، مثلا في السعودية يحصل مطور PHP سنويا على حوالي 16 ألف دولار [4]، فيما يحص مطور بايثون على حوالي 18 ألف دولار سنويا [5]. أجور مطوّري PHP على العموم أقل من أجور مطوري بايثون وروبي، لكنّ الرواتب لا تُحدد بلغة البرمجة وحسب، إذ يمكن أن يحصل مطوّر PHP محترف وذو خبرة على أكثر مما يحصل عليه مطورو بايثون أو روبي، فالعبرة هنا بالاحترافِية وإتقان العمل. محاسن ومساوئ كل لغة بايثون محاسن مساوئ سهلة التعلم ومناسبة للمبتدئين هناك إصداران غير متوافقان منها صياغة بايثون بسيطة وقريبة من اللغة الطبيعية التعامل مع الأخطاء ليس مثاليا مختصرة وموجزة غير مناسبة لتطبيقات الجوال تتمتع بشعبية كبيرة لدى المبرمجين ليست مثالية للبرامج التي تعتمد على الاستخدام المكثف للذاكرة مكتبة ضخمة تساعد على تطوير كافة أنواع التطبيقات ليست مناسبة للبرامج المتوازية التي تعمل على المعالجات المتعددة روبي محاسن مساوئ مناسبة للبرامج الكبيرة صعبة على المبتدئين تمكن من تطوير التطبيقات بسرعة مصادر تعلم روبي على العموم أقل من بايثون و PHP مجتمع نشيط وحيوي ومكتبة كبيرة بطيئة موازنة باللغات الأخرى تتوفر على إحدى أفضل منصات تطوير تطبيقات الويب: ruby on rails التطوير والتحديث بطيئ PHP محاسن مساوئ سهلة التعلم صياغتها ليست ببساطة بايثون تدعم جميع خوادم الويب الرئيسية مثل: أباتشي ومايكروسوفت و Netscape أسماء الدوال مربكة وغير متناسقة لها شعبية كبيرة جدا لدى مطوري الويب بطيئة موازنة باللغات الأخرى مدعومة من أكبر نظام لإدارة المحتوى، وهو ووردبريس لا تدعم التطبيقات المتوازية خلاصة القول لقد استعرضنا مميزات لغات بايثون وروبي و PHP، ووازنّا بينها من عدة جوانب، وذكرنا بعض مساوئ ومحاسن كل منها. خلاصة القول أنّه لا توجد لغة مثالية تصلح للجميع. لكن إن كنت مبتدئا ولم تكن لك خبرة سابقة بالبرمجة، فإني أنصحك بأن تبدأ بلغة بايثون، فبساطتها وسهولتها ستساعدك على هضم المفاهيم البرمجية بسرعة وبعدها يمكنك أن تنتقل إلى تعلم اللغة التي تريدها بخطى ثابتة وأنت متمكن من المفاهيم البرمجية الأساسية التي تشترك بها كل لغات البرمجة. أما إن كانت لك خبرة سابقة في البرمجة وأردت أن تطور مستواك وتعمل على مشاريع كبيرة، فيمكن أن تتعلم روبي. وإن كنت تريد أن تتخصص في تطوير تطبيقات الويب أو تريد العمل بووردبريس، فالأولَى أن تتعلم PHP. اقرأ أيضًا تعرف على أبرز مميزات لغة بايثون علم البيانات Data science: الدليل الشامل
-
الملكية الفريدة الإصدار ≥ C++ 11 std::unique_ptr هو قالب صنف (class template) يُدير دورة حياة الكائنات المخزّنة ديناميكيًا، وعلى خلاف std::shared_ptr، فإنّ كل كائن ديناميكي يكون مملوكًا لمؤشّر حصري (std::unique_ptr) واحد في أيّ لحظة. // أنشئ عددًا صحيحًا ديناميكيًا تساوي قيمته 20 ويكون مملوكًا من مؤشّر وحيد std::unique_ptr<int> ptr = std::make_unique<int>(20); ملاحظة: std::unique_ptr متاحٌ منذ الإصدار C++ 11، أما std::make_unique فمنذ الإصدار C++ 14. المتغيّر ptr يحتوي على مؤشّر إلى عدد صحيح (int) مخزّن ديناميكيًّا، ويُحذف الكائن المملوك عندما يخرج مؤشّر حصريّ (unique pointer) يمتلك كائنًا عن النطاق (scope)، أي أنّ المُدمِّر (destructor) الخاصّ به سيُستدعى إذا كان الكائن من نوع صنف (class type)، كما ستُحرّر ذاكرة ذلك الكائن. ولاستخدام std::unique_ptr و std::make_unique مع المصفوفات، استخدم الصياغة التالية: // إنشاء مؤشّر حصري يشير إلى عدد صحيح يساوي 59 std::unique_ptr<int> ptr = std::make_unique<int>(59); // إنشاء مؤشّر حصري يشير إلى مصفوفة من 15 عددًا صحيحا std::unique_ptr<int[]> ptr = std::make_unique<int[]>(15); يمكنك الوصول إلى مؤشّر حصريّ (std::unique_ptr) كما تصل إلى أيّ مؤشّر خام، لأنه يزيد تحميل (overloads) تلك العوامل، كما يمكنك نقل ملكية محتويات مؤشّر ذكي إلى مؤشّر آخر باستخدام std::move التي ستجعل المؤشّر الذكي الأصلي يشير إلى nullptr. // 1. std::unique_ptr std::unique_ptr <int> ptr = std::make_unique <int> (); // 1 تغيير القيمة إلى *ptr = 1; // 2. std::unique_ptr // سيفقد ملكية الكائن 'ptr' فإن 'ptr2' إلى 'ptr' بنقل std::unique_ptr <int> ptr2 = std::move(ptr); int a = *ptr2; // 'a' is 1 int b = *ptr; // 'nullptr' يساوي ptr // بسبب أمر النقل أعلاه تمرير unique_ptr إلى دالّة كمعامل: void foo(std::unique_ptr <int> ptr) { // ضع شيفرتك هنا } std::unique_ptr <int> ptr = std::make_unique <int> (59); foo(std::move(ptr)) إعادة مؤشّر unique_ptr من دالّة هي الطريقة المفضلة في C++ 11 لكتابة الدوال المُنتجة (factory functions)، إذ أنّها تعبّر بوضوح عن ملكية القيمَة المعادة، فالمُستدعي يمتلك المؤشّر unique_ptr الناتج وهو المسؤول عنه. std::unique_ptr <int> foo() { std::unique_ptr <int> ptr = std::make_unique <int> (59); return ptr; } std::unique_ptr <int> ptr = foo(); قارن هذا بالشيفرة التالية: int* foo_cpp03(); int* p = foo_cpp03(); // أم يجب علي حذفه في مرحلة ما p هل أملك // جواب هذا السؤال غير واضح الإصدار <C++ 14 قُدِّم قالب الصنف make_unique منذ الإصدار C++ 14. لكن من السهل إضافته يدويًا إلى C++ 11: template < typename T, typename...Args > typename std::enable_if < !std::is_array <T> ::value, std::unique_ptr <T>> ::type make_unique(Args && ...args) { return std::unique_ptr <T> (new T(std::forward < Args > (args)...)); } // لأجل المصفوفات make_unique استخدام template < typename T > typename std::enable_if < std::is_array <T> ::value, std::unique_ptr <T>> ::type make_unique(size_t n) { return std::unique_ptr <T> (new typename std::remove_extent <T> ::type[n]()); } الإصدار ≥ C++ 11 على عكس المؤشّر "الذكي" std::auto_ptr الغبي، فإن المؤشّر unique_ptr يستطيع أن يُستنسخ (instantiated) عبر تخصيص المتجه -وليس std::vector- إذ كانت الأمثلة السابقة للتخصيصات العددية، أما إن أردنا الحصول على مصفوفة من الأعداد الصحيحة المخزّنة ديناميكيًا، فإنك تحدد int[] كنوع للقالب -وليس فقط int-، انظر: std::unique_ptr<int[]> arr_ptr = std::make_unique<int[]>(10); يمكن تبسيط الشّيفرة باستخدام: auto arr_ptr = std::make_unique<int[]>(10); الآن، يمكنك استخدام arr_ptr كما لو كان مصفوفة: arr_ptr[2] = 10; ولا داعي للقلق بشأن إلغاء تخصيص الذاكرة (de-allocation)، فهذا الإصدار المُتخصّص في القوالب يستدعي المُنشِئاتِ (constructors) والمدمِّرات (destructors) بالشكل المناسب، وعليه فإن استخدام الإصدار المتجهي من unique_ptr أو المتجه vector نفسه تعود للتفضيل الشخصي. كان std::auto_ptr متاحًا في الإصدارات السابقة لـ C++ 11. وعلى عكس unique_ptr فإن مؤشّرات auto_ptr مسموح بنسخها، وسيؤدّي هذا إلى أن يفقِد المصدر ptr ملكية المؤشِّر لتتحوّل الملكيّة إلى الهدف. الملكية المشتركة std::shared_ptr يُعرِّف قالب الصنف std::shared_ptr مؤشّرًا مُشتركًا قادرًا على مشاركة ملكيّة كائن ما مع مؤشّرات مشتركة أخرى، وهذا يتعارض مع طبيعة المؤشّرات الحصرية (std::unique_ptr) التي تمثّل ملكية حصرية. ويُنفَّذ سلوك المشاركة عبر تقنية تُعرف بعدِّ المراجع (reference counting)، حيث يُخزّن عدد المؤشّرات المشتركة التي تشير إلى الكائن مع هذا الكائن نفسه، ويُدمَّر هذا الكائن تلقائيًا عندما يصل هذا العدد إلى الصفر إما بسبب تدمير النسخة الأخيرة من std::shared_ptr أو إعادة تعيينها. انظر المثال التالي حيث يكون firstShared مؤشرًا مشتركًا لنسخة جديدة من Foo: std::shared_ptr<Foo> firstShared = std::make_shared<Foo>(/*args*/); لإنشاء عدّة مؤشّرات ذكية تشترك في نفس الكائن، نحتاج إلى إنشاء مؤشّر مشترك آخر يأخذ اسم المؤشّر الأول المشترك، ولدينا الآن طريقتان لفعل ذلك: std::shared_ptr<Foo> secondShared(firstShared); // الطريقة 1: الإنشاء بالنسخ std::shared_ptr<Foo> secondShared; secondShared = firstShared; // الطريقة 2: الإسناد كلا الطّريقتان المَذكورتان أعلاه تجعلان secondShared مؤشّرًا مشتركًا يتشارك ملكية نُسخة Foo مع firstShared. تعمل المؤشّرات الذكية مثل المؤشّرات الخام، هذا يعني أنه يمكنك استخدام * لتحصيلها، كما أنّ العامل العادي -> يعمل أيضًا: secondShared->test(); // Foo::test() يستدعي أخيرًا، عندما يخرج آخر مؤشّر مشترك مُكنّى (aliased) عن النطاق، فسيُستدعى المدمّر الخاصّ بنسخة Foo. تنبيه: قد يؤدي إنشاء مؤشّر مشترك إلى إطلاق اعتراض bad_alloc عند الحاجة إلى تخصيص بيانات إضافية في الملكيّة المشتركة، وفي حال تمرير مؤشّر عادي إلى المُنشئ فإنه سيَفترض أنه يمتلك الكائن الذي يشير إليه ذلك المؤشّر وسَيستدعي دالّة الحذف (deleter) في حالة إطلاق اعتراض. هذا يعني أنّ shared_ptr<T>(new T(args)) لن تُسرِّب كائن T في حال فشل تخصيص ذاكرة shared_ptr<T>. أيضًا يُنصح باستخدام make_shared<T>(args) أو allocate_shared<T>(alloc, args) إذ يحسّن كفاءة تخصيص الذاكرة. تخصيص ذاكرة المصفوفات باستخدام المؤشرات المشتركة C++ 11 =< الإصدار < C++ 17 لا توجد طريقة مباشرة لتخصيص ذاكرة المصفوفات باستخدام make_shared<>، لكن من الممكن إنشاء مصفوفات shared_ptr<> باستخدام new و std::default_delete. على سبيل المثال، لتخصِيص ذاكرة مصفوفة عُشارية مكوّنة من أعداد صحيحة، يمكننا كتابة الشيفرة التالية: shared_ptr<int> sh(new int[10], std::default_delete<int[]>()); يجب تحديد std::default_delete هنا للتأكّد من تنظيف الذاكرة المُخصّصة بشكل صحيح باستخدام delete[]. وإذا عرفنا حجم المصفوفة في وقت التصريف، يمكننا القيام بذلك بالطريقة التالية: template < class Arr > struct shared_array_maker {}; template < class T, std::size_t N > struct shared_array_maker < T[N] > { std::shared_ptr <T> operator() const { auto r = std::make_shared < std::array < T, N >> (); if (!r) return {}; return {r.data(), r}; } }; template < class Arr > auto make_shared_array() -> decltype(shared_array_maker < Arr > {}()) { return shared_array_maker < Arr > {}(); } سيعيد make_shared_array<int[10]> عندئذٍ المؤشّرَ المشترك shared_ptr<int> مشيرًا إلى عناصر المصفوفة الذين أنشئوا افتراضيًا. الإصدار ≥ C++ 17 تحسَّن دعم المؤشّرات المشتركة للمصفوفات في الإصدار C++ 17، فلم يعد ضروريًا تحديد دالّة حذف (array-deleter) للمصفوفة بشكل صريح، وصار من الممكن تحصيل المؤشّر المشترك باستخدام عامل فهرسة المصفوفات []: std::shared_ptr<int[]> sh(new int[10]); sh[0] = 42; تستطيع المؤشرات المشتركة أن تشير إلى كائن فرعي من الكائن الذي تمتلكه، انظر: struct Foo { int x; }; std::shared_ptr<Foo> p1 = std::make_shared<Foo>(); std::shared_ptr<int> p2(p1, &p1->x); يمتلك كل من p2 و p1 الكائن المنتمي إلى النوع Foo إلا أنّ p2 يشير إلى العضو العددي الصحيح x، وهذا يعني أنّه في حال خرج p1 عن النطاق أو أُعِيد تعيينه فسيظلّ الكائن Foo حيًّا، ممّا يضمن أنّ p2 لن يتراجع ((dangle. ملاحظة مهمة: لا تعرِف المؤشّرات المشتركة إلا نفسَها وبقيّة المؤشّرات المشتركة الأخرى التي أنشئت باستخدام المُنشئ المكنّى (alias constructor). ولا تعرف أيّ مؤشّرات أخرى حتى المؤشّرات المشتركة التي أنشئت بالإشارة إلى نفس نُسخة Foo. انظر ()shared1.reset في المثال التالي إذ سيحذف foo لأن shared1 هو المؤشر المشترك الوحيد الذي يمتلكه: Foo *foo = new Foo; std::shared_ptr < Foo > (foo); std::shared_ptr < Foo > shared2(foo); // لا تفعل هذا shared1.reset(); shared2 -> test(); // قد حُذفت shared2 الخاصة بـ foo سلوك غير محدد إذ أن. نقل ملكية المؤشرات المشتركة افتراضيًّا، تزيد المؤشّرات المشتركة shared_ptr عدد المراجع (reference count) لكنها لا تنقل الملكية، غير أننا نستطيع جعلها تنقل الملكية باستخدام std::move: shared_ptr<int> up = make_shared<int>(); // نقل الملكية shared_ptr<int> up2 = move(up); // 1 ذي العدّاد up2 يساوي 0، وملكية المؤشّر محصورة في up الآن، عدّاد المراجع الخاص بـ المشاركة بملكية مؤقتة يمكن أن تشير نُسخ المؤشّرات الضعيفة std::weak_ptr إلى الكائنات المملوكة لنُسخ المؤشّرات المشتركة std::shared_ptr وتصبح مالِكة مؤقتة، هذا يعني أنّ المؤشّرات الضعيفة لا تغير عدد مراجع الكائن، وعليه لا تمنع حذف الكائن إذا أُعيد إسناد كافّة مؤشّرات الكائن المشتركة أو حذفها. انظر المثال التالي إذ سنستخدم المؤشّرات الضعيفة للسماح بحذف كائن: #include <memory> #include <vector> struct TreeNode { std::weak_ptr < TreeNode > parent; std::vector < std::shared_ptr < TreeNode > > children; }; int main() { // TreeNode إنشاء std::shared_ptr < TreeNode > root(new TreeNode); // إعطاء الأب 100 عقدة فرعية for (size_t i = 0; i < 100; ++i) { std::shared_ptr < TreeNode > child(new TreeNode); root -> children.push_back(child); child -> parent = root; } // ومعه العُقَد الفرعية root إعادة تعيين المؤشّر المشترك، وتدمير الكائن root.reset(); } تُسنّد العقدة الجذر إلى parent بينما تُضاف عقد فرعية (child nodes) إلى فروع العقدة الجذر، كما يصرَّح العضو parent كمؤشّر ضعيف بدلاً من مؤشّر مشترك حتى لا يُزاد في عدد مراجع العقدة الجذر، وسيُحذَف الجذر عند إعادة تعيين العقدة الجذر في نهاية main(). أيضًا، نظرًا لأنّ مراجع المؤشّر المشترك المتبقيّة التي تشير إلى العقد الفرعية قد ضُمِّنت في مجموعة الجذر children، لذا ستُدمَّر جميع العقد الفرعية لاحقًا. قد لا تُحرّر الذاكرة المخصّصة التي تخصّ المؤشّر المشترك حتى يصل العدّادان المراجعيّان shared_ptr و weak_ptr إلى الصفر. #include <memory> int main() { { std::weak_ptr <int> wk; { // عبر تخصيص الذاكرة مرة واحدة std::make_shared تُحسَّن // تخصّص الذاكرة مرتين std::shared_ptr<int>(new int(42)) std::shared_ptr <int> sh = std::make_shared <int> (42); wk = sh; // ينبغي أن تكون قد حُرِّرت الآن sh ذاكرة } // ما تزال حية wk ّلكن } // (sh و wk) حُرِّرت الذاكرة الآن } نظرًا لأنّ المؤشّرات الضعيفة (std::weak_ptr) لا تُبقي الكائن الذي تشير إليه على قيد الحياة، فلا يمكن الوصول المباشر للبيانات عبر المؤشّرات الضعيفة، بيْد أنّها توفّر تابعًا lock()، والذي يُرجعَ مؤشّرًا مشتركًا std::shared_ptr إلى الكائن المشار إليه: #include <cassert> #include <memory> int main() { { std::weak_ptr <int> wk; std::shared_ptr <int> sp; { std::shared_ptr <int> sh = std::make_shared <int> (42); wk = sh; // wk سيؤدي إلى إنشاء مؤشّر مشترك يشير إلى الكائن الذي يشير إليه lock استدعاء sp = wk.lock(); // sp عند هذه اللحظة، على خلاف sh ستُحذف } // تُبقي البيانات حية sp // إن أردنا lock() ما يزال بإمكاننا استدعاء // wk من أجل لحصول على مؤشّر مشترك يشير إلى نفس البيانات من assert( * sp == 42); assert(!wk.expired()); // سيمحو البيانات sp إعادة إسناد // لأنه آخر مؤشّر مشترك له ملكية sp.reset(); // سيعيد مؤشّرا مشتركا فارغا wk على lock محاولة استدعاء // لأن البيانات قد حُذِفت سلفا sp = wk.lock(); assert(!sp); assert(wk.expired()); } } استخدام دوال حذف مخصصة لتغليف واجهة C تتوفّر العديد من واجهات C (مثل SDL2) على دوالّ الحذف (deletion functions) الخاصّة بها. هذا يعني أنّه لا يمكنك استخدام المؤشّرات الذكية مباشرة: std::unique_ptr<SDL_Surface> a; // لن يعمل، غير آمن بدلاً من ذلك، سيكون عليك أن تعرّف دالّة الحذف الخاصّة بك، تستخدم الأمثلة هنا بنية SDL_Surface التي يجب تحريرها باستخدام الدالّة SDL_FreeSurface()، بيْد أنّه يجب تكييفها مع العديد من واجهات C الأخرى. كذلك يجب أن تكون دالة الحذف قابلة للاستدعاء باستخدام مؤشّر وسيط (pointer argument) على النحو التالي: std::unique_ptr<SDL_Surface, void(*)(SDL_Surface*)> a(pointer, SDL_FreeSurface); سيعمل أيّ كائن آخر قابل للاستدعاء أيضًا، على سبيل المثال: struct SurfaceDeleter { void operator()(SDL_Surface* surf) { SDL_FreeSurface(surf); } }; std::unique_ptr < SDL_Surface, SurfaceDeleter > a(pointer, SurfaceDeleter {}); // آمن std::unique_ptr < SDL_Surface, SurfaceDeleter > b(pointer); // مكافئ للشيفرة أعلاه سيوفر لك هذا إدارة آمنة واقتصادية للذاكرة بدون استخدام unique_ptr وكذلك ستحصل على الأمان من ناحية الاعتراضات (Exceptions). لاحظ أنّ دالة الحذف جزء من النوع بالنسبة للمؤشّر الحصري (unique_ptr)، ويستطيع التنفيذ (implementation) استخدام تحسين الأساس الفارغ (Empty base optimization) لتجنّب أي تغيير في الحجم بالنسبة لدَوالّ الحذف المخصّصة الفارغة (Empty custom deleters). لذا، رغم أنّ: std::unique_ptr<SDL_Surface, SurfaceDeleter> std::unique_ptr<SDL_Surface, void(*)(SDL_Surface*)> يحلّان المشكلة بطريقة متماثلة، فإنّ حجم النوع الأوّل يساوي حجم مؤشّر واحد، بينما يجب أن يحتفظ النوع الأخير بمؤشّرين: أي المؤشّر SDL_Surface * ومؤشّر الدالة! أيضًا، يُفضّل تغليف دوالّ الحذف المخصّصة الحُرّةفي نوع فارغ، ويمكن استخدام مؤشّر مشترك (shared_ptr) بدلاً من مؤشّر حصري (unique_ptr) في الحالات التي يكون فيها عدُّ المراجع مهمًا. تخزِّن المؤشّرات المشتركة دالّة الحذف دائمًا، مما يؤدّي إلى محوِ نوع دالّة الحذف، هذا قد يكون مفيدًا في الواجهات البرمجية (APIs). يعيب استخدام المؤشّرات المشتركة هو أنّ حجم ذاكرة تخزين دالّة الحذف ستكون أكبر، وتكلفة صيانة عدّاد المراجع ستكون أكبر كذلك. // دالة الحذف مطلوبة في وقت الإنشاء وهي جزء من النوع std::unique_ptr<SDL_Surface, void(*)(SDL_Surface*)> a(pointer, SDL_FreeSurface); // دالة الحذف مطلوبة في وقت الإنشاء ولكنها ليست جزءًا من النوع std::shared_ptr<SDL_Surface> b(pointer, SDL_FreeSurface); الإصدار ≥ C++ 17 تسهّل template auto تغليف دوال الحذف المخصّصة: template <auto DeleteFn> struct FunctionDeleter { template <class T> void operator()(T* ptr) { DeleteFn(ptr); } }; template <class T, auto DeleteFn> using unique_ptr_deleter = std::unique_ptr<T, FunctionDeleter<DeleteFn>>; المثال أعلاه سُيصبح: unique_ptr_deleter<SDL_Surface, SDL_FreeSurface> c(pointer); الغرض من auto في المثال السابق هو التعامل مع جميع الدوالّ الحرّة (free functions)، سواء كانت تُعيد void (مثل SDL_FreeSurface) أم لا (مثل fclose). الملكية الحصرية بدون دلالة النقل الإصدار ++> ملاحظة: أُهمِلت std::auto_ptr في C++ 11 وستُزال تمامًا في C++ 17، لذا لا ينبغي أن تستخدمها إلّا إن كنت مضطرًا لاستخدام C++ 03 أو إصدار سابق وكنت على استعداد لتوخّي الحذر الشديد، أما فيما سوى ذلك فيوصى بالانتقال إلى unique_ptr و std::move. قبل المؤشّرات الحصريّة (std::unique_ptr) والدلالات النقليّة (move semantics)، كان لدينا std::auto_ptr، والذي يوفّر ملكيّة حصريّة، غير أنّه ينقل الملكية عند النسخ. وكما هو الحال مع جميع المؤشّرات الذكية، فإن المؤشّر std::auto_ptr ينظف الموارد تلقائيًا: { std::auto_ptr<int> p(new int(42)); std::cout << *p; } // هنا، لا يوجد تسرب في الذاكرة p تُحذف لكن يسمح بمالك واحد فقط: std::auto_ptr<X> px = ...; std::auto_ptr<X> py = px; // فارغ الآن px هذا يسمح باستخدام std::auto_ptr للإبقاء على المِلكِيّة صريحةً وحصريّة، لكن مع خطر خسارة الملكية بشكل غير مقصود: void f(std::auto_ptr < X > ) { // X افتراض ملكية // تحذفها في نهاية النطاق. }; std::auto_ptr < X > px = ...; f(px); // X ملكية f تتطلب // فارغ الآن px px -> foo(); // NPE! // لا يحذف px.~auto_ptr() حدث نقل الملكية في مُنشئ النَّسْخ (copy constructor)، ويأخذ مُنشئ النَّسخ وعامل تعيين النَّسخ الخاصّ بالمؤشّر auto_ptr معامِلاته بواسطة مرجع غير ثابت (non-const) حتى يمكن تعديلها. هذا مثال على ذلك: template < typename T > class auto_ptr { T * ptr; public: auto_ptr(auto_ptr & rhs): ptr(rhs.release()) {} auto_ptr & operator = (auto_ptr & rhs) { reset(rhs.release()); return *this; } T * release() { T * tmp = ptr; ptr = nullptr; return tmp; } void reset(T * tmp = nullptr) { if (ptr != tmp) { delete ptr; ptr = tmp; } } /* دوال أخرى */ }; هذا يكسر الدلالة النسخيّة (copy semantics) التي تتطلّب أن ينتُج عن عمليّة نسخ كائنٍ ما نسختان متكافئتان. إن كان T نوعًا قابلًا للنسخ، فيمكن كتابة: T a = ...; T b(a); assert(b == a); لكن ليس هذا هو الحال بالنسبة إلى auto_ptr، لهذا من غير الآمن وضع auto_ptr في الحاويات. تحويل المؤشرات المشتركة لا يمكن استخدام: static_cast const_cast dynamic_cast reinterpret_cast مباشرةً على المؤشّرات المشتركة std::shared_ptr للحصول على مؤشّر يتشارك المِلكِيَّة مع المؤشّر المُمرَّر كوسيط، وإنما يجب استخدام الدوالّ: std::static_pointer_cast std::const_pointer_cast std::dynamic_pointer_cast std::reinterpret_pointer_cast struct Base { virtual~Base() noexcept {}; }; struct Derived: Base {}; auto derivedPtr(std::make_shared < Derived > ()); auto basePtr(std::static_pointer_cast < Base > (derivedPtr)); auto constBasePtr(std::const_pointer_cast < Base const > (basePtr)); auto constDerivedPtr(std::dynamic_pointer_cast < Derived const > (constBasePtr)); لاحظ أنّ std::reinterpret_pointer_cast غير متاحة في C++ 11 و C++ 14، إذ لم تُقترح إلا في N3920، ولم تُعتمد في مكتبة الأساسيات (Library Fundamentals) TS إلّا في فبراير 2014. لكن يبقى من الممكن تنفيذها على النحو التالي: template < typename To, typename From > inline std::shared_ptr < To > reinterpret_pointer_cast( std::shared_ptr < From > const & ptr) noexcept { return std::shared_ptr < To > (ptr, reinterpret_cast < To* > (ptr.get())); } كتابة مؤشر ذكي: value_ptr value_ptr هو مؤشّر ذكيّ يتصرّف كقيمة، فعند النسخ ينسخ محتوياته، وعند الإنشاء ينشئ محتوياته أيضًا. انظر: // std::default_delete: مثل template <class T> struct default_copier { // null فارغا ويعيد القيمة T const* ينبغي أن يعالج الناسخ نوعا T *operator()(T const *tin) const { if (!tin) return nullptr; return new T(*tin); } void operator()(void *dest, T const *tin) const { if (!tin) return; return new (dest) T(*tin); } }; // لمعالجة القيمة الفارغة tag صنف: struct empty_ptr_t { }; constexpr empty_ptr_t empty_ptr{}; // مؤشر القيمة يطبع نفسه: template <class T, class Copier = default_copier<T>, class Deleter = std::default_delete<T>, class Base = std::unique_ptr<T, Deleter>> struct value_ptr : Base, private Copier { using copier_type = Copier; // unique_ptr من typedefs أيضًا using Base::Base; value_ptr(T const &t) : Base(std::make_unique<T>(t)), Copier() { } value_ptr(T &&t) : Base(std::make_unique<T>(std::move(t))), Copier() { } // لا يكون فارغا أبدا: value_ptr() : Base(std::make_unique<T>()), Copier() { } value_ptr(empty_ptr_t) {} value_ptr(Base b, Copier c = {}) : Base(std::move(b)), Copier(std::move(c)) { } Copier const &get_copier() const { return *this; } value_ptr clone() const { return { Base( get_copier()(this->get()), this->get_deleter()), get_copier()}; } value_ptr(value_ptr &&) = default; value_ptr &operator=(value_ptr &&) = default; value_ptr(value_ptr const &o) : value_ptr(o.clone()) {} value_ptr &operator=(value_ptr const &o) { if (o && *this) { // عيّن المحتوى إن كانا فارغيْن: **this = *o; } else { // وإلا فعيِّن قيمة منسوخة: *this = o.clone(); } return *this; } value_ptr &operator=(T const &t) { if (*this) { **this = t; } else { *this = value_ptr(t); } لا تكون قيمة المؤشّر الذكي (value_ptr) فارغة إلّا إذا أنشأته باستخدام empty_ptr_t، أو قمت بعملية نقل (move) منه، وهذ يكشف حقيقة أنّه حصريّ (unique_ptr)، لذلك سيعمل العامل explicit operator bool() const عليه. يعيد التابع .get() مرجعًا (إذ أنه لا يكاد يكون فارغًا أبدًا)، فيما يعيد التابع .get_pointer() مؤشّرًا. ويمكن أن يكون هذا المؤشّر الذكي مفيدًا في حال كنّا نريد دلالة قيميّة (value-semantics)، لكن لا نريد الكشف عن المحتويات خارج نطاق التنفيذ. كذلك يمكن باستخدام ناسخ Copier غير افتراضي أن نتعامل مع الأصناف الأساسية الوهمية (virtual base classes) التي تعرف كيفيّة إنتاج نُسخ من الصنف المشتق وتحويلها إلى أنواع قيميّة (value-types). جعل المؤشرات المشتركة تشير إلى this يتيح لك enable_shared_from_this الحصول على نُسخة صالحة من مؤشّر مشترك يشير إلى [this](رابط الفصل 32)، وسترث تابع shared_from_this عبر اشتقاق صنف من قالب الصنف enable_shared_from_this، ليعيد مؤشّرا مشتركًا يشير إلى this. لاحظ أنه لا بدّ من إنشاء الكائن كمؤشّر مشترك (shared_ptr): #include <memory> class A : public enable_shared_from_this<A> { }; A *ap1 = new A(); shared_ptr<A> ap2(ap1); // تحضير مؤشّر مشتركا يشير إلى الكائن الذي يحتويه // ثم الحصول على مؤشّر مشترك يشير إلى الكائن من داخل الكائن نفسه shared_ptr<A> ap3 = ap1->shared_from_this(); int c3 = ap3.use_count(); // =2: يشير إلى نفس الكائن. ملاحظة: لا يمكنك استدعاء enable_shared_from_this داخل المُنشئ. #include <memory> // enable_shared_from_this class Widget: public std::enable_shared_from_this < Widget > { public: void DoSomething() { std::shared_ptr < Widget > self = shared_from_this(); someEvent -> Register(self); } private: ... }; int main() { ... auto w = std::make_shared < Widget > (); w->DoSomething(); ... } إذا استخدمت shared_from_this() على كائن غير مملوك من مؤشّر مشترك، مثل كائن تلقائي محلي (local automatic object) أو كائن عام (global object)، فإنّ السلوك لن يكون محدّدًا. لكن المصرِّف أصبح يطلق الاعتراض std::bad_alloc منذ الإصدار C++ 17. يكافئ استخدام shared_from_this() من داخل مُنشئ، استخدامه على كائن غير مملوك من مؤشّر مشترك، لأنّ الكائنات ستكون مملوكة للمؤشّر المشترك بعد عودة المُنشئ. هذا الدرس جزء من سلسلة دروس عن C++. ترجمة -بتصرّف- للفصل Chapter 33: Smart Pointers من كتاب C++ Notes for Professionals
-
المؤشّر هو عنوان يشير إلى موقع في الذاكرة، وتُستخدم المؤشرات عادة للسماح للدوالّ أو هياكل البيانات بالحصول على معلومات عن الذاكرة وتعديلها دون الحاجة إلى نسخ الذاكرة المشار إليها، والمؤشّرات قابلة للاستخدام سواءً مع الأنواع الأوليّة (المٌضمّنة) أو الأنواع التي يعرّفها المستخدم. وتستفيد المؤشّرات من عامل التحصيل * و العنونة & و السهم ->، إذ يُستخدم عاملا * و -> للوصول إلى الذاكرة المشار إليها، فيما يُستخدم المعامل & للحصول على عنوانٍ في الذاكرة. عمليات المؤشرات يوجد مُعاملان يختصان بالمؤشّرات، وهما: معامل العنونة (Address-of operator) وهو & الذي يعيد عنوان عاملِهِ في الذاكرة، ومعامل التحصيل (Dereference) وهو * الذي يعيد قيمة المتغير الموجود في العنوان المحدّد بواسطة عامله. int var = 20; int *ptr; ptr = &var; cout << var << endl; // 20 (قيمة المتغير) cout << ptr << endl; // 0x234f119 (موقع المتغير في الذاكرة) cout << *ptr << endl; // 20(ptr قيمة المتغير المخزن في مؤشر) يُستخدم رمز النجمة * للتصريح عن مؤشّر لمجرد التوضيح بأنه مؤشر، ولا ينبغي أن تخلط بينه وبين عامل التحصيل (dereference operator) الذي يُستخدم للحصول على القيمة الموجودة في عنوان محدّد، إذ هما شيئان مختلفان مُمثّلان بنفس الرمز. أساسيات المؤشرات الإصدار ++> ملاحظة: في كل ما يلي، سنفترض وجود الثابت C++ 11) nullptr). بالنسبة للإصدارات السابقة، بدّل nullptr مكان NULL، وهو الثابت الذي كان يؤدي وظيفة مشابهة. إنشاء متغير المؤشر يمكن إنشاء متغيّرات المؤشّرات باستخدام صيغة *، على سبيل المثال: int *pointer_to_int;. تحتوي المتغيرات من نوع المؤشّرات (int * مثلا) على عنوان ذاكرة يكون موقعًا تُخزَّن فيه بيانات النوع الأساسي (كـ int * مرة أخرى). ويتضح الفرق عند مقارنة حجم المتغير مع حجم المؤشّر الذي يشير إلى نفس النوع. انظر المثال التالي الذي نصرح فيه عن بُنية من نوع big_struct تحتوي على ثلاثة أعداد long long int: typedef struct { long long int foo1; long long int foo2; long long int foo3; } big_struct; والآن ننشئ متغير bar من نوع big_struct، ثم ننشئ المتغير p_bar من نوع pointer to big_struce، ونهيئه إلى المؤشر الفارغ nullptr: big_struct bar; big_struct * p_bar0 = nullptr; // `bar` يطبع حجم std::cout << "sizeof(bar) = " << sizeof(bar) << std::endl; // `p_bar` يطبع حجم std::cout << "sizeof(p_bar0) = " << sizeof(p_bar0) << std::endl; /* الناتج sizeof(bar) = 24 sizeof(p_bar0) = 8 */ أخذ عنوان متغير آخر يمكن إسناد قيمة مؤشّر إلى مؤشر آخر مثل المتغيرات العادية بالضبط، إلا أنّه في حال المؤشّرات فإنّ عنوان الذاكرة هو الذي يُنسخ من مؤشّر إلى آخر وليس البيانات الحقيقية التي يشير إليها المؤشّر. كذلك قد تأخذ المؤشّرات القيمة nullptr التي تمثّل موقعًا فارغًا في الذاكرة، وتمثل المؤشّرات التي تساوي nullptr موقعًا غير صالح في الذاكرة، وعليه فهي لا تشير إلى أيّ بيانات فعليّة. نحصل على عنوان الذاكرة الخاص بمتغيّر من نوع ما عن طريق إِسباق المتغيّر بعامل العَنْوَنة &، وتكون القيمة المعادة من & هي مؤشّر إلى النوع الأساسي الذي يحتوي على عنوان ذاكرة المتغير، وهي بيانات صالحة طالما لم يخرج المتغير من النطاق. انظر المثال التالي حيث ننسخ p_bar0 إلى p_bar1، ثم نأخذ عنوان bar إلى p_bar_2، وعليه فإن p_bar1 يصير فارغًا nullptr، وp_bar2 صار يساوي bar&: big_struct *p_bar1 = p_bar0; big_struct *p_bar2 = &bar; لنجعل الآن p_bar0 يساوي &bar: p_bar0 = p_bar2; p_bar2 = nullptr; // p_bar0 == &bar // p_bar1 == nullptr // p_bar2 == nullptr ذلك يخالف سلوك المراجع (references) كما يلي: لا يؤدي إسناد مؤشّر إلى آخر إلى استبدال الذاكرة التي يشير إليها المؤشِّر المُسنَد إليه؛ يمكن أن تكون المؤشّرات فارغة. عنوان العامل ينبغي أن يكون صريحًا. الوصول إلى محتوى المؤشّر يتطلب الوصول إلى العنوان استخدام العامل &، أمّا الوصول إلى المحتوى فيتطلب استخدام عامل التحصيل * كسابقة (prefix)، عندما يُحصَّل مؤشّر فإنّه يُصبح متغيرًا من النوع الأساسي (underlying)، بل يكون مرجعًا إليه على الحقيقة، ويمكن بعدها قراءته وتعديله إن لم يكن ثابتًا (const). انظر المثال التالي حيث يشير p_bar0 إلى bar ويطبع 5 في الخطوة 1، ثم نسند القيمة التي يشير إليها المؤشر p_bar0 إلى baz في الخطوة 2، لتحتوي الأخيرة نسخة من البيانات المشار إليها من قبل p_bar0 في الخطوة 3: (*p_bar0).foo1 = 5; // 1 الخطوة std::cout << "bar.foo1 = " << bar.foo1 << std::endl; // 2 الخطوة big_struct baz; baz = *p_bar0; // 3 الخطوة std::cout << "baz.foo1 = " << baz.foo1 << std::endl; يُختصر العامليْن * و . بالرمز ->: std::cout << "bar.foo1 = " << (*p_bar0).foo1 << std::endl; // 5 std::cout << "bar.foo1 = " << p_bar0->foo1 << std::endl; // 5 تحصيل مؤشّرات غير صالحة يجب أن تتأكد عند تحصيل مؤشر إلى أنّه يشير إلى بيانات صالحة، إذ قد يؤدي تحصيل مؤشّر غير صالح (أو مؤشّر فارغ) إلى حدوث خرق للذاكرة (memory access violation)، أو إلى قراءة البيانات المُهمَلة (garbage data) أو كتابتها. big_struct *never_do_this() { // never_do_this هذا متغير محلي، ولا يوجد خارج big_struct retval; retval.foo1 = 11; // retval إعادة عنوان return &retval; // تم تدميرها، وأي شيفرة تستخدم القيمة المعادة من قبل retval // لها مؤشّر إلى مساحة في الذاكرة `never_do_this` // تحتوي البيانات المهملة } في مثل هذه الحالة، يطلق المصرّفان g++ و clang++ التحذيرات التالية: (Clang) warning: address of stack memory associated with local variable 'retval' returned [- Wreturn-stack-address] (Gcc) warning: address of local variable ‘retval’ returned [-Wreturn-local-addr] وعليه يجب توخي الحذر عند تمرير المؤشّرات كوسائط إلى دوال، إذ أنّها قد تكون فارغة: void naive_code(big_struct *ptr_big_struct) { // ... `ptr_big_struct` شيفرة لا تتحقق من صلاحية ptr_big_struct -> foo1 = 12; } // (ٍSegmentation) خطأ في التجزئة naive_code(nullptr); حسابيات المؤشرات الزيادة والإنقَاص المؤشرات قابلة للزيادة أو الإنقاص منها، وتؤدي زيادة مؤشّر إلى نقل قيمة المؤشّر إلى العنصر الذي يلي العنصر المُشار إليه حاليًا في [المصفوفة](رابط الفصل 8)، أمّا إنقاصه فينقله إلى العنصر السابق في المصفوفة. كذلك لا يجوز إجراء العمليات الحسابيّة على المؤشّرات التي تشير إلى نوع ناقص مثل void. char* str = new char[10]; // str = 0x010 ++str; // str = 0x011 in this case sizeof(char) = 1 byte int* arr = new int[10]; // arr = 0x00100 ++arr; // arr = 0x00104 if sizeof(int) = 4 bytes void* ptr = (void* ) new char[10]; ++ptr; // نوع ناقص void في حال زيادة مؤشّر يشير إلى العنصر الأخير، فإنّ المؤشّر سيشير إلى العنصر الذي يلي العنصر الأخير في المصفوفة، ولا يمكن تحصيل مؤشّر كهذا لكن يمكن إنقاصه، وتؤدي زيادة مؤشّر إلى العنصر الذي يلي العنصر الأخير في المصفوفة أو إنقاص مؤشّر إلى العنصر الأول إلى سلوك غير محدّد. أيضًا، يمكن التعامل مع المؤشّرات التي تشير إلى أنواع أخرى غير المصفوفات كما لو كانت تشير إلى مصفوفات أحادية. الجمع والطرح يمكن إضافة قيم عددية صحيحة إلى المؤشّرات، ويكافئ ذلك زيادة قيمة المؤشّر بعدد ما خلاف 1، كما يمكن أيضًا طرح قيم عددية صحيحة من المؤشّرات أيضًا مما يؤدي إلى إنقاص قيمة المؤشّر، ويجب أن يشير المؤشر إلى نوع كامل كما هو الحال مع الزيادة والإنقَاص اللذيْن شرحناهما أعلاه. char* str = new char[10]; // str = 0x010 str += 2; // str = 0x010 + 2 * sizeof(char) = 0x012 int* arr = new int[10]; // arr = 0x100 arr += 2; // arr = 0x100 + 2 * sizeof(int) = 0x108, assuming sizeof(int) == 4. الفرق بين المؤشرات يمكن حساب الفرق بين مؤشِّرين يشيران إلى نفس النوع، لكن يجب أن يكون المؤشّران داخل نفس كائن المصفوفة، وإلا قد تحدث نتائج غير متوقعة. فإذا كان لدينا مؤشّران P و Q في نفس المصفوفة، وكان P يشير إلى العنصر رقم i في المصفوفة و Q يشير إلى العنصر رقم j، فإنّ P - Q سيشير إلى العنصر رقم i - j في المصفوفة، ويكون نوع النتيجة std::ptrdiff_t من <cstddef>. char* start = new char[10]; // str = 0x010 char* test = &start[5]; std::ptrdiff_t diff = test - start; // 5 يساوي std::ptrdiff_t diff = start - test; // -5 يساوي المؤشرات إلى الأعضاء مؤشّرات إلى الدوال التابعة الساكنة تشبه الدوال التابعة الساكنة (static) دوالَّ C/C++ العادية باستثناء نطاقها، كما يلي: تكون موجودة داخل الصنف (class)، لذا يجب أن يُرفق اسمها باسم الصنف. لديها حق الوصول للأعضاء العامّة (public) والمحميّة (protected) والخاصّة (private)، فإذا كان لديك حق الوصول إلى دالة تابع ساكن static وسميته بشكل صحيح (أي أرفقته باسم الكائن الذي ينتمي إليه)، فيمكنك الإشارة إلى تلك الدالّة كما لو كانت دالّة عادية: typedef int Fn(int); // هي نوع دالة تأخذ عددًا صحيحًا وتعيد عددًا صحيحا Fn // 'Fn' من النّوع MyFn() ّلاحظ أن int MyFn(int i) { return 2 * i; } class Class { public: // 'Fn' من النوع Static() لاحظ أن static int Static(int i) { return 3 * i; } }; // الصنف int main() { Fn *fn; // Fn هو مؤشّر إلى النوع fn fn = &MyFn; // أشِر إلى دالة ما fn(3); // استدعها fn = &Class::Static; // أشِر إلى دالة أخرى fn(4); // استدعها } // main() مؤشّرات إلى الدوال التوابع للوصول إلى دالة تابعة من صنف ما، يجب أن يكون لديك "مقبض" (handle) للنسخة المحدّدة، إمّا للنسخة نفسها أو مؤشّر أو مرجع إليها. وإذا كان لديك نسخة من صنف فيمكنك الإشارة إلى أعضائها باستخدام مؤشّر-إلى-عضو (pointer-to-member)، لاحظ أنّه يجب أن يكون المؤشّر من نفس النوع الذي تشير إليه تلك الأعضاء. typedef int Fn(int); // هي نوع دالة تأخذ عددًا صحيحًا وتعيد عددًا صحيحًا Fn class Class { public: // 'Fn' من النوع A() لاحظ أن int A(int a) { return 2 * a; } // 'Fn' من النوع B() لاحظ أن int B(int b) { return 3 * b; } }; // صنف int main() { Class c; // تحتاج نسخة صنف لتجرب معها Class * p = & c; // تحتاج مؤشر صنف لتجرب معه Fn Class::*fn; // داخل الصنف Fn هو مؤشّر إلى النوع fn fn = &Class::A; // داخل أي صنف A يشير إلى fn (c.*fn)(5); // Fn عبر c الخاصة بـ A مرر 5 إلى دالة fn = & Class::B; // داخل أي صنف B يشير الآن إلى fn (p ->*fn)(6); // Fn عبر c الخاصة بـ B مرر 6 إلى دالة } // main() على خلاف المؤشّرات التي تشير إلى متغيرات الأعضاء كما في المثال السابق، ويجب أن يكون الجمع بين نسخة الصنف والمؤشّر-إلى-العضو باستخدام الأقواس، الأمر الذي قد يبدو غريبًا كأن *. و *<- لم تكونا كافيتين! المؤشّرات إلى متغيرات الأعضاء للوصول إلى عضو من class، يجب أن يكون لديك "مقبض" للنسخة المحددة، إما النسخة نفسها أو مؤشّر أو مرجع إليها. وإذا كانت لديك نسخة من الصنف class فتستطيع الإشارة إلى أعضائها باستخدام مؤشّر-إلى-عضو، لاحظ أنّ المؤشّر يجب أن يكون من نفس النوع الذي يشير إليه تلك الأعضاء. class Class { public: int x, y, z; char m, n, o; }; // صنف int x; // (Global) متغير عام int main() { Class c; // تحتاج نسخة صنف لتجرب معها Class *p = &c; // تحتاج مؤشر صنف لتجرب معه int *p_i; // int مؤشّر إلى p_i = & x // x المؤشّر يشير الآن إلى p_i = &c.x; // c الخاص بـ x الآن يشير إلى العنصر int Class::*p_C_i; // مؤشّر إلى عدد صحيح داخل الصنف p_C_i = &Class::x; // داخل الصنف x الإشارة إلى int i = c.*p_C_i; // c داخل النسخة x لإيجاد p_c_i استخدام p_C_i = &Class::y; // داخل أي صنف y يشير إلى i = c.*p_C_i // c داخل النسخة y لإيجاد p_c_i استخدام p_C_i = &Class::m; // خطأ char Class::*p_C_c = &Class::m; // هذا أفضل } // main() تتطلّب صياغة المؤشّر العضوي بعض العناصر الإضافية: لتعريف نوع المؤشّر، تحتاج إلى ذِكر النّوع الأساسي إضافة إلى حقيقة أنه داخل صنف: int Class::*ptr;. إذا كان لديك صنف أو مرجع وتريد استخدامه مع مؤشّر-إلى-عضو فستحتاج إلى استخدام المعامل .* (يشبه المعامل .). إذا كان لديك مؤشّر يشير إلى صنف وتريد استخدامه مع مؤشّر-إلى-عضو، فستحتاج إلى استخدام المعامل ->* (يشبه المعامل ->). مؤشّرات إلى متغيرات الأعضاء الساكنة متغيرات الأعضاء الساكنة تشبه متغيرات C / C++ العادية، باستثناء نطاقها: إذ تكون موجودة داخل الصنف (class)، لذلك يجب أن يُرفق اسمها مع اسم الصنف؛ تتمتع بحق الوصول إلى العناصر العامّة (public) والمحميّة (protected) والخاصّة (private). لذلك إن كان لديك حق الوصول إلى متغير عضو ساكن وأرفقته باسم الكائن الذي ينتمي إليه بالشكل الصحيح، فيمكنك الإشارة إلى ذلك المتغير مثل أيّ متغير عادي: class Class { public: static int i; }; // صنف int Class::i = 1; // i تعريف قيمة int j = 2; // متغير عام int main() { int k = 3; // متغير محلي int *p; p = &k; // k الإشارة إلى *p = 2; // تعديل المؤشر p = &j; // j الإشارة إلى *p = 3; // تعديل المؤشر p = &Class::i; // Class::i الإشارة إلى *p = 4; // تعديل المؤشر } // main() مؤشر This جميع الدوال التوابع غير الساكنة لها معامِل خفيّ، وهو مؤشّر يشير إلى نسخة من الصنف يُسمّى this، ويُدرج هذا المعامل خُفية في بداية قائمة المعاملات ويُعالَج من قِبل المُصرِّف. ويمكن الوصول إلى عضو من الصنف داخل دالة تابعة عبر this، مما يسمح للمُصرِّف باستخدام دالة تابعة غير ساكنة لكل النسخ، ويسمح لدالة تابعة ما باستدعاء الدوال التوابع الأخرى بأشكال متعددة. struct ThisPointer { int i; ThisPointer(int ii); virtual void func(); int get_i() const; void set_i(int ii); }; ThisPointer::ThisPointer(int ii): i(ii) {} يعيد المصرّف كتابتها على النحو التالي: ThisPointer::ThisPointer(int ii): this -> i(ii) {} يكون المنشئ مسؤولًا عن تحويل الذاكرة المخصصة إلى this، وبما أنه المسؤول عن إنشاء الكائن أيضًا فإن this لن تكون صالحة تمامًا إلى أن يتم إنشاء النسخة. نتابع المثال: /* virtual */ void ThisPointer::func() { if (some_external_condition) { set_i(182); } else { i = 218; } } // المصرّف يعيد كتابتها على النحو التالي /* virtual */ void ThisPointer::func(ThisPointer* this) { if (some_external_condition) { this -> set_i(182); } else { this -> i = 218; } } int ThisPointer::get_i() const { return i; } // المصرّف يعيد كتابتها كـ int ThisPointer::get_i(const ThisPointer* this) { return this -> i; } void ThisPointer::set_i(int ii) { i = ii; } // المصرّف يعيد كتابتها كـ void ThisPointer::set_i(ThisPointer* this, int ii) { this -> i = ii; } يمكن استخدام this بأمان -ضمنيًا أو بشكل صريح- داخل المنشئ للوصول إلى أيّ حقل سبقت تهيئته أو حقل في صنف أب (parent class)، لكن بالمقابل فإن الوصول -ضمنيًا أو بشكل صريح- إلى الحقول التي لم تُهيّأ بعد أو إلى أيّ حقل في الصنف المشتق، يُعد من الممارسات غير الآمنة نظرًا لأنّ الصنف المشتق لم يُهيّأ بعد، وعليه تكون حقُوله ليست لا مهيّأة ولا موجودة. كذلك ليس من الآمن استدعاء الدوال التابعة الوهميّة عبر this في المُنشئ، ذلك أنّ دوالّ الصنف المشتق لن تُأخذ بالحسبان نظرًا لأنّ الصنف المشتق لم يُنشأ بعد، وعليه فإنّ مُنشئه لم يُحدِّث vtable بعد. لاحظ أيضًا أن نوع الكائن في المُنشئ هو النوع الذي يُنشئه المُنشئ نفسه، ويبقى هذا صحيحًا حتى لو عُرِّف الكائن على أنه نوع مشتق. انظر المثال التالي حيث يكون كل من ctd_good و ctd_bad من نوع CtorThisBase داخل CtorThisBase()، ومن نوع CtorThis داخل CtorThis()، رغم أنّ نوعهما المعياري هو CtorThisDerived. ومع الاستمرار في الاشتقاق من الصنف الأساسي فإنّ النسخة تمرّ تدريجيًا عبر التسلسل الهرمي للصنف حتى تصبح نسخةً مكتملة من النوع المراد لها. class CtorThisBase { short s; public: CtorThisBase(): s(516) {} }; class CtorThis: public CtorThisBase { int i, j, k; public: // منشئ جيد CtorThis(): i(s + 42), j(this->i), k(j) {} // منشئ سيء CtorThis(int ii): i(ii), j(this->k), k(b ? 51 : -51) { virt_func(); } virtual void virt_func() { i += 2; } }; class CtorThisDerived: public CtorThis { bool b; public: CtorThisDerived(): b(true) {} CtorThisDerived(int ii): CtorThis(ii), b(false) {} void virt_func() override { k += (2 * i); } }; // ... CtorThisDerived ctd_good; CtorThisDerived ctd_bad(3); باعتبار هذه الأصناف والتوابع: في المُنشئ الجيّد (انظر الشيفرة)، فإنّه بالنسبة إلى ctd_good: يُنشأ CtorThisBase بالكامل بحلول وقت إدخال المُنشئ CtorThis، لذا تكون s في حالة صالحة أثناء تهيئة i، ومن ثم يمكن الوصول إليها. تُهيّأ i قبل الوصول إلى j(this->i)، لذلك تكون i في حالة صالحة أثناء تهيئة j، ومن ثم يمكن الوصول إليها. تُهيّأ j قبل الوصول إلى k(j)، لذلك تكون j في حالة صالحة أثناء تهيئة k، ومن ثم يمكن الوصول إليها. في المُنشئ السيئ، فإنّه بالنسبة إلى ctd_bad: تُهيّأ k بعد الوصول إلى j(this->k)، لذلك تكون k في حالة غير صالحة أثناء تهيئة j ، ويحدث سلوك غير محدد عند محاولة الوصول إليها. لا يُنشأ CtorThisDerived إلّا بعد إنشاء CtorThis، لذلك تكون b في حالة غير صالحة أثناء تهيئة k ، وقد تؤدّي محاولة الوصول إليها إلى سلوك غير محدّد. يبقى الكائنctd_bad من النوع CtorThis إلى أن يغادر التابع CtorThis()، ولن يتم تحديثه لاستخدام الجدول الوهمي (vtable) الخاص بالصنف المشتق CtorThisDerived حتّى التابع CtorThisDerived(). وعليه تستدعي virt_func () التابع CtorThis::virt_func() بغض النظر إن كان الهدف هو استدعاؤه هو أو استدعاء CtorThisDerived::virt_func(). استخدام المؤشر this للوصول إلى بيانات الأعضاء لا يعدّ استخدام مؤشّر this في هذا السياق أمرًا ضروريًا، ولكنه سيجعل الشفرة أوضح للقارئ من خلال الإشارة إلى أنّ دالّة ما أو متغيرًا هو عضو من الصنف. انظر هذا المثال: // مثال على هذا المؤشّر #include <iostream> #include <string> using std::cout; using std::endl; class Class { public: Class(); ~Class(); int getPrivateNumber() const; private: int private_number = 42; }; Class::Class() {} Class::~Class() {} int Class::getPrivateNumber() const { return this -> private_number; } int main() { Class class_example; cout << class_example.getPrivateNumber() << endl; } يمكنك مشاهدة مثال حيّ من هنا. استخدام المؤشر this للتفريق بين المعامِلات وبيانات الأعضاء هذه استراتيجية مفيدة لتمييز البيانات العضويّة عن المعاملات، لنأخذ مثالًا: #include <iostream> #include <string> using std::cout; using std::endl; /* * @class Dog * @member name * Dog's name * @function bark * Dog Barks! * @function getName * To Get Private * Name Variable */ class Dog { public: Dog(std::string name); ~Dog(); void bark() const; std::string getName() const; private: std::string name; }; Dog::Dog(std::string name) { this->name هو متغير الاسم من صنف dog، ويكون name من معامِل الدالة. نتابع: this -> name = name; } Dog::~Dog() {} void Dog::bark() const { cout << "BARK" << endl; } std::string Dog::getName() const { return this -> name; } int main() { Dog dog("Max"); cout << dog.getName() << endl; dog.bark(); } كما ترى هنا في المنشئ فقد نفذنا ما يلي: this->name = name; لاحظ أننا أسنَدنا المُعاملَ name إلى اسم المتغير الخاصّ من الصنف Dog (this->name). انظر هنا لرؤية تطبيق حي للشيفرة أعلاه. المؤهلات الخاصة بالمؤشر this (this Pointer CV-Qualifiers) يمكن للمؤشّر this أن يؤهَّل (cv-qualified) - أي تُحدَّد طبيعته، أهو ثابت أم متغير - مثل أيّ مؤشّر آخر. لكن بما أن المعامل this لا يُدرَج في قائمة المعاملات، فيجب استخدام صيغة خاصة بالمؤشّر this؛ لذا تُدرَج المؤهّلات (cv-qualifiers) بعد قائمة المعاملات، وقبل متن الدالة. struct ThisCVQ { void no_qualifier() {} // "this" is: ThisCVQ* void c_qualifier() const {} // "this" is: const ThisCVQ* void v_qualifier() volatile {} // "this" is: volatile ThisCVQ* void cv_qualifier() const volatile {} // "this" is: const volatile ThisCVQ* }; بما أن this معامِل فيمكن زيادة تحميل (overload) دالّة على أساس مؤهِّلات this. struct CVOverload { int func() { return 3; } int func() const { return 33; } int func() volatile { return 333; } int func() const volatile { return 3333; } }; لن تكون الدالّة قادرة على الكتابة في متغيرات الأعضاء من خلال this عندما يكون ثابتًا (const) (بما في ذلك const volatile)، سواء ضمنيًا أو بشكل صريح، والاستثناء الوحيد لهذا هو متغيرات الأعضاء القابلة للتغيير (mutable)، والتي يمكن أن تُكتب بغض النظر عن ثبوتيّتها. لهذا تُستخدم const للإشارة إلى أنّ التابع لا يغيِّر الحالة المنطقية للكائن -الطريقة التي يظهر بها الكائن للعالم الخارجي- حتى لو عدّل الحالة المادية، وهي الطريقة التي يظهر بها الكائن داخليًا. لاحظ أنّ لغة C++ تبني الثباتيّة (constness) على الحالة المنطقية وليس الحالة المادية. class DoSomethingComplexAndOrExpensive { mutable ResultType cached_result; mutable bool state_changed; ResultType calculate_result(); void modify_somehow(const Param & p); // ... public: DoSomethingComplexAndOrExpensive(Param p): state_changed(true) { modify_somehow(p); } void change_state(Param p) { modify_somehow(p); state_changed = true; } // إعادة نتيجة تحتاج إلى حسابات معقدة // يُحدَّد كثابت بما أنه لا يوجد سبب لتعديل الحالة المنطقية. ResultType get_result() const; }; ResultType DoSomethingComplexAndOrExpensive::get_result() const { يمكن تعديل cached_result و state_changed حتى مع مؤشر const ثابت، ورغم ان الدالة لن تغير الحالة المنطقية إلا أنها تعدّل الحالة المادية بتخزين النتيجة مؤقتًا كي لا تحتاج إلى إعادة حسابها في كل مرة تُستدعى الدالة فيها. ترى هذا واضحًا من قابلية كل من cached_result و state_changed للتغيير. انظر: if (state_changed) { cached_result = calculate_result(); state_changed = false; } return cached_result; } ورغم أنك تستطيع استخدام const_cast مع this لجعله غير مؤهّل (non-cv-qualified)، إلا أنّه يوصى بتجنّب ذلك، ويُنصح باستخدام mutable بدلًا منه. كذلك قد تُحدث الكائنات غير المؤهّلة (const_cast) سلوكًا غير مُحدّدٍ عند استخدامها على كائن ثابت (const)، على عكس mutable التي صُمِّمت لتكون آمنة للاستخدام. هناك استثناء لهذه القاعدة، وهو تعريف توابع وصول غير مؤهّلة (non-cv-qualified accessors) عبر توابع وصول ثابتة (const)، نظرًا لأنّ ذلك يضمن أنّ الكائن لن يكون ثابتًا إذا استُدعِيت النسخة غير المؤهّلة، لذلك لا توجد مخاطرة هنا. class CVAccessor { int arr[5]; public: const int & get_arr_element(size_t i) const { return arr[i]; } int & get_arr_element(size_t i) { return const_cast < int & > (const_cast < const CVAccessor * > (this) -> get_arr_element(i)); } }; هذا يمنع التكرار غير الضروري للشيفرة. وكما في المؤشّرات العادية، فإن كان this متغيّرًا volatile -بما في ذلك const volatile- فإنه سيُحمَّل من الذاكرة في كل مرة يتم الوصول إليه بدلاً من تخزينه مؤقتًا، ومن ثم فإنّ تأثيره على سرعة البرنامج يشبه تأُثير التصريح عن مؤشّر متغيّر volatile، لذا يجب توخّي الحذر. لاحظ أنّه إذا كانت نسخة ما مؤهّلةً ثباتيًّا، فإنّ الدوال التابعة الوحيدة التي يُسمح لها بالوصول إليها هي التي يحمل المؤشّر this الخاص بها نفس التأهيل للنسخة ذاتها على الأقل: يمكن للنُّسخ غير المؤهّلة أن تصل إلى كل الدوال التابعة. يمكن للنسخ الثابتة (const) الوصول إلى الدوال ذات تأهيل const و const volatile. يمكن للنسخ المتغيّرة (volatile) الوصول إلى الدوالّ ذات تأهيل volatile و const volatile. يمكن للنسخ ذات التأهيل const volatile الوصول إلى الدوالّ ذات تأهيل const volatile. هذا أحد المبادئ الأساسية للثباتيّة: struct CVAccess { void func() {} void func_c() const {} void func_v() volatile {} void func_cv() const volatile {} }; CVAccess cva; cva.func(); // جيد cva.func_c(); // جيد cva.func_v(); // جيد cva.func_cv(); // جيد const CVAccess c_cva; c_cva.func(); // خطأ c_cva.func_c(); // جيد c_cva.func_v(); // خطأ c_cva.func_cv(); // جيد volatile CVAccess v_cva; v_cva.func(); // خطأ v_cva.func_c(); // خطأ v_cva.func_v(); // جيد v_cva.func_cv(); // جيد const volatile CVAccess cv_cva; cv_cva.func(); // خطأ cv_cva.func_c(); // خطأ cv_cva.func_v(); // خطأ cv_cva.func_cv(); // جيد مؤهلات مؤشر this المرجعية الإصدار C++ 11 يمكننا تطبيق المؤهّلات المرجعِيّة (ref-qualifiers) على *this على نحو مماثل لمؤهّلات this الثباتيّة، وتُستخدم المؤهّلات المرجعيّة للاختيار بين دلالات المرجع العادية والقيميّة اليمينيّة، ممّا يسمح للمُصرِّف أن يستخدم دلالات النسخ (copy) أو النقل (move) وفقًا لأيّهما أنسب، وتُطبَّق على *this بدلاً من this. وتجدر الإشارة أنه رغم استخدام المؤهِّلات المرجعية لصيغة المراجع (reference syntax) فلا يزال this مؤشّرًا، كذلك لاحظ أنّ المؤهّلات المرجِعيّة لا تغيّر في الواقع نوع *this، بيْد أنه من الأسهل وصفها وفهمها من هذا المنظور. struct RefQualifiers { std::string s; RefQualifiers(const std::string & ss = "The nameless one."): s(ss) {} // نسخة عادية void func() & { std::cout << "Accessed on normal instance " << s << std::endl; } // قيمة يمينية void func() && { std::cout << "Accessed on temporary instance " << s << std::endl; } const std::string & still_a_pointer() & { return this -> s; } const std::string & still_a_pointer() && { this -> s = "Bob"; return this -> s; } }; // ... RefQualifiers rf("Fred"); rf.func(); // الخرج: Accessed on normal instance Fred RefQualifiers {}.func(); // الخرج: Accessed on temporary instance The nameless one لا يمكن زيادة تحميل دالة تابعة مع مؤهّل مرجعي مرة وبدونه مرّة أخرى، بل يختار المبرمج أحد الأمرَين. الجميل في الأمر أنه يمكن استخدام المؤهّلات الثباتيّة مع المؤهلات المرجعيّة، مما يسمح باتباع قواعد الثباتيّة. انظر المثال التالي: struct RefCV { void func() & {} void func() && {} void func() const & {} void func() const && {} void func() volatile & {} void func() volatile && {} void func() const volatile & {} void func() const volatile && {} }; هذا الدرس جزء من سلسلة دروس عن C++. ترجمة -بتصرّف- للفصول 30 وحتى 32 من كتاب C++ Notes for Professionals
-
عامل الإسناد (Assignment Operator) يُستخدم "عامل الإسناد" لإحلال بيانات كائن ما مكان بيانات كائن موجود سلفًا (مُهيّأ مُسبقًا). انظر المثال التالي: // عامل الإسناد #include <iostream> #include <string> using std::cout; using std::endl; class Foo { public: Foo(int data) { this -> data = data; }~Foo() {}; Foo & operator = (const Foo & rhs) { data = rhs.data; return *this; } int data; }; int main() { Foo foo(2); // Foo(int data) استدعاء Foo foo2(42); foo = foo2; // استدعاء عامل الإسناد cout << foo.data << endl; // 42 } تستطيع هنا أن تلاحظ أنّنا استدعينا عامل الإسناد بعد أن هيّئنا الكائن foo، ثم نسند بعد ذلك foo2 إلى foo، وتكون جميع التغييرات التي ستحدث عند استدعاء عامل الإسناد مُعرّفة في الدالة operator=. انظر هذا المثال الحي. منشئ النسخ (Copy Constructor) مُنشئ النَّسخ من ناحية أخرى على النقيض من عامل الإسناد، إذ يُستخدم لتهيئة كائن غير موجود مسبقًا (أو غير مهيّأ مسبقًا)، هذا يعني أنه ينسخ جميع البيانات من الكائن الذي يُسند إليه، دون تهيئة الكائن الذي يتم نسخه بالفعل. دعنا نلقي نظرة على نفس الشيفرة أعلاه ولكن مع استخدام مُنشئ النسخ بدلًا من منشئ الإسناد: //منشئ النسخ #include <iostream> #include <string> using std::cout; using std::endl; class Foo { public: Foo(int data) { this -> data = data; }~Foo() {}; Foo(const Foo & rhs) { data = rhs.data; } int data; }; int main() { Foo foo(2); //Foo(int data) استدعاء Foo foo2 = foo; // استدعاء منشئ النسخ cout << foo2.data << endl; } في التعبير Foo foo2 = foo; في الدالّة الرئيسية، أسندنا الكائن على الفور قبل تهيئته، ما يعني أنه مُنشِئ نسخ، لكن لاحظ أنّنا لم نكن بحاجة إلى تمرير المعامل (int) للكائن foo2، لأننا سحبنا البيانات السابقة تلقائيًا من الكائن foo. انظر هذا المثال الحي للمزيد. مُنشئ النسخ مقابل منشئ الإسناد بعد هذه النظرة السريعة على مفهومي مُنشئ النسخ ومنشئ الإسناد ورؤية مثال عن عمل كلٍ منهما، سننظر الآن كيف يمكن استخدامهما معًا في نفس الشيفرة: // منشئ النسخ مقابل منشئ الإسناد #include <iostream> #include <string> using std::cout; using std::endl; class Foo { public: Foo(int data) { this -> data = data; }~Foo() {}; Foo(const Foo & rhs) { data = rhs.data; } Foo & operator = (const Foo & rhs) { data = rhs.data; return *this; } int data; }; int main() { Foo foo(2); //Foo(int data) استدعاء المنشئ العادي Foo foo2 = foo; // استدعاء منشئ النسخ cout << foo2.data << endl; Foo foo3(42); foo3 = foo; // استدعاء منشئ الإسناد cout << foo3.data << endl; } الناتج: 2 2 لقد استدعينا في البداية مُنشئ النسخ عن طريق التعبير Foo foo2 = foo; لأننا لم نهيّئه من قبل، ثم استدعينا بعد ذلك عامل الإسناد على foo3 لأنه سبقت تهيئته (foo3=foo). هذا الدرس جزء من سلسلة دروس عن C++. ترجمة -بتصرّف- للفصل Chapter 29: Copying vs Assignment من كتاب C++ Notes for Professionals