std::optional: القيم الاختيارية
تُستخدم القيم الاختيارية (المعروفة أيضًا باسم "أنواع الشّك") لتمثيل نوع قد يكون محتواه موجودًا أو لا، وقد قُدِّمت في C++ 17 على هيئة صنف std::optional
. فمثلًا، قد يحتوي كائن من النوع std::optional<int>
على قيمة من النوع int
، أو قد لا يحتوي على أيّ قيمة. وتُستخدَم القيم الاختيارية إمّا لتمثيل قيمة قد لا تكون موجودة، أو كنوعٍ للقيمة المعادة من دالّة قد تفشل أحيانًا في إعادة نتيجة ذات معنى.
استخدام قيمة اختيارية لتمثيل غياب القيمة
كانت المؤشّرات التي تحمل القيمة nullptr
قبل C++ 17 تمثّل عدم وجود أيّ قيمة، يعد هذا حلاً جيدًا للكائنات الكبيرة التي خُصِّصت ديناميكيًا وتُدار بواسطة مؤشّرات. لكن لا يعمل هذا الحلّ بشكل جيّد مع الأنواع الصغيرة أو الأوّلية (primitive) مثل int
، والتي نادرًا ما تُخصّص أو تُدار ديناميكيًا من قبل المؤشّرات، ويوفّر std::optional
حلاً مثاليًا لهذه المشكلة الشائعة.
في المثال أدناه، عرّفنا البنية Person
، والتي تمثل شخصًا، هذا الشخص يمكنه أن يمتلك حيوانًا أليفًا (pet)، لكنّ ذلك ليس ضروريًا. ولكي نخبر المصرّف بأنّ الحقل pet
اختياري هنا، فسنُصرّح عنه بواسطة المُغلّف std::optional
.
#include <iostream> #include <optional> #include <string> struct Animal { std::string name; }; struct Person { std::string name; std::optional < Animal > pet; }; int main() { Person person; person.name = "John"; if (person.pet) { std::cout << person.name << "'s pet's name is " << person.pet - > name << std::endl; } else { std::cout << person.name << " is alone." << std::endl; } }
القيم الاختيارية كقيمة معادة
انظر المثال التالي:
std::optional < float > divide(float a, float b) { if (b != 0. f) return a / b; return {}; }
في المثال أعلاه، سنعيد الكسر a/b
، ولكن إذا لم يكن الكسر مُعرّفا (إن كان b
يساوي 0 مثلًا)، فسَنعيد القيمة الاختيارية الفارغة. هذا مثال أكثر تعقيدًا:
template < class Range, class Pred > auto find_if( Range&& r, Pred&& p ) { using std::begin; using std::end; auto b = begin(r), e = end(r); auto r = std::find_if(b, e, p); using iterator = decltype(r); if (r == e) return std::optional < iterator > (); return std::optional < iterator > (r); } template < class Range, class T > auto find( Range&& r, T const& t ) { return find_if( std::forward<Range>(r), [&t](auto&& x){return x==t;} ); }
تبحث الدالّة find( some_range, 7 )
في الحاوية أو النطاق some_range
عن شيء يساوي العدد 7
، وتفعل الدالة find_if
عبر دالّة شرطية (predicate)، فتعيد إمّا قيمة اختيارية فارغة إذا لم يُعثر على أيّ شيء يساوي العدد 7
، أو عنصرًا اختياريا يحتوي على مُكرّر إلى العنصر في حال كان موجودًا. هذا يتيح لك القيام بما يلي:
if (find(vec, 7)) { // code }
أو حتى:
if (auto oit = find(vec, 7)) { vec.erase( * oit); }
دون الحاجة إلى استخدام مُكرّرات begin/end أو إجراء الاختبارات.
value_or
انظر المثال التالي:
void print_name( std::ostream& os, std::optional<std::string> const& name ) { std::cout "Name is: " << name.value_or("<name missing>") << '\n'; }
يعيد التابع value_or
القيمة المخزّنة في القيمة الاختيارية، أو يعيد الوسيط إذا لم يكن هناك أيّ شيء مُخزّن. يتيح لك هذا إمكانية أخذ القيمة الاختيارية (التي يمكن أن تكون فارغة) وتحديد سلوك افتراضي عندما تكون بحاجة إلى قيمة، وهكذا الطريقة، يمكن ترك تحديد "السلوك الافتراضي" إلى أن تكون هناك حاجة إليه، بدلًا من إنشاء قيمة افتراضية داخل مُحرِّك ما.
مقاربات أخرى للقيم الاختيارية
هناك العديد من الطرق الأخرى لحلّ المشكلة التي تحلها القيم الاختياريّة std::optional
، لكن لا طريقة كاملة من تلك الطرق:
القيم الاختيارية مقابل المؤشّر
في بعض الحالات، يمكننا تمثيل "اختياريّةِ كائنٍ" عبر توفير مؤشّر يشير إلى كائن موجود أو مؤشّر فارغ nullptr
للإشارة إلى فشل العمليّة. ولكنّ استخدام هذه الطريقة يقتصر على الحالات التي تكون فيها الكائنات موجودة بالفعل - بالمقابل، يمكن للقيم الاختيارية optional
أن تُستخدَم لإعادة كائنات جديدة دون الحاجة إلى تخصيص الذاكرة.
القيم الاختيارية مقابل القيم التنبيهية
من المقاربات الشائعة استخدام قيمة خاصّة للإشارة إلى أنّ القيمة لا معنى لها، وقد تكون هذه القيمة مثلًا 0 أو -1 بالنسبة للأعداد الصحيحة، أو nullptr
بالنسبة للمؤشّرات.
مثلًا، لنفترض أنّ هناك دالّة تحاول العثور على فهرس أول ظهور لحرف في سلسلة نصية، في حال كان الحرف موجودًا في السلسلة النصية، فستعيد فهرس أول ظهور له، أمّا في حال لم يكن موجودًا، فستعيد القيمة -1 للدلالة على أنّ الحرف غير موجود في السلسلة النصية. القيمة -1 تمثل القيمة التنبيهية، لأنها تنبّهنا إلى أمر ما (في هذا المثال، تنبِّهنا إلى عدم وجود الحرف المبحوث عنه في السلسلة النصية).
المشكلة في هذه المقاربة أنّها تقلل من مساحة القيم الصالحة (لا يمكنك التمييز بين القيمة 0 الصالحة والقيمة 0 التي لا معنى لها)، وليست كل الأنواع فيها خيار طبيعي للقيم التنبيهية.
القيم الاختيارية مقابل الأزواج std::pair ,>
من المقاربات الشائعة أيضًا توفير زوج يكون أحد عُنصَريه قيمةً بوليانية bool
للإشارة إلى كون القيمة ذات معنى أم لا، ويشترط هذا أن يكون نوع القيمة قابلًا للإنشاء افتراضيًا (default-constructible) في حالة حدوث خطأ، وهو أمر غير ممكن في بعض الأنواع، وقد يكون ممكنًا ولكن غير مرغوب بالنسبة لأنواع أخرى. بالمقابل، لا تحتاج القيم الاختيارية optional<T>
في حال حدوث خطأ إلى بناء أي شيء.
استخدام القيم الاختيارية لتمثيل فشل دالة
قبل C++ 17، كانت الدوالّّ تمثِّل الفشلَ عادة بإحدى الطرق التالية:
- إعادة مؤشّر فارغ.
-
على سبيل المثال، استدعاء دالّة
Delegate *App::get_delegate()
على نسخة من الصنفApp
ليس لها مُفوّض (delegate) سيعيدnullptr
. - يُعدّ هذا حلاً جيدًا للكائنات التي خُصِّصت ديناميكيًا أو الكائنات الكبيرة التي تُدار عبر المُؤشّرات، لكنه ليس حلاً جيدًا بالنسبة للكائنات الصغيرة التي عادةً ما تكون مرصوصة (stack-allocated) وتُمرّر عن طريق النسخ.
- تحجز قيمة محدّدة من النوع المُعاد للإشارة إلى الفشل.
-
على سبيل المثال، قد يؤدي استدعاء دالّة
unsigned shortest_path_distance(Vertex a, Vertex b)
على رأسين (vertices) غير مُتصلين إلى إعادة 0 للإشارة إلى هذه الحقيقة. -
يتم إقران القيمة المُعادة مع قيمة بوليانية
bool
لتحديد ما إذا كانت القيمة المعادة ذات معنى أم لا. -
على سبيل المثال، يؤدي استدعاء دالّة
std::pair<int, bool> parse(const std::string &str)
باستخدام وسيط يحتوي سلسلة نصّية لا تتضمّن عددًا صحيحًا إلى إعادة زوج يضمّ عددًا صحيحا غير محدد وقيمة بوليانية تساويfalse
.
في هذا المثال، يُعطى لزيد حيوانين أليفَين، سوسن وسوسان، ثم تُستدعى الدالّة Person::pet_with_name()
لاسترداد الكلب "وافي". ونظرًا لأنّ زيد لا يملك كلبًا باسم "وافي"، فإن الدالّة ستفشل، وستُعاد القيمة std::nullopt
بدلاً من ذلك.
#include <iostream> #include <optional> #include <string> #include <vector> struct Animal { std::string name; }; struct Person { std::string name; std::vector < Animal > pets; std::optional < Animal > pet_with_name(const std::string & name) { for (const Animal & pet: pets) { if (pet.name == name) { return pet; } } return std::nullopt; } }; int main() { Person john; zaid.name = "زيد"; Animal susan; susan.name = "سوسن"; zaid.pets.push_back(susan); Animal susanne; susanne.name = "سوسان"; zaid.pets.push_back(susanne); std::optional < Animal > dog = john.pet_with_name("وافي"); if (dog) { std::cout << "يملك زيد حيوانًا أليفًا اسمه وافي" << std::endl; } else { std::cout <<”لا يملك زيد حيوانًا أليفًا اسمه وافي" << std::endl; } }
std::function: تغليف الكائنات القابلة للاستدعاء
يوضح المثال التالي طريقة استخدام std::function
:
#include <iostream> #include <functional> std:: function < void(int, const std::string & ) > myFuncObj; void theFunc(int i, const std::string & s) { std::cout << s << ": " << i << std::endl; } int main(int argc, char * argv[]) { myFuncObj = theFunc; myFuncObj(10, "hello world"); }
استخدام std::function مع std::bind
إن احتجت إلى استدعاء دالّة مع تمرير وسائط إليها فإنّ استخدام std::function
مع std::bind
سيعطيك حلولًا فعّالة للغاية كما هو موضّح أدناه.
class A { public: std:: function < void(int, const std::string & ) > m_CbFunc = nullptr; void foo() { if (m_CbFunc) { m_CbFunc(100, "event fired"); } } }; class B { public: B() { auto aFunc = std::bind( & B::eventHandler, this, std::placeholders::_1, std::placeholders::_2); anObjA.m_CbFunc = aFunc; } void eventHandler(int i, const std::string & s) { std::cout << s << ": " << i << std::endl; } void DoSomethingOnA() { anObjA.foo(); } A anObjA; }; int main(int argc, char * argv[]) { B anObjB; anObjB.DoSomethingOnA(); }
ربط std::function مع نوع آخر قابل للاستدعاء
يوضح المثال التالي كيفية استخدام std::function
لاستدعاء دالة من نمط C، ودالة تابعة لصنف، وعامل ()operator
، ودالة لامدا. ويتم استدعاء الدالة من خلال الوسائط الصحيحة ووسائط بترتيب مختلف، وكذلك بأنواع وأعداد مختلفة.
#include <iostream> #include <functional> #include <iostream> #include <vector> using std::cout; using std::endl; using namespace std::placeholders; // دالّة بسيطة لتُستدعى double foo_fn(int x, float y, double z) { double res = x + y + z; std::cout << "foo_fn called with arguments: " << x << ", " << y << ", " << z << " result is : " << res << std::endl; return res; } // بنية مع دالة تابعة لاستدعائها struct foo_struct { // الدالة المراد استدعاؤها double foo_fn(int x, float y, double z) { double res = x + y + z; std::cout << "foo_struct::foo_fn called with arguments: " << x << ", " << y << ", " << z << " result is : " << res << std::endl; return res; } // هذا التابع له بصمة مختلفة، مع ذلك يمكن استخدامه // لاحظ أنّ ترتيب المعاملات قد تغير double foo_fn_4(int x, double z, float y, long xx) { double res = x + y + z + xx; std::cout << "foo_struct::foo_fn_4 called with arguments: " << x << ", " << z << ", " << y << ", " << xx << " result is : " << res << std::endl; return res; } // جعل الكائن بأكمله قابلا للاستدعاء operator() التحميل الزائد للعامل double operator()(int x, float y, double z) { double res = x + y + z; std::cout << "foo_struct::operator() called with arguments: " << x << ", " << y << ", " << z << " result is : " << res << std::endl; return res; } }; int main(void) { // typedefs using function_type = std:: function < double(int, float, double) > ; // foo_struct نسخة foo_struct fs; // سنخزّن هنا كل الدوالّ المربوطة std::vector < function_type > bindings; // var #1 - يمكن استخدام دالّة بسيطة وحسب function_type var1 = foo_fn; bindings.push_back(var1); // var #2 - يمكنك استخدام تابع function_type var2 = std::bind( & foo_struct::foo_fn, fs, _1, _2, _3); bindings.push_back(var2); // var #3 - يمكنك استخدام تابع مع بصمة مختلفة // لها عدد مختلف من المعاملات ومن أنواع مختلفة foo_fn_4 function_type var3 = std::bind(&foo_struct::foo_fn_4, fs, _1, _3, _2, 0l); bindings.push_back(var3); // var #4 - مُحمَّل تحميلا زائدا operator() يمكنك استخدام كائن ذي معامل function_type var4 = fs; bindings.push_back(var4); // var #5 - lambda يمكنك استخدام دالّة function_type var5 = [](int x, float y, double z) { double res = x + y + z; std::cout << "lambda called with arguments: " << x << ", " << y << ", " << z << " result is : " << res << std::endl; return res; }; bindings.push_back(var5); std::cout << "Test stored functions with arguments: x = 1, y = 2, z = 3" << std::endl; for (auto f: bindings) f(1, 2, 3); }
انظر هذا المثال الحي.
الناتج:
Test stored functions with arguments: x = 1, y = 2, z = 3 foo_fn called with arguments: 1, 2, 3 result is : 6 foo_struct::foo_fn called with arguments: 1, 2, 3 result is : 6 foo_struct::foo_fn_4 called with arguments: 1, 3, 2, 0 result is : 6 foo_struct::operator() called with arguments: 1, 2, 3 result is : 6 lambda called with arguments: 1, 2, 3 result is : 6
تخزين وسائط الدالة في صف std::tuple
تحتاج بعض البرامج إلى تخزين الوسائط إلى حين استدعاء بعض الدوالّ في المستقبل. ويوضّح هذا المثال كيفية استدعاء أيّ دالّة باستخدام الوسائط المخزّنة في صفّ std::tuple
#include <iostream> #include <functional> #include <tuple> #include <iostream> // دالّة بسيطة لاستدعائها double foo_fn(int x, float y, double z) { double res = x + y + z; std::cout << "foo_fn called. x = " << x << " y = " << y << " z = " << z << " res=" << res; return res; } // مساعِدات من أجل تسريع الصف. template < int... > struct seq {}; template < int N, int...S > struct gens: gens < N - 1, N - 1, S... > {}; template < int...S > struct gens < 0, S... > { typedef seq < S... > type; }; //استدعاء المساعِدات template < typename FN, typename P, int...S > double call_fn_internal(const FN & fn, const P & params, const seq < S... > ) { return fn(std::get < S > (params)...); } // std::tuple استدعاء الدالّة مع الوسائط المُخزنة في template < typename Ret, typename...Args > Ret call_fn(const std:: function < Ret(Args...) > & fn, const std::tuple < Args... > & params) { return call_fn_internal(fn, params, typename gens < sizeof...(Args) > ::type()); } int main(void) { // الوسائط std::tuple < int, float, double > t = std::make_tuple(1, 5, 10); // الدالّة المراد استدعاؤها std:: function < double(int, float, double) > fn = foo_fn; // استدعاء الدالّة مع تمرير الوسائط المُخزّنة إليها call_fn(fn, t); }
هذا مثال حي
الناتج:
foo_fn called. x = 1 y = 5 z = 10 res=16
استخدام std::function مع تعابير لامدا والصنف std::bind
#include <iostream> #include <functional> using std::placeholders::_1; // std::bind ستُستخدَم في مثال int stdf_foobar(int x, std:: function < int(int) > moo) { return x + moo(x); // std::function moo استدعاء } int foo (int x) { return 2+x; } int foo_2 (int x, int y) { return 9*x + y; } int main() { int a = 2; /* مؤشّرات الدوالّ */ std::cout << stdf_foobar(a, & foo) << std::endl; // 6 ( 2 + (2+2) ) // stdf_foobar(2, foo) يمكن أن تكون أيضا /* Lambda تعابير */ /* std::function في كائن lambda يمكن تخزين دالّة مُغلقة من تعبير */ std::cout << stdf_foobar(a, [capture_value](int param) -> int { return 7 + capture_value * param; }) << std::endl; // result: 15 == value + (7 * capture_value * value) == 2 + (7 + 3 * 2) /* std::bind تعابير */ /* std::bind يمكن تمرير نتيجة التعبير * مثلا عبر ربط المعاملات باستدعاء مؤشّر دالّة */ int b = stdf_foobar(a, std::bind(foo_2, _1, 3)); std::cout << b << std::endl; // b == 23 == 2 + ( 9*2 + 3 ) int c = stdf_foobar(a, std::bind(foo_2, 5, _1)); std::cout << c << std::endl; // c == 49 == 2 + ( 9*5 + 2 ) return 0; }
الحمل الزائد للدوال (function overhead)
يمكن أن تتسبب std::function
في حِمل زائد كبير لأنّ std::function
لها دلالات قيمية (value semantics)، فسيكون من اللازم أن تنسخ أو تنقل ما استُدعِي إليها. ولكن بما أنها تستطيع أخذ كائن قابل للاستدعاء من أيّ نوع، فسيتعينّ عليها في كثير من الأحيان أن تخصّص ذاكرة ديناميكية بذلك.
تحتوي بعض تطبيقات function
على ما يسمى "تحسين الكائنات الصغيرة" (small object optimization)، وفيها تُخزّن الأنواع الصغيرة مثل مؤشّرات الدوالّ أو مؤشّرات الأعضاء أو الكائنات الدالّية (Functors) ذات الحالة الصغيرة، مباشرة في كائن الدالّة function
. لكنّ هذا لن يعمل إلّا إن كان النوع قابلًا للإنشاء النقلي عند الاعتراض (noexcept move constructible). أيضًا، لا يتطلب معيار C++ أن توفّر جميع التطبيقات مثل هذا التحسين. إليك المثال التالي:
// ملف الترويسة using MyPredicate = std::function<bool(const MyValue &, const MyValue &)>; void SortMyContainer(MyContainer &C, const MyPredicate &pred); // الملف المصدري void SortMyContainer(MyContainer &C, const MyPredicate &pred) { std::sort(C.begin(), C.end(), pred); }
يُعدّ معامل القالب هو الحلّ المفضّل لـ SortMyContainer
، ولكن إن افترضنا أنّ هذا غير ممكن أو غير مرغوب فيه لسبب من الأسباب، فلن تحتاج SortMyContainer
إلى تخزين pred
إلا في إطار استدعائها. ومع ذلك، فقد تُخصّص لـ pred
ذاكرةً إن كان الكائن الدّالِّي (functor) المُعطى ذا حجم غير معروف.
تخصّص function
الذاكرة لأنّها تحتاج إلى شيءٍ لتنسخ أو تنقل إليه، كما أنها تأخذ ملكية الكائن القابل للاستدعاء الذي مُرِّر إليها، لكنّ SortMyContainer
لا تحتاج إلى امتلاك المُستدعِي ولكنها تحتاج إلى مرجع إليه وحسب، لذا فلا داعي لاستخدام function
هنا. كذلك لا يوجد أي نوع دالّة قياسي يشير حصرًا إلى كائنٍ قابل للاستدعاء، لذلك سيكون عليك أن تنتظر إيجاد حلّ لهذا، أو يمكنك التعايش مع الحمل الزائد.
أيضّا، لا تملك function
أيّ وسيلة فعّالة للتحكم في الموضع الذي تأتي منه تخصيصات الذاكرة الخاصّة بالكائن. صحيح أنّ لها مُنشئات تأخذ كائن تخصيص allocator
، ولكنّ العديد من التقديمات لا تقدّمها بالشكل الصحيح … أو لا تقدما بتاتًا.
الإصدار ≥ C++ 17
لم يعُد المنشئ function
الذي يأخذ كائن تخصيصٍ allocator
جزءًا من النوع. لذلك لا توجد أيّ طريقة لإدارة التخصيص. واعلم أن استدعاء function
أبطأ من استدعاء المحتويات مباشرةً. كذلك يجب أن يكون الاستدعاء عبر function
غير مباشر لأنّ نُسَخَ function
يمكن أن تحتوي كائنًا قابلًا للاستدعاء، ويكافئ الحِمل الزائد الناتج عن استدعاء function
الحِمل الزائد الناتج عن استدعاء دالّة وهمية.
std::forward_list: القوائم الإدراجية
النوع std::forward_list
هو حاوية تدعم الإدراج السريع للعناصر من أي مكان فيها وكذلك إزالة تلك العناصر، لكنها لا تدعم الوصول العشوائي السريع.
يُنفَّذ النوع std::forward_list
كقائمة مرتبطة أحادية (singly-linked list)، وليس لها عمومًا أيّ حِمل زائد (overhead) مقارنة بتطبيقها في C. وتوفّر هذه الحاوية، مقارنة بـ std::list
، مساحة تخزين أكثر كفاءة عند غياب الحاجة للتكرار ثنائي الاتجاه (bidirectional iteration). انظر المثال التالي:
#include <forward_list> #include <string> #include <iostream> template < typename T > std::ostream & operator << (std::ostream & s, const std::forward_list < T > & v) { s.put('['); char comma[3] = { '\0', ' ', '\0' }; for (const auto & e: v) { s << comma << e; comma[0] = ','; } return s << ']'; } int main() { // c++11 صياغة قائمة المهييء في std::forward_list < std::string > words1 { "the", "frogurt", "is", "also", "cursed" }; std::cout << "words1: " << words1 << '\n'; // words2 == words1 std::forward_list < std::string > words2(words1.begin(), words1.end()); std::cout << "words2: " << words2 << '\n'; // words3 == words1 std::forward_list < std::string > words3(words1); std::cout << "words3: " << words3 << '\n'; // words4 is {"Mo", "Mo", "Mo", "Mo", "Mo"} std::forward_list < std::string > words4(5, "Mo"); std::cout << "words4: " << words4 << '\n'; }
الناتج:
words1: [the, frogurt, is, also, cursed] words2: [the, frogurt, is, also, cursed] words3: [the, frogurt, is, also, cursed] words4: [Mo, Mo, Mo, Mo, Mo]
التوابع
إليك قائمة التوابع الخاصة بالنوع std::forward_list:
اسم التابع | التعريف |
---|---|
operator=
|
يعيّن قيمًا إلى الحاوية |
assign
|
يعيّن قيمًا إلى الحاوية |
get_allocator
|
يعيد المخصِّص المرتبط به (associated allocator) |
front
|
يصل إلى العنصر الأول |
before_begin
|
يعيد مكررًا إلى العنصر قبل البداية |
cbefore_begin
|
يعيد مكررًا ثابت إلى العنصر قبل البداية |
begin
|
يعيد مكررًا إلى البداية |
cbegin
|
يعيد مكررًا ثابت إلى البداية |
end
|
يعيد مكررًا إلى النهاية |
cend
|
يعيد مكررًا إلى النهاية |
empty
|
يتحقق إن كانت الحاوية فارغة |
max_size
|
يعيد الحد الأقصى للعدد الممكن من العناصر |
clear
|
يمسح المحتويات |
insert_after
|
يُدرِج عناصرًا بعد عنصر ما |
emplace_after
|
ينشئ عنصرًا مكان عنصر آخر |
erase_after
|
يمحو عنصرًا موجودا بعد عنصر ما |
push_front
|
يدرج عنصرًا في البداية |
emplace_front
|
ينشئ عنصرًا في البداية |
pop_front
|
يزيل العنصر الأول |
resize
|
يغير عدد العناصر المخزنة |
swap
|
يبدل المحتويات |
merge
|
يدمج قائمتين مرتبتين |
splice_after
|
ينقل عناصر من قائمة أمامية أخرى |
remove
|
يزيل العناصر التي تحقق شرطا محددا |
remove_if
|
يزيل العناصر التي تحقق شرطا محددا |
reverse
|
يعكس ترتيب العناصر |
unique
|
يزيل العناصر المتساوية المتتالية |
sort
|
يرتب العناصر |
std::pair: الأزواج
عوامل الموازنة
معامِلات هذه العوامل هي lhs
و rhs
:
-
operator==
- يتحقّق هذا العامل من أنّ عناصر كلا الزوجينlhs
وrhs
متساويان، وتكون القيمة المُعادة هيtrue
إن كانlhs.first == rhs.first
وlhs.second == rhs.second
، وإلّا فستكونfalse
.
std::pair < int, int > p1 = std::make_pair(1, 2); std::pair < int, int > p2 = std::make_pair(2, 2); if (p1 == p2) std::cout << "equals"; else std::cout << "not equal"; // ستُظهر التعليمة هذا، لأن الزوجين غير متماثلين
-
operator!=
- يتحقّق هذا العامل ممّا إذا كان أيّ من عناصر الزوجيينlhs
وrhs
غير متساويين، وتكون القيمة المُعادة هيtrue
إن كانlhs.first != rhs.first
أوlhs.second != rhs.second
، وإلا فستُعاد القيمةfalse
. -
operator<
- إذا كانlhs.first<rhs.first
، فسيُعيدtrue
وإن كانrhs.first<lhs.first
فسيُعيدfalse
. وكذلك إن كانlhs.second<rhs.second
فسيُعيدtrue
، أما خلاف ذلك سيُعيدfalse
. -
operator<=
- يُعيد!(rhs<lhs)
-
operator>
- يعيدrhs<lhs
-
operator>=
- يعيد!(lhs<rhs)
هذا مثال آخر يستخدم حاويات أزواج، ويستخدم العامل operator<
لترتيب الحاوية.
#include <iostream> #include <utility> #include <vector> #include <algorithm> #include <string> int main() { std::vector<std::pair<int, std::string>> v = { {2, "baz"}, {2, "bar"}, {1, "foo"} }; std::sort(v.begin(), v.end()); for (const auto & p: v) { std::cout << "(" << p.first << "," << p.second << ") "; // (1,foo) (2,bar) (2,baz) :الناتج } }
إنشاء زوج والوصول إلى عناصره
تتيح لنا الأزواج أن نعامل كائِنين كما لو كانا كائنًا واحدًا، ويمكن إنشاء الأزواج بسهولة بمساعدة دالّة القالب std::make_pair
. وهناك طريقة أخرى، وهي إنشاء زوج وتعيين عنصُريه (first
و second
) لاحقًا.
#include <iostream> #include <utility> int main() { std::pair < int, int > p = std::make_pair(1, 2); // إنشاء الزوج std::cout << p.first << " " << p.second << std::endl; // الوصول إلى العناصر // يمكننا أيضا إنشاء الزوج وتعيين عناصره لاحقا std::pair < int, int > p1; p1.first = 3; p1.second = 4; std::cout << p1.first << " " << p1.second << std::endl; // يمكننا أيضا إنشاء زوج باستخدام منشئ std::pair < int, int > p2 = std::pair < int, int > (5, 6); std::cout << p2.first << " " << p2.second << std::endl; return 0; }
std::atomics: الأنواع الذرية
كل استنساخ (instantiation) وتخصيص للقالب std::atomic
يعرّف نوعًا ذريًا (atomic type)، فإن قامت أحد الخيوط (threads) بالكتابة في كائن ذرّي أثناء قراءة مسلك آخر منه، فإنّ السّلوك سيكون مُعرّفًا بشكل جيد، ولن يحدث أيّ مشكل.
إضافة إلى ذلك، قد يؤدّي الدخول إلى الكائنات الذرية إلى تهيئة تزامن بين الخيوط ويطلب دخولًا غير ذرّي للذاكرة (non-atomic memory accesses) كما هو مُعرَّف من قِبل std::memory_order
. يمكن استنساخ std::atomic
مع أيّ نوع قابل للنسخ (TriviallyCopyable type T. std::atomic
)، لكن std::atomic
ليست قابلة للنسخ أو النقل.
توفّر المكتبة القياسية تخصيصات للقالب std::atomic
للأنواع التالية:
1) تعريف تخصيص كامل للنوع البولياني bool
، وعُرِّفت قيمة التعريف النوعي (typedef) الخاصة به بحيث يُتعامل معه على أنّه نوع ذرّي std::atomic<T>
غير مُخصَّص، فيما عدا أنّه ستكون له مخطط (layout) قياسي، ومنشئ افتراضي أولي، ومدمّرات واضحة، كما سيدعم صيغة التهيئة الإجمالية (aggregate initialization syntax):
Typedef | التخصيص |
---|---|
std::atomic_bool
|
std::atomic<bool>
|
2) تخصيصات كاملة وتعريفات نوعية typedefs للأنواع العددية الصحيحة، كما يلي:
Typedef | التخصيص |
---|---|
std::atomic_char
|
std::atomic<char >
|
std::atomic_char
|
std::atomic<char>
|
std::atomic_schar
|
std::atomic<signed char>
|
std::atomic_uchar
|
std::atomic<unsigned char>
|
std::atomic_short
|
std::atomic<short>
|
std::atomic_ushort
|
std::atomic<unsigned short>
|
std::atomic_int
|
std::atomic<int>
|
std::atomic_uint
|
std::atomic<unsigned int>
|
std::atomic_long
|
std::atomic<long>
|
std::atomic_ulong
|
std::atomic<unsigned long>
|
std::atomic_llong
|
std::atomic<long long>
|
std::atomic_ullong
|
std::atomic<unsigned long long>
|
std::atomic_char16_t
|
std::atomic<char16_t>
|
std::atomic_char32_t
|
std::atomic<char32_t>
|
std::atomic_wchar_t
|
std::atomic<wchar_t>
|
std::atomic_int8_t
|
std::atomic<std::int8_t>
|
std::atomic_uint8_t
|
std::atomic<std::uint8_t>
|
std::atomic_int16_t
|
std::atomic<std::int16_t>
|
std::atomic_uint16_t
|
std::atomic<std::uint16_t>
|
std::atomic_int32_t
|
std::atomic<std::int32_t>
|
std::atomic_uint32_t
|
std::atomic<std::uint32_t>
|
std::atomic_int64_t
|
std::atomic<std::int64_t>
|
std::atomic_uint64_t
|
std::atomic<std::uint64_t>
|
std::atomic_int_least8_t
|
std::atomic<std::int_least8_t>
|
std::atomic_uint_least8_t
|
std::atomic<std::uint_least8_t>
|
std::atomic_int_least16_t
|
std::atomic<std::int_least16_t>
|
std::atomic_uint_least16_t
|
std::atomic<std::uint_least16_t>
|
std::atomic_int_least32_t
|
std::atomic<std::int_least32_t>
|
std::atomic_uint_least32_t
|
std::atomic<std::uint_least32_t>
|
std::atomic_int_least64_t
|
std::atomic<std::int_least64_t>
|
std::atomic_uint_least64_t
|
std::atomic<std::uint_least64_t>
|
std::atomic_int_fast8_t
|
std::atomic<std::int_fast8_t>
|
std::atomic_uint_fast8_t
|
std::atomic<std::uint_fast8_t>
|
std::atomic_int_fast16_t
|
std::atomic<std::int_fast16_t>
|
std::atomic_uint_fast16_t
|
std::atomic<std::uint_fast16_t>
|
std::atomic_int_fast32_t
|
std::atomic<std::int_fast32_t>
|
std::atomic_uint_fast32_t
|
std::atomic<std::uint_fast32_t>
|
std::atomic_int_fast64_t
|
std::atomic<std::int_fast64_t>
|
std::atomic_uint_fast64_t
|
std::atomic<std::uint_fast64_t>
|
std::atomic_intptr_t
|
std::atomic<std::intptr_t>
|
std::atomic_uintptr_t
|
std::atomic<std::uintptr_t>
|
std::atomic_size_t
|
std::atomic<std::size_t>
|
std::atomic_ptrdiff_t
|
std::atomic<std::ptrdiff_t>
|
std::atomic_intmax_t
|
std::atomic<std::intmax_t>
|
std::atomic_uintmax_t
|
std::atomic<std::uintmax_t>
|
هذا مثال بسيط على استخدام std::atomic_int
:
#include <iostream> // std::cout #include <atomic> // std::atomic, std::memory_order_relaxed #include <thread> // std::thread std::atomic_int foo(0); void set_foo(int x) { foo.store(x, std::memory_order_relaxed); // تعيين القيمة الذرية } void print_foo() { int x; do { x = foo.load(std::memory_order_relaxed); // الحصول على القيمة الذرّية } while (x == 0); std::cout << "foo: " << x << '\n'; } int main() { std::thread first(print_foo); std::thread second(set_foo, 10); first.join(); //second.join(); return 0; } // foo: 10
std::variant: المتغايرات
إنشاء مؤشرات للتوابع الزائفة (Create pseudo-method pointers)
يمكنك استخدام كائن متغَاير (Variant) للشطب الخفيف للنوع (light weight type erasure). انظر المثال التالي:
template < class F > struct pseudo_method { F f; // C++17 السماح باستنتاج نوع الصنف في pseudo_method(F && fin): f(std::move(fin)) {} // عامل بحث كوينج->* لا بأس بما أنه تابع زائف template < class Variant > // متغاير LHS للتحقق من أنّ SFINAE إضافة اختبار friend decltype(auto) operator->*( Variant&& var, pseudo_method const& method ) { // تعيد تعبير لامدا يعيد توجيه استدعاء دالة ما var->*method // مما يجعلها تبدو كأنها تتصرف كمؤشّر تابع return [&](auto&&...args)->decltype(auto) { // للحصول على نوع المتغاير visit استخدم return std::visit( [&](auto&& self)->decltype(auto) { return method.f( decltype(self)(self), decltype(args)(args)... ); }, std::forward < Var > (var) ); }; } };
يؤدي هذا إلى إنشاء نوع يزيد تحميل العامل operator->*
بمتغاير Variant
على الجانب الأيسر. في المثال التالي، سنستخدم استنتاج نوع الصنف الخاص بـ C++ 17 من أجل إيجاد وسيط القالب لـ print
، يجب أن يكون self
هو أول وسيط يأخذه تابع لامدا الزائف، ثم يأخذ بقية الوسائط ثم يستدعي الدالة.
pseudo_method print = [](auto&& self, auto&&...args)->decltype(auto) { return decltype(self)(self).print( decltype(args)(args)... ); };
والآن إن كان لدينا نوعان لكل منهما تابع print
:
struct A { void print(std::ostream & os) const { os << "A"; } }; struct B { void print(std::ostream & os) const { os << "B"; } };
لاحظ أنّهما نوعان غير مترابطان، نستطيع تنفيذ ما يلي:
std::variant<A,B> var = A{}; (var->*print)(std::cout);
سيتم إرسال الاستدعاء مباشرة إلى A::print(std::cout)
، لكن لو هيأنا var
باستخدام B{}
، فسيتم إرساله إلى B::print(std::cout)
.
وإذا أنشأنا نوعًا جديدًا C …
struct C {};
… فسيكون لدينا:
std::variant<A,B,C> var = A{}; (var->*print)(std::cout);
ستفشل عملية التصريف، لأنه لا يوجد تابع C.print(std::cout)
.
سوف يسمح توسيع الشيفرة أعلاه باكتشاف واستخدام دوال print
، ربّما باستخدام if constexpr
ضمن التابع الزائف print
.
هذا مثال حي يستخدم boost::variant
بدلاً من std::variant
.
الاستخدامات الرئيسية للمتغايرات
تنشي الشيفرة التالية متغايرًا (اتحادًا موسومًا tagged union) يمكنه تخزين إمّا عدد صحيح (int
) وإمّا سلسلة نصيةstring
.
std::variant< int, std::string > var;
يمكننا تخزين أحد هذين النوعين في الكائن المتغاير:
var = "hello"s;
ويمكننا الوصول إلى مُحتوياته عبر std::visit
:
// "hello\n" طباعة visit( [](auto&& e) { std::cout << e << '\n'; }, var);
عن طريق تمرير دالّّة لامدا متعددة الأشكال أو كائن دالّّة مشابه، وإذا كنا متأكّدين من النوع، فيمكننا الحصول عليه على النحو التالي:
auto str = std::get<std::string>(var);
ولكن هذا سوف يرفع اعتراضًا إن أخطأنا تقدير النوع.
auto* str = std::get_if<std::string>(&var);
إن أخطأت التقدير فستُعاد القيمة nullptr
.
تضمن المُتغايرات عدم تخصيص ذاكرة ديناميكية، باستثناء تلك التي تُخصَّص من قبل أنواعها المُضمّنة، ولا يُخزَّن إلا نوع واحد فقط في المتغاير، ويرافقها في حالات نادرة رفع اعتراضات أثناء الإسناد وغياب إمكانية آمنة للتراجع يمكن أن يصبح المتغاير فارغًا.
كما تتيح لك المُتغايرات تخزين قيم من عدّة أنواع في متغيّر واحد بأمان وكفاءة. فهي أساسًا اتحاداتunion
ذكية وآمنة.
إنشاء متغاير
هذا لا يشمل المُخصِّصات (allocators).
struct A {}; struct B { B()=default; B(B const&)=default; B(int){}; }; struct C { C()=delete; C(int) {}; C(C const&)=default; }; struct D { D( std::initializer_list<int> ) {}; D(D const&)=default; D()=default; }; std::variant < A, B > var_ab0; // A() يحتوي std::variant < A, B > var_ab1 = 7; // a B(7) يحتوي std::variant < A, B > var_ab2 = var_ab1; // a B(7) يحتوي std::variant < A, B, C > var_abc0 { std::in_place_type < C > , 7 }; // a C(7) يحتوي std::variant < C > var_c0; // C لأجل ctor غير قانوني، لا توجد قيمة افتراضية لـ std::variant<A,D> var_ad0( std::in_place_type<D>, {1,3,3,4} ); // D{1,3,3,4} يحتوي std::variant < A, D > var_ad1(std::in_place_index < 0 > ); // A{} يحتوي std::variant<A,D> var_ad2( std::in_place_index<1>, {1,3,3,4} ); // D{1,3,3,4} يحتوي
std::iomanip و std::any
std::setprecision
عند استخدام std::setprecision
في التعبير out << setprecision(n)
أو in >> setprecision(n)
، فإنّها تضبط معامل الدقة (precision parameter) الخاصّ بمجرى الخرج أو الدخل عند القيمة n
.
معامل هذه الدالّة يكون عددًا صحيحًا، ويمثل قيمة الدقة الجديدة. انظر المثال التالي:
#include <iostream> #include <iomanip> #include <cmath> #include <limits> int main() { const long double pi = std::acos(-1.L); std::cout << "default precision (6): " << pi << '\n' << "std::precision(10): " << std::setprecision(10) << pi << '\n' << "max precision: " << std::setprecision(std::numeric_limits < long double > ::digits10 + 1) << pi << '\n'; } //Output // 3.14159 : في الدقة الافتراضية (6) يكون الناتج // std::precision(10): 3.141592654 // 3.141592653589793239 :الدقة القصوى
std::setfill
عند استخدام std::setfill
في تعبير out << setfill(c)
، فإنّها تُضبط محرف الملء (fill character) الخاصّ بمجرى الخرج عند القيمة c
.
ملاحظة: يمكن الحصول على محرف الملء الحالي عبر الدالّة std::ostream::fill
. مثال:
#include <iostream> #include <iomanip> int main() { std::cout << "default fill: " << std::setw(10) << 42 << '\n' << "setfill('*'): " << std::setfill('*') << std::setw(10) << 42 << '\n'; } // 42 :الافتراضي // setfill('*'): ********42
std::setiosflags
عند استخدام std::setiosflags
في التعبير out << setiosflags(mask)
أو in >> setiosflags(mask)
، فإنّها تضبط كل رايات التنسيق (format flags) الخاصّة بمجرى الخرج أو الدخل كما هو محدّد من قِبل القناع (mask).
هذه قائمة بكل رايات std::ios_base::fmtflags
:
-
dec
: استخدام أساس عشري لدخل وخرج (I / O) الأعداد الصحيحة -
oct
: استخدام أساس ثماني (octal base) لدخل وخرج الأعداد الصحيحة -
hex
: استخدام أساس ست عشري (hexadecimal base) لدخل وخرج الأعداد الصحيحة -
basefield
-dec|oct|hex|0
: مفيدة لتقنيع (masking) العمليات -
left
: التعديل الأيسر (إضافة محارف الملْء إلى اليمين) -
right
: التعديل الأيمن (إضافة محارف الملء إلى اليسار) -
internal
: التعديل الداخلي (إضافة محارف الملء إلى نقطة معيّنة في الداخل) -
adjustfield
-left|right|internal
: مفيدة لتقنيع العمليات. -
scientific
: توليد الأنواع العددية العشرية باستخدام الصيغة العلمية، أو الصيغة الثمانية (hex notation) في حال اقترنت بـ fixed. -
fixed
: توليد الأنواع العددية العشرية باستخدام الصيغة الثابتة (fixed notation)، أو الصيغة الثمانية (hex notation) في حال اقترنت بـ scientific. *floatfield
-scientific|fixed|(scientific|fixed)|0
: مفيدة لتقنيع لعمليات -
boolalpha
: إدراج واستخراج نوع منطقي وفق تنسيق أبجدي رقمي -
showbase
: إنشاء سابقة (prefix) تشير إلى الأساس الرقمي لخرج الأعداد الصحيحة، وتتطلّب إشارة إلى العُملة في حال الدخل والخرج الماليّ. -
showpoint
: إنشاء محرفَ الفاصلة العشرية (decimal-point character) دون قيد أو شرط لخرج الأعداد العشرية -
showpos
: توليد المحرف+
للأعداد غير السالبة -
skipws
: تخطي المسافات البيضاء الموجودة في البداية قبل عمليات الإدخال -
unitbuf
: نقل (flush) الناتج بعد كل عملية خرج -
uppercase
: استبدال بعض الأحرف الصغيرة بالأحرف الكبيرة المقابلة لها في بعض مخرجات عمليات الإخراج.
أمثلة على المعدِّلات:
#include <iostream> #include <string> #include<iomanip> int main() { int l_iTemp = 47; std::cout << std::resetiosflags(std::ios_base::basefield); std::cout << std::setiosflags(std::ios_base::oct) << l_iTemp << std::endl; // ==> 57 std::cout << std::resetiosflags(std::ios_base::basefield); std::cout << std::setiosflags(std::ios_base::hex) << l_iTemp << std::endl; // ==> 2f std::cout << std::setiosflags(std::ios_base::uppercase) << l_iTemp << std::endl; // ==> 2F std::cout << std::setfill('0') << std::setw(12); std::cout << std::resetiosflags(std::ios_base::uppercase); std::cout << std::setiosflags(std::ios_base::right) << l_iTemp << std::endl; // ==> 00000000002f std::cout << std::resetiosflags(std::ios_base::basefield | std::ios_base::adjustfield); std::cout << std::setfill('.') << std::setw(10); std::cout << std::setiosflags(std::ios_base::left) << l_iTemp << std::endl; // ==> 47........ std::cout << std::resetiosflags(std::ios_base::adjustfield) << std::setfill('#'); std::cout << std::setiosflags(std::ios_base::internal | std::ios_base::showpos); std::cout << std::setw(10) << l_iTemp << std::endl; // ==> +#######47 double l_dTemp = -1.2; double pi = 3.14159265359; std::cout << pi << " " << l_dTemp << std::endl; // ==> +3.14159 -1.2 std::cout << std::setiosflags(std::ios_base::showpoint) << l_dTemp << std::endl; // ==> -1.20000 std::cout << setiosflags(std::ios_base::scientific) << pi << std::endl; // ==> +3.141593e+00 std::cout << std::resetiosflags(std::ios_base::floatfield); std::cout << setiosflags(std::ios_base::fixed) << pi << std::endl; // ==> +3.141593 bool b = true; std::cout << std::setiosflags(std::ios_base::unitbuf | std::ios_base::boolalpha) << b; // ==> true return 0; }
std::setw
انظر المثال التالي حيث يطبع السطر الثاني val
في أقصى يسار شاشة الخرج، بينما يطبعها السطر الثالث في حقل إخراج طوله 10 بدءًا من النهاية اليمنى للحقل:
int val = 10; std::cout << val << std::endl; std::cout << std::setw(10) << val << std::endl;
يكون الخرج ما يلي:
10 10 1234567890
(السطر الأخير موجود للمساعدة على رؤية مواضع الأحرف).
عندما نحتاج إلى أن يكون الخرج مُنسّقًا بتنسيق معيّن، فقد نحتاج إلى ضبط عرض الحقل، ويمكن القيام بذلك باستخدام std::setw
و std::iomanip
. توضّح الشيفرة التالية صيغة std::setw
:
std::setw(int n)
يمثّل n
في هذا المثال طول حقل الخرج الذي سيُعيَّن.
std::any
يوضح المثال التالي كيفية استخدام std::any
:
std::any an_object{ std::string("hello world") }; if (an_object.has_value()) { std::cout << std::any_cast<std::string>(an_object) << '\n'; } try { std::any_cast<int>(an_object); } catch(std::bad_any_cast&) { std::cout << "Wrong type\n"; } std::any_cast<std::string&>(an_object) = "42"; std::cout << std::any_cast<std::string>(an_object) << '\n';
المخرجات الناتجة:
hello world Wrong type 42
std::set و std::multiset: المجموعات والمجموعات المتعددة
تمثّل المجموعات "set
" نوعًا من الحاويات عناصرها مُرتّبة وغير مكرّرة، أمّا المجموعات المتعدّدة multiset
، فتشبه المجموعات العادية، لكن العناصر المتعددة تكون لها نفس القيمة.
تغيير الترتيب الافتراضي لمجموعة ما
لدى الصّنفين set
و multiset
توابع مقارنة افتراضية، ولكن قد تحتاج أحيانًا في بعض الحالات إلى زيادة تحميلها. فمثلًا، لنفترض أنّنا نخزّن سلاسل نصية في مجموعة ما، ونحن نعلم أن تلك السلاسل تحتوي على قيم رقمية فقط. يكون الترتيب الافتراضي قائمًا على المقارنة الأبجدية للسلاسل النصّية، وعليه فلن يتطابق الترتيب مع الترتيب الرقمي. وإن أردت ترتيبها ترتيبًا عدديًا فستحتاج إلى كائن دالّي (functor) لزيادة تحميل تابع الموازنة:
#include <iostream> #include <set> #include <stdlib.h> struct custom_compare final { bool operator()(const std::string & left, const std::string & right) const { int nLeft = atoi(left.c_str()); int nRight = atoi(right.c_str()); return nLeft < nRight; } }; int main() { std::set<std::string> sut({"1", "2", "5", "23", "6", "290"}); std::cout << "### Default sort on std::set<std::string> :" << std::endl; for (auto && data: sut) std::cout << data << std::endl; std::set<std::string, custom_compare> sut_custom({"1", "2", "5", "23", "6", "290"}, custom_compare {}); std::cout << std::endl << "### Custom sort on set :" << std::endl; for (auto && data: sut_custom) std::cout << data << std::endl; auto compare_via_lambda = [](auto &&lhs, auto &&rhs){ return lhs > rhs; }; using set_via_lambda = std::set<std::string, decltype(compare_via_lambda)>; set_via_lambda sut_reverse_via_lambda({"1", "2", "5", "23", "6", "290"}, compare_via_lambda); std::cout << std::endl << "### Lambda sort on set :" << std::endl; for (auto && data: sut_reverse_via_lambda) std::cout << data << std::endl; return 0; }
يكون الخرج ما يلي:
### Default sort on std::set<std::string> : 1 2 23 290 5 6 ### Custom sort on set : 1 2 5 6 23 290 ### Lambda sort on set : 6 5 290 23 2 1
في المثال أعلاه، يمكن استخدام ثلاث طرق مختلفة لإضافة عمليات مقارنة إلى المجموعات "std::set
"، ولكلّ منها فوائدها.
الترتيب الافتراضي
يستخدم الترتيب الافتراضي عامل المقارنة الخاصّ بالمفتاح (الوسيط الأول للقالب)، وغالبًا ما يكون المفتاح إعدادًا افتراضيًا مناسبًا للدالّة std::less<T>
. وستستخدم هذه الدالة العامل operator<
الخاص بالكائن ما لم تكن قد خُصِّصت، هذا مفيد خاصّة عندما تحاول شيفرة أخرى استخدام ترتيب معيّن، إذ يجعل الشيفرة متناسقة.
ستؤدي كتابة الشيفرة بهذه الطريقة إلى تسهيل تحديثها عندما تكون تغييرات المفتاح جزءًا من واجهة برمجية (API)، فمثلًا إن كان لدينا صنف يحتوي على عضوين، وسيتغيّر إلى صنف يحتوي 3 أعضاء، فستُحدَّث جميع النُّسخ عبر تحديث operator<
الخاص بالصنف. وكما تتوقع، فإن استخدام التصنيف الافتراضي كخيار افتراضي منطقي ومقبول.
الترتيب المُخصّص
يمكن إضافة ترتيب مُخصّص عبر كائن له عامل مقارنة عندما لا تكون المقارنة الافتراضية مناسبة، كما في المثال أعلاه إذ تشير السلاسل النصيّة إلى أعداد صحيحة.
يمكن أيضًا استخدام الترتيب المُخصّص في حال كنت ترغب في مقارنة المؤشّرات (الذكية) استنادًا إلى الكائن الذي تشير إليه، أو في حال كنت تحتاج إلى قيود خاصّة في عملية المقارنة، كمقارنة الأزواج std::pair
بقيمة العنصر الأول first
فقط.
يجب أن تحرص على أن يكون الترتيب مستقرًّا (stable sorting) عند إنشاء عامل مقارنة، أي أنّ نتيجة عامل المقارنة بعد الإدراج يجب ألا تتغيّر، وإلا فهذا يعني أنّ السلوك غير محدّد. لذلك احرص على ألّا يستخدم عامل المقارنة إلّا البيانات الثابتة (الأعضاء، الدوالّ الثابتة، …).
وستصادف غالبًا -كما في المثال أعلاه- أصنافًا بدون عوامل مقارنة، وينتج عن هذا منشئاتٌ افتراضية ومنشئاتُ نسخ (copy constructors)، ويسمح لك المُنشئ الافتراضي بحذف النسخة في وقت الإنشاء، كما أنّ منشئ النسخ ضروريّ لأنّ المجموعة تأخذ نسخة من مُعامل المقارنة.
الترتيب عبر تعابير لامدا
تعابير لامدا هي طريقة مختصرة لكتابة الدوالّ، وتتيح لنا كتابة امل المقارنة في سطور قليلة مما يسهل قراءة الشيفرة الكلّية.
ما يعيب استخدام تعابير لامدا هو أنّه سيكون لكلّ واحد منها نوع محدّد في وقت التصريف، لذلك سيكون التعبير decltype(lambda)
مختلفًا في كل تُصرَّف نفس وحدة التصريف (ملف cpp) عند وجود أكثر من وحدة تصريف تكون مُدرجة في ملفات الترويسة، ولهذا يوصى باستخدام كائنات الدوالّ كعوامل مقارنة عند استخدامها داخل ملفات الترويسة.
سترى هذا النوع من الإنشاء غالبًا عند استخدام مجموعة "std::set
" ضمن النطاق المحلي للدالّة. في حين يُفضَّل استعمال كائن دالّة عند استخدامه كوسيط لدالّة أو كعضو في صنف.
خيارات الترتيب الأخرى
نظرًا لأنّ عامل المقارنة الخاصّ بالمجموعات std::set
عبارة عن وسيط قالب، فيمكن استخدام جميع الكائنات القابلة للاستدعاء كعوامل مقارنة، وما الأمثلة أعلاه إلا حالات خاصّة وحسب، ولا توجد قيود على هذه الكائنات القابلة للاستدعاء إلا ما يلي:
- يجب أن تكون نسخة قابلة للإنشاء النّسخي (copy constructable)
- ويجب أن تكون قابلة للاستدعاء مع وسيطين من نوع المفتاح نفسه (التحويلات الضمنية مسموح بها رغم عدم استحسانها، لأنها قد تضرّ بالأداء).
حذف قيم من مجموعة
إذا كنت تريد إفراغ المجموعة أو المجموعة المتعدّدة من كل عناصرها، فيمكنك استخدام clear
:
std::set < int > sut; sut.insert(10); sut.insert(15); sut.insert(22); sut.insert(3); sut.clear(); // يساوي 0 sut حجم
ثم يمكنك استخدام التابع erase
الذي يوفّر بعض الوظائف التي تشبه عملية الإدراج:
std::set < int > sut; std::set < int > ::iterator it; sut.insert(10); sut.insert(15); sut.insert(22); sut.insert(3); sut.insert(30); sut.insert(33); sut.insert(45); // الحذف البسيط sut.erase(3); // استخدام مكرّر it = sut.find(22); sut.erase(it); // حذف مجال من القيم it = sut.find(33); sut.erase(it, sut.end()); std::cout << std::endl << "Set under test contains:" << std::endl; for (it = sut.begin(); it != sut.end(); ++it) { std::cout << * it << std::endl; }
الخرج سيكون:
Set under test contains: 10 15 30
تنطبق كلّ هذه التوابع أيضًا على المجموعات المتعدّدة multiset
، يرجى ملاحظة أنّه في حال طلبت حذف عنصر من مجموعة متعدّدة multiset
وكان ذلك العنصر مُكرَّرًا، فستُحذَف جميع العناصر التي تساوي ذلك العنصر.
إدراج قيم في مجموعة
هناك ثلاث طرق لإدراج العناصر في المجموعات.
-
إدراج بسيط للقيمة باستخدام التابع
insert
الذي يعيد زوجًا، ممّا يسمح للمُستدعي بالتحقق مما إذا كان الإدراج قد تمّ أم لا. - يمكن الإدراج بإعطاء تلميح عن الموضع الذي ستُدرج فيه القيمة، والهدف من ذلك هو تحسين وقت الإدراج، لكن المشكلة أنّنا لا نعرف دائمًا الموضع الذي يجب أن تُدرج فيه القيمة. انتبه في هذه الحالة لأن طريقة إعطاء التلميح تختلف بحسب إصدارات المصرّفات.
- أخيرًا، يمكنك إدراج عدة قيم عن طريق إعطاء مؤشّر للبداية (مُضمّن) والنهاية (غير مُضمّن).
#include <iostream> #include <set> int main() { std::set < int > sut; std::set < int > ::iterator it; std::pair < std::set < int > ::iterator, bool > ret; // إدراج بسيط sut.insert(7); sut.insert(5); sut.insert(12); ret = sut.insert(23); if (ret.second == true) std::cout << "# 23 has been inserted!" << std::endl; ret = sut.insert(23); // بما أنها مجموعة، والعدد 23 موجودا سلفا فيها، فستفشل عملية الإدراج if (ret.second == false) std::cout << "# 23 already present in set!" << std::endl; // إدراج مع تلميح لتسريع الأداء it = sut.end(); // وما بعده C++11 هذه الحالة محسَّنة في // بالنسبة للإصدارات السابقة، يمكن التأشير إلى العنصر الذي يسبق موضع الإدراج sut.insert(it, 30); // إدراج مجال من القيم std::set < int > sut2; sut2.insert(20); sut2.insert(30); sut2.insert(45); std::set < int > ::iterator itStart = sut2.begin(); std::set < int > ::iterator itEnd = sut2.end(); sut.insert(itStart, itEnd); // يُستثنى المكرر الثاني من الإدراج std::cout << std::endl << "Set under test contains:" << std::endl; for (it = sut.begin(); it != sut.end(); ++it) { std::cout << * it << std::endl; } return 0; }
سينتج لنا الخرج التالي:
# 23 has been inserted! # 23 already present in set! Set under test contains: 5 7 12 20 23 30 45
إدراج القيم في مجموعة متعددة
جميع طرق الإدراج الخاصّة بالمجموعات تنطبق أيضًا على المجموعات المتعدّدة، لكن هناك خيار آخر، وهو تمرير قائمة تهيئة initializer_list:
auto il = { 7, 5, 12 }; std::multiset < int > msut; msut.insert(il);
البحث عن القيم في المجموعات والمجموعات المتعدّدة
هناك عدّة طرق للبحث عن قيمة معيّنة في مجموعة std::set
أو مجموعة متعدّدة std::multiset
، وللحصول على مُكرِّر يشير إلى موضع أوّل ظهور لمفتاح مُعيّن، يمكن استخدام الدالّة find()
التي تعيد end()
إذا لم يكن المفتاح موجودًا.
std::set < int > sut; sut.insert(10); sut.insert(15); sut.insert(22); sut.insert(3); // 3, 10, 15, 22 auto itS = sut.find(10); // *itS == 10 القيمة موجودة، لذا itS = sut.find(555); // itS == sut.end() لم يُعثَر على القيمة، لذا std::multiset < int > msut; sut.insert(10); sut.insert(15); sut.insert(22); sut.insert(15); sut.insert(3); // 3, 10, 15, 15, 22 auto itMS = msut.find(10);
الطريقة الأخرى هي استخدام الدالّة count()
، والتي تحسُب عدد القيم المطابقة التي عُثِر عليها في المجموعة أو المجموعة المتعدّدة (في حالة المجموعات، ستكون القيمة المُعادة إمّا 0 أو 1).
باستخدام نفس القيم المذكورة أعلاه، سيكون لدينا:
int result = sut.count(10); // 1 result = sut.count(555); // 0 result = msut.count(10); // 1 result = msut.count(15); // 2
في حالة المجموعات المتعدّدة، يمكن أن تكون هناك عدّة عناصر لها نفس القيمة، وللحصول على مجالٍ (range) يمثّل تلك العناصر يمكن استخدام الدالّة equal_range()
التي تُعيد زوجًا يتألّف من مُكرّر الحدّ الأدنى (مُضمّن) ومُكرّر الحدّ الأعلى (غير مضمَن) على التوالي. وإذا لم يكن المفتاح موجودًا فسيشير كلا المُكرِّران إلى أقرب قيمة عليا وفق تابع المقارنة المُستخدم لترتيب المجموعة المتعدّدة المُعطاة.
auto eqr = msut.equal_range(15); auto st = eqr.first; // '15' يشير إلى العنصر الأول auto en = eqr.second; // '22' يشير إلى العنصر eqr = msut.equal_range(9); // '10' يشيران إلى eqr.second و eqr.first كل من
std::integer_sequence: تسلسلات الأعداد الصحيحة
يمثّل قالب الصنف std::integer_sequence<Type, Values...>
سلسلة من القيم العددية الصحيحة من نوع Type
، حيث Type
هو أحد أنواع الأعداد الصحيحة المُضمّنة.
تُستخدم هذه التسلسلات عند تنفيذ قوالب الأصناف أو الدوالّ التي تحتاج إلى الوصول الموضعي (positional access)، وتحتوي المكتبة القياسية أيضًا على أنواع مصنَعيّة (factory types) تنشئ تسلسلات تصاعدية من الأعداد الصحيحة انطلاقًا من عدد العناصر المُراد.
تحويل صفّ std::tuple<T...>
إلى معاملات دالّة
يمكن استخدام صفّ std::tuple<T...>
لتمرير عدّة قيم إلى دالّة، فمثلًا يمكن استخدامه لتخزين سلسلة من المعاملات على شكل صف انتظار (queue)، ويجب تحويل عناصر هذه الصفوف عند معالجتها إلى وسائط استدعاء للدالة.
#include <array> #include <iostream> #include <string> include <tuple> #include <utility> // ---------------------------------------------------------------------------- // الدوالّ المراد استدعاؤها void f(int i, std::string const & s) { std::cout << "f(" << i << ", " << s << ")\n"; } void f(int i, double d, std::string const & s) { std::cout << "f(" << i << ", " << d << ", " << s << ")\n"; } void f(char c, int i, double d, std::string const & s) { std::cout << "f(" << c << ", " << i << ", " << d << ", " << s << ")\n"; } void f(int i, int j, int k) { std::cout << "f(" << i << ", " << j << ", " << k << ")\n"; } // ---------------------------------------------------------------------------- // الدالّة الفعلية التي توسّع الصف template < typename Tuple, std::size_t...I > void process(Tuple const & tuple, std::index_sequence < I... > ) { f(std::get < I > (tuple)...); } // الواجهة المراد استدعاؤها، للأسف يجب أن تُرسل إلى دالّة أخرى لاستخلاص سلسلة الفهارس المُنشأة // std::make_index_sequence<N> من template < typename Tuple > void process(Tuple const & tuple) { process(tuple, std::make_index_sequence < std::tuple_size < Tuple > ::value > ()); } // ---------------------------------------------------------------------------- int main() { process(std::make_tuple(1, 3.14, std::string("foo"))); process(std::make_tuple('a', 2, 2.71, std::string("bar"))); process(std::make_pair(3, std::string("pair"))); process(std::array < int, 3 > { 1, 2, 3 }); }
طالما كان الصنف يدعم std::get<I>(object)
و std::tuple_size<T>::value
، فيمكن توسيعه باستخدام الدالّة process()
أعلاه، إذ أنّ الدالّة نفسها مستقلّة تمامًا عن عدد الوسائط.
إنشاء حزمة مُعاملات مُكوّنة من أعداد صحيحة
تُستخدَم std::integer_sequence
لتخزين سلسلة من الأعداد الصحيحة التي يمكن تحويلها إلى حُزمة معاملات، وفائدتها الرئيسيّة هو إمكانية إنشاء قوالب الأصناف المصنعيّة التي ستنشئ تلك التسلسلات:
#include <iostream> #include <initializer_list> #include <utility> template < typename T, T...I > void print_sequence(std::integer_sequence < T, I... > ) { std::initializer_list < bool > { bool(std::cout << I << ' ')... }; std::cout << '\n'; } template < int Offset, typename T, T...I > void print_offset_sequence(std::integer_sequence < T, I... > ) { print_sequence(std::integer_sequence < T, T(I + Offset)... > ()); } int main() { // تحديد التسلسلات بشكل صريح print_sequence(std::integer_sequence < int, 1, 2, 3 > ()); print_sequence(std::integer_sequence < char, 'f', 'o', 'o' > ()); // توليد التسلسلات print_sequence(std::make_index_sequence < 10 > ()); print_sequence(std::make_integer_sequence < short, 10 > ()); print_offset_sequence < 'A' > (std::make_integer_sequence < char, 26 > ()); }
يَستخدم قالب الدّالة print_sequence()
قائمة التهيئة std::initializer_list<bool>
عند توسيع تسلسل الأعداد الصحيحة لضمان ترتيب التقييم، وتجنّب إنشاء متغيّر [مصفوفة] غير مستخدم.
تحويل سلسلة من الفهارس إلى نُسخ من عنصر ما
يؤدي توسيع حزمة معاملات من الفهارس في تعبير فاصلة (comma expression) يحمل قيمةً ما، إلى إنشاء نسخة من القيمة المقابلة لكل فهرس. ويرى المُصرِّفان gcc
و clang
أنّ الفهرس ليس له أيّ تأثير، لذا يطلقان تحذيرًا بشأنه (يمكن إسكات gcc
عبر تحويل الفهرس إلى قيمة فارغة void
):
#include <algorithm> #include <array> #include <iostream> #include <iterator> #include <string> #include <utility> template < typename T, std::size_t...I > std::array < T, sizeof...(I) > make_array(T const & value, std::index_sequence < I... > ) { return std::array < T, sizeof...(I) > { (I, value)... }; } template < int N, typename T > std::array < T, N > make_array(T const & value) { return make_array(value, std::make_index_sequence < N > ()); } int main() { auto array = make_array < 20 > (std::string("value")); std::copy(array.begin(), array.end(), std::ostream_iterator < std::string > (std::cout, " ")); std::cout << "\n"; }
هذا الدرس جزء من سلسلة مقالات عن C++.
ترجمة -بتصرّف- للفصول 51 وحتى 60 من كتاب C++ Notes for Professionals
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.