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

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

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

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

  • Days Won

    6

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

38 Excellent

9 متابعين

  1. نبدأ بدايةً بسرد بعض الأمور المتعلقة بالقاموس أو النوع 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
  2. المتّجه هو مصفوفة ديناميكية تخزّن البيانات تلقائيًّا، ويمكن الوصول إلى العناصر الموجودة في المتّجه بنفس طريقة المصفوفات، مع ميزة أنّ المتّجهات يمكن أن يتغير حجمها ديناميكيًا. وبشأن التخزين، تُوضع بيانات المتجه عادة في ذاكرة مُخصّصة ديناميكيًا، ومن ثم فإنها تتطلّب بعض الحمل الزائد (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
  3. النوع 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‎ أو للعمل على السلاسل النصّية التي ينبغي حفظ محتوياتها، ثم يمكن العمل على النسخة بعد ذلك . ستكون تكلفة هذه الخيارات جزءًا من تكلفة حجز القطع، لكن إذا كنت تريد استخدام خوارزميات سريعة ولم تتمكن من تجاوز إشكاليات ‎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
  4. مثال بسيط يحتوي المثال التالي على شيفرة يُراد تقسيمها إلى عدّة ملفّات مصدرية، سننظر الآن في كل ملف على حدة: الملفات المصدرية 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
  5. تُستخدم فضاءات الأسماء (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
  6. تُسمى الدوال المُعرّفة بالكلمة المفتاحية ‎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
  7. الدوال الوهمية النهائية (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
  8. يمكن تعريف عوامل مثل ‎+‎ و ‎->‎ في ++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
  9. تصنيف كلفة تمرير وسيط إلى معامل يقسم تحليل التحميل الزائد تكلفة تمرير وسيط (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
  10. أساسيات الأصناف الصنف (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
  11. يُقدر أنّ عدد لغات البرمجة الإجمالي يتجاوز 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.
  12. الملكية الفريدة الإصدار ≥ 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
  13. المؤشّر هو عنوان يشير إلى موقع في الذاكرة، وتُستخدم المؤشرات عادة للسماح للدوالّ أو هياكل البيانات بالحصول على معلومات عن الذاكرة وتعديلها دون الحاجة إلى نسخ الذاكرة المشار إليها، والمؤشّرات قابلة للاستخدام سواءً مع الأنواع الأوليّة (المٌضمّنة) أو الأنواع التي يعرّفها المستخدم. وتستفيد المؤشّرات من عامل التحصيل ‎*‎ و العنونة ‎&‎ و السهم ‎->‎، إذ يُستخدم عاملا * و ‎‎->‎ للوصول إلى الذاكرة المشار إليها، فيما يُستخدم المعامل ‎&‎ للحصول على عنوانٍ في الذاكرة. عمليات المؤشرات يوجد مُعاملان يختصان بالمؤشّرات، وهما: معامل العنونة (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
  14. عامل الإسناد (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
  15. تتشابه المراجع في سلوكها وتختلف عن المؤشّرات الثابتة (const pointers)، وتُعرَّف عن طريق إتباع الرمز ‎&‎ ‏‏(ampersand) باسم نوع. int i = 10; int &refi = i; يمثّل ‎refi‎ في المثال أعلاه مرجعًا مربوطًا (reference bound) إلى ‎i‎، كذلك فإن المراجع تُجرِّد مفهوم المؤشرات وتتصرف كاسم للكائن المُشار إليه: refi = 20; // i = 20; يمكنك أيضًا تعريف عدة مراجع في تعريف واحد: int i = 10, j = 20; int &refi = i, &refj = j; // خطأ شائع // int& refi = i, k = j; في المثال السابق ستكون refi من نوع ‎&int رغم أن k ستكون من نوع int وليس ‎&int. أيضًا، يجب تهيئة المراجع بشكل صحيح في التعريف، ولن يمكنك تعديلها بعد ذلك. انظر المثال التالي حيث تؤدي الشيفرة إلى خطأ في التصريف لأن الإعلان عن متغير المرجع i يتطلب مهيِّئًا. int &i; كذلك لا يمكن ربط مرجع إلى المؤشر الفارغ ‎nullptr‎ مباشرة، على عكس المؤشرات، انظر المثال التالي حيث نحصل على خطأ لاستحالة ربط مرجع غير ثابت لقيمة يسارية من نوع int بعنصر من نوع nullptr_t: int *const ptri = nullptr; int &refi = nullptr; الدلالات القيميّة والمرجعية يكون لنوع ما دلالة قيميّة (value semantics) إذا كانت حالة الكائن القابلة للمُلاحظة مختلفة وظيفيًا عن جميع الكائنات الأخرى من ذلك النوع. هذا يعني أنه إذا نسخت أحد الكائنات فستحصل على كائن جديد، ولن تؤثر التعديلات على الكائن الجديد بأي شكل من الأشكال على الكائن القديم. معظم أنواع C++‎ الأساسية لها دلالات قيميّة، انظر المثال التالي حيث تطبع std::cout << i الرقم 5 لأن i لم تتأثر بالتغييرات التي أجريت على j. int i = 5; int j = i; // منسوخ j += 20; std::cout << i; // j لم تتأثر بالتغييرات التي أجريناها على i تطبع 5، لأنّ معظم الأنواع المُعرّفة في المكتبة القياسية لها دلالة قيميّة أيضًا: std::vector<int> v1(5, 12); // مصفوفة خماسية كل قيمها تساوي 12 std::vector<int> v2 = v1; // نسخ المتجهة v2[3] = 6; v2[4] = 9; std::cout << v1[3] << " " << v1[4]; // "12 12" يقال إنّ نوعًا ما له دلالة مرجعيّة (reference semantics) إذا تشاركت نُسخ ذلك النوع حالتها القابلة للملاحظة مع كائنات أخرى (خارجية)، بحيث يؤدي تعديل كائن واحد إلى تغيير حالة كائن آخر، وللمؤشّرات دلالة قيميّة في C++‎ فيما يتعلّق بهويّة الكائن الذي تشير إليه، ولها دلالة مرجعيّة فيما يتعلق بحالة الكائن الذي تشير إليه: int *pi = new int(4); int *pi2 = pi; pi = new int(16); assert(pi2 != pi); // تتحقق دائما int *pj = pi; *pj += 5; std::cout << *pi; // يشيران إلى نفس العنصر pj و pi تطبع 9 لأن كذلك فإن مراجع C++‎ لها دلالة مرجعية. النسخ العميق ودعم النقل إذا كنت تريد أن تجعل لنوع ما دلالة قيميّة وكان ذلك النوع يحتاج إلى تخزين الكائنات الديناميكية (dynamically allocated)، فسيحتاج ذلك النوع عند عمليات النسخ إلى تخصيص (allocate) نسخ جديدة من تلك الكائنات، كما يجب أن يفعل ذلك أيضًا عند تعيين النسخة (copy assignment). يُسمّى هذا النوع من النسخ "النسخ العميق" (deep copy) إذ أنّه يحوّل الدلالة المرجعية إلى دلالة قيمية: struct Inner { int i; }; const int NUM_INNER = 5; class Value { private: Inner *array_; // في العادة يكون لها دلالة مرجعية public: Value(): array_(new Inner[NUM_INNER]) {}~Value() { delete[] array_; } Value(const Value &val): array_(new Inner[NUM_INNER]) { for (int i = 0; i < NUM_INNER; ++i) array_[i] = val.array_[i]; } Value &operator = (const Value &val) { for (int i = 0; i < NUM_INNER; ++i) array_[i] = val.array_[i]; return *this; } }; الإصدار ≥ C++‎ 11 تسمح الدلالة النقليّة (Move semantics) لنوع مثل الصنف ‎Value‎ أن يتجنب نسخ بياناته المرجعية (referenced data)، وإذا استعمل المُستخدم القيمة بطريقة تؤدي إلى نقلٍ (move) فيمكن أن يؤدّي ذلك إلى تفريغ الكائن المنسوخ من البيانات التي أشار إليها: struct Inner { int i; }; constexpr auto NUM_INNER = 5; class Value { private: Inner *array_; // في العادة يكون لها دلالة مرجعية public: Value(): array_(new Inner[NUM_INNER]) {} // nullptr يُسمح بالنقل حتى لو أعادت ~Value() { delete[] array_; } Value(const Value &val): array_(new Inner[NUM_INNER]) { for (int i = 0; i < NUM_INNER; ++i) array_[i] = val.array_[i]; } Value &operator = (const Value &val) { for (int i = 0; i < NUM_INNER; ++i) array_[i] = val.array_[i]; return *this; } // النقل يعني لا تخصيص للذاكرة // لا يمكن إطلاق اعتراض Value(Value &&val) noexcept: array_(val.array_) { // لقد أخذنا القيمة القديمة val.array_ = nullptr; } // لا يمكن رفع اعتراضات Value &operator = (Value &&val) noexcept { // ستُدمّر قريبا val حيلة ذكية، لأن // لقد استبدلنا بياناتنا ببياناته، وسيدمّر مدمّره بياناتنا std::swap(array_, val.array_); } }; لا شك أننا نستطيع جعل مثل هذا النوع غير قابل للنسخ إن أردنا منع النسخ العميق مع السماح بنقل الكائن، انظر: struct Inner { int i; }; constexpr auto NUM_INNER = 5; class Value { private: Inner *array_; // في العادة يكون لها دلالة مرجعية public: Value(): array_(new Inner[NUM_INNER]) {} // nullptr يُسمح بالنقل حتى لو أعادت ~Value() { delete[] array_; } Value(const Value &val) = delete; Value &operator = (const Value &val) = delete; // النقل يعني لا تخصيص للذاكرة // لا يمكن إطلاق اعتراض Value(Value &&val) noexcept: array_(val.array_) { // لقد أخذنا القيمة القديمة val.array_ = nullptr; } // لا يمكن رفع اعتراضات Value &operator = (Value &&val) noexcept { // ستُدمّر قريبا val حيلة ذكية، لأن // لقد استبدلنا بياناتنا ببياناته، مدمّره سيدمّر بياناتنا std::swap(array_, val.array_); } }; نستطيع أن نطبق قاعدة الصفر (Rule of Zero) من خلال استخدام مؤشّر فريد ‎unique_ptr‎: struct Inner { int i; }; constexpr auto NUM_INNER = 5; class Value { private: unique_ptr<Inner []>array_; // نوع للنقل فقط public: Value(): array_(new Inner[NUM_INNER]) {} // لا داعي للحذف الصريح، أو حتى التصريح ~Value() = default; { delete[] array_; } // لا داعي للحذف الصريح، أو حتى التصريح Value(const Value &val) = default; Value &operator = (const Value &val) = default; // تنفيذ النقل عنصرا بعنصر Value(Value &&val) noexcept = default; // تنفيذ النقل عنصرا بعنصر Value &operator = (Value &&val) noexcept = default; }; هذا الدرس جزء من سلسلة دروس عن C++‎. ترجمة -بتصرّف- للفصل Chapter 26: References والفصل Chapter 27: Value and Reference Semantics من كتاب C++ Notes for Professionals