سيظهر معيار C ++ 20 قريبًا ، على الأرجح ، سيتم إضافة مفهوم
النطاقات ، لكن قلة من الناس يعرفون ماهية هذه الأشياء وما الذي يتناولونه. لم أتمكن من الوصول إلى جمهور واسع من المصادر باللغة الروسية حول هذا الوحش ، لذلك أود أن أتحدث في هذا المقال أكثر حوله ، بناءً على محاضرة أرنو شودل
"من المتكررين
إلى النطاقات: تطور القادم من المحكمة الخاصة بلبنان" من الاجتماع C ++ 2015- من السنة. سأحاول توضيح هذه المقالة قدر الإمكان بالنسبة لأولئك الذين واجهوا هذا المفهوم أولاً ، وفي نفس الوقت سأتحدث عن جميع أنواع الرقائق مثل المحولات الفاصلة لأولئك الذين هم على دراية بهذا المفهوم بالفعل ويريدون معرفة المزيد.
المكتبات ذات النطاقات
في وقت كتابة هذا التقرير ، كانت هناك ثلاث مكتبات رئيسية تقوم بتنفيذ الفواصل الزمنية:
المكتبة الأولى ، في الواقع ، هي منشئ هذا المفهوم (وهذا ليس مفاجئًا ، لأنه لا يوجد شيء في مجموعة مكتبات
Boost :)). والثاني هو مكتبة إريك نيبلر ، والتي سيتم وصفها لاحقًا. وأخيرًا ، المكتبة الأخيرة ، كما قد تتخيلها ، كتبت بواسطة خلية فكرية ، والتي ، على سبيل المثال ، قد طورت وحسنت Boost.Range.
لماذا فترات هو مستقبلنا؟
بالنسبة لأولئك الذين ليسوا على دراية بمفهوم الفاصل الزمني ، نعرّف هذا المفهوم غير التافه على أنه المفهوم الذي له بداية ونهاية (
زوج من التكرارات ).
دعونا الآن نفكر في المهمة التالية: يوجد متجه ، من الضروري إزالة جميع العناصر المكررة منه. وفقًا للمعايير الحالية ، سنحلها كما يلي:
std::vector<T> vec=...; std::sort( vec.begin(), vec.end() ); vec.erase( std::unique( vec.begin(), vec.end() ), vec.end() );
في هذه الحالة ، نشير إلى اسم المتجه بقدر
6 مرات! ومع ذلك ، باستخدام مفهوم الفواصل الزمنية (الجمع بين التكرارات في بداية ونهاية المتجه في كائن واحد) ، يمكننا أن نكتب عدة مرات أسهل عن طريق تحديد المتجه المطلوب
مرة واحدة فقط:
tc::unique_inplace( tc::sort(vec) );
أي من الفواصل الزمنية هي حاليا ضمن المعيار الحالي؟
في معيار C ++ 11 ، تمت إضافة نطاق يستند إلى الحلقة والوصول الشامل إلى بداية / نهاية الحاويات ، وفي أحدث معيار C ++ 17 ، لم يتم إضافة أي شيء جديد يتعلق بفواصل زمنية.
for ( int& i : <range_expression> ) { ... }
std::begin/end(<range_expression>)
فترات المستقبل
دعنا الآن نتحدث عن مكتبة Range V3 التي سبق ذكرها. قام إريك نيبلر ، منشئ مشروعه ، بإنشاء مشروع
المواصفات الفنية الخاص بـ Range ، حيث قام بتعديل مكتبة
الخوارزمية لدعم الفواصل الزمنية. يبدو شيء مثل هذا:
namespace ranges { template< typename Rng, typename What > decltype(auto) find( Rng && rng, What const& what ) { return std::find( ranges::begin(rng), ranges::end(rng), what ); } }
يوجد على موقعه بعض المعاينة لما يريد توحيدها ، هذا هو
Range V3 .
ما يمكن اعتبار مجموعة؟
أولاً وقبل كل شيء ،
الحاويات (المتجهات ، السلسلة ، القائمة ، إلخ) ، لأنها لها بداية ونهاية. من الواضح أن للحاويات عناصرها الخاصة ، أي عندما نشير إلى الحاويات ، فإننا نشير إلى جميع عناصرها. وبالمثل ، عند النسخ والإعلان عن ثابت (النسخ العميق والاتساق). ثانياً ، يمكن أيضًا اعتبار
طرق العرض فواصل زمنية. المشاهدات هي مجرد زوج من التكرارات التي تشير إلى البداية والنهاية ، على التوالي. هنا أبسط تنفيذها:
template<typename It> struct iterator_range { It m_itBegin; It m_itEnd; It begin() const { return m_itBegin; } It end() const { return m_itEnd; } };
المشاهدات ، بدورها ، تشير فقط إلى العناصر ، وبالتالي فإن النسخ والاتساق كسولان (هذا لا يؤثر على العناصر).
محولات الفاصل
لم يتوقف مخترعو الفواصل الزمنية عند هذا ، لأنه بخلاف ذلك سيكون هذا المفهوم عديم الجدوى. لذلك ، قاموا بتقديم مفهوم مثل محولات النطاق.
تحويل محول
ضع في اعتبارك المهمة التالية: دع المتجه
int يعطى ، حيث نحتاج إلى إيجاد العنصر الأول مساوي 4:
std::vector<int> v; auto it = ranges::find(v, 4);
الآن دعنا نتخيل أن نوع المتجه ليس int ، لكن نوعًا ما من البنية المكتوبة ذاتيا المعقدة ، ولكن يوجد به int ، والمهمة لا تزال كما هي:
struct A { int id; double data; }; std::vector<A> v={...}; auto it = ranges::find_if( v, [](A const& a) { return a.id == 4; } );
من الواضح أن هذين الرمزين متشابهان في الدلالات ، لكنهما يختلفان اختلافًا كبيرًا في بناء الجملة ، لأنه في الحالة الأخيرة ، كان علينا أن نكتب وظيفة تعمل يدويًا عبر الحقل
int . ولكن إذا كنت تستخدم محول تحويل (محول
تحويل ) ، فسيبدو كل شيء أكثر إيجازًا:
struct A { int id; double data; }; std::vector<A> v={...}; auto it = ranges::find( tc::transform(v, std::mem_fn(&A::id)), 4);
في الواقع ، فإن محول التحويل "يحول" هيكلنا من خلال إنشاء فئة غلاف حول الحقل int. من الواضح أن المؤشر يشير إلى حقل
المعرف ، ولكن إذا أردنا أن يشير إلى البنية بأكملها ،
فسنحتاج إلى إضافة
.base () في النهاية. يقوم هذا الأمر بتغليف الحقل ، والذي يمكن للمؤشر تشغيله عبر البنية بأكملها:
auto it = ranges::find( tc::transform(v, std::mem_fn(&A::id)), 4).base();
فيما يلي مثال على تنفيذ محول التحويل (يتكون من متكررين ، ولكل منهما مُشغل خاص به):
template<typename Base, typename Func> struct transform_range { struct iterator { private: Func m_func;
محول مرشح
وإذا كنا في المهمة الأخيرة نحتاج إلى إيجاد ليس أول عنصر من هذا القبيل ، ولكن "تصفية" الحقل
بأكمله من
int لوجود هذه العناصر؟ في هذه الحالة ، سوف نستخدم محول عامل التصفية:
tc::filter( v, [](A const& a) { return 4 == a.id; } );
لاحظ أن المرشح يتم تنفيذه بتكاسل أثناء التكرار.
وهنا تطبيقه الساذج (يتم تطبيق شيء مثل هذا في Boost.Range):
template<typename Base, typename Func> struct filter_range { struct iterator { private: Func m_func;
كما نرى ، هناك حاجة لتكرار اثنين هنا بدلا من واحد ، كما كان في محول التحويل. التكرار الثاني ضروري حتى لا يتجاوز عن طريق الخطأ حدود الحاوية أثناء التكرار.
بعض التحسينات
حسنًا ، ولكن ما شكل المكرر من
tc :: filter (tc :: filter (tc :: filter (...))) ؟
Boost.range
كجزء من التنفيذ أعلاه ، يبدو كما يلي:
خافت القلب لا يشاهد!m_func3
m_it3
m_func2
m_it2
m_func1
m_it1;
m_itEnd1;
m_itEnd2
m_func1
m_it1;
m_itEnd1;
m_itEnd3
m_func2
m_it2
m_func1
m_it1;
m_itEnd1;
m_itEnd2
m_func1
m_it1;
m_itEnd1;
من الواضح ، هذا غير فعال
بشكل رهيب .
المدى v3
دعونا نفكر في كيفية تحسين هذا المحول. كانت فكرة Eric Nibler هي وضع معلومات عامة (طرف فاعل ومؤشر في النهاية) في كائن المحول ، وبعد ذلك يمكننا تخزين رابط إلى كائن هذا المحول والمكرر المطلوب
*m_rng
m_it
بعد ذلك ، في إطار هذا التطبيق ، سيبدو المرشح الثلاثي مثل هذا:
تايكm_rng3
m_it3
m_rng2
m_it2
m_rng1
m_it1
هذا لا يزال غير مثالي ، على الرغم من أنه في بعض الأحيان أسرع من التنفيذ السابق.
فكر الخلية ، مؤشر الفهرس
الآن النظر في حل خلية التفكير. وقدموا
مفهوم مؤشر ما يسمى لحل هذه المشكلة. الفهرس عبارة عن مكرر يقوم بتنفيذ جميع العمليات نفسها مثل مكرر منتظم ، لكنه يقوم بذلك عن طريق الإشارة إلى فواصل زمنية.
template<typename Base, typename Func> struct index_range { ... using Index = ...; Index begin_index() const; Index end_index() const; void increment_index( Index& idx ) const; void decrement_index( Index& idx ) const; reference dereference( Index& idx ) const; ... };
نعرض كيفية دمج فهرس مع مكرر منتظم.
من الواضح أن التكرار العادي يمكن اعتباره أيضًا فهرسًا. في الاتجاه المعاكس ، يمكن تنفيذ التوافق على سبيل المثال مثل هذا:
template<typename IndexRng> struct iterator_for_index { IndexRng* m_rng; typename IndexRng::Index m_idx; iterator& operator++() { m_rng.increment_index(m_idx); return *this; } ... };
بعد ذلك سيتم تنفيذ المرشح الثلاثي بكفاءة عالية:
template<typename Base, typename Func> struct filter_range { Func m_func; Base& m_base; using Index = typename Base::Index; void increment_index( Index& idx ) const { do { m_base.increment_index(idx); } while ( idx != m_base.end_index() && !m_func(m_base.dereference_index(idx)) ); } };
template<typename IndexRng> struct iterator_for_index { IndexRng* m_rng; typename IndexRng::Index m_idx; ... };
في إطار هذا التطبيق ، ستعمل الخوارزمية بسرعة بغض النظر عن عمق الفلتر.
فترات مع حاويات lvalue و rvalue
الآن لنرى كيف تعمل الفواصل الزمنية مع حاويات lvalue و rvalue:
القيمة
نطاق V3 وخلية التفكير تتصرف مع lvalue. لنفترض أن لدينا كودًا مثل هذا:
auto rng = view::filter(vec, pred1); bool b = ranges::any_of(rng, pred2);
هنا لدينا ناقل مُعلن سابقًا يكمن في الذاكرة (القيمة) ، ونحن بحاجة إلى إنشاء فاصل زمني ثم العمل معه بطريقة أو بأخرى. ننشئ طريقة عرض باستخدام
view :: filter أو
tc :: filter ونصبح سعداء ، لا توجد أخطاء ، ويمكننا بعد ذلك استخدام طريقة العرض هذه ، على سبيل المثال ، في any_of.
المدى V3 و rvalue
ومع ذلك ، إذا لم يكن متجهنا في الذاكرة بعد (على سبيل المثال ، إذا كنا مجرد إنشائه) ، وكنا نواجه نفس المهمة ، فسنحاول أن نكتب ونواجه خطأ:
auto rng = view::filter(create_vector(), pred1);
لماذا ظهرت؟ سوف يكون العرض رابطًا متدليًا إلى rvalue نظرًا لحقيقة أننا ننشئ متجهًا ونضعه مباشرةً في عامل تصفية ، أي أنه سيكون هناك رابط rvalue في المرشح ، والذي سيشير بعد ذلك إلى شيء غير معروف عندما ينتقل المحول البرمجي إلى السطر التالي ويحدث خطأ. لحل هذه المشكلة ، توصل Range V3 إلى
إجراء :
auto rng = action::filter(create_vector(), pred1);
العمل يفعل كل شيء في وقت واحد ، أي أنه يتطلب ناقلًا ، ويقوم بتصفية المرشحات ووضعه في فاصل زمني. ومع ذلك ، فإن الطرح هو أنه لم يعد كسولًا ، وحاولت خلية التفكير إصلاح هذا الطرح.
التفكير الخلية و rvalue
قامت Think-cell بعمله بحيث يتم إنشاء الحاوية بدلاً من المشاهدة:
auto rng = tc::filter(creates_vector(), pred1); bool b = ranges::any_of(rng, pred2);
نتيجة لذلك ، لا نواجه خطأً مماثلاً ، لأنه في تطبيقه يجمع المرشح حاوية rvalue بدلاً من الارتباط ، لذلك يحدث هذا بتكاسل. لم يرغب Range V3 في القيام بذلك لأنهم كانوا يخشون أن تكون هناك أخطاء بسبب أن عامل التصفية يتصرف إما كطريقة عرض أو كحاوية ، لكن خلية التفكير مقتنعة بأن المبرمجين يفهمون كيف يتصرف المرشح ، معظم الأخطاء تنشأ على وجه التحديد بسبب هذا "الكسل".
فترات المولدات
نحن نعمم مفهوم الفواصل الزمنية. في الواقع ، هناك فترات دون التكرار. يطلق عليهم
نطاقات المولدات . لنفترض أن لدينا عنصر واجهة المستخدم الرسومية (عنصر واجهة) ونحن نسمي القطعة نقل. لدينا نافذة تطالب بنقل عنصر واجهة المستخدم الخاصة بها ، ولدينا أيضًا زر في
مربع القائمة ، ويجب أن تنتقل نافذة أخرى أيضًا من خلال عناصر واجهة المستخدم ، أي أننا نطلق على
traverse_widgets ، التي تربط العناصر بمُشغل (
يمكنك أن تقول أن هناك وظيفة تعداد حيث يمكنك قم بتوصيل المشغّل ، وتسرد الوظيفة جميع العناصر الموجودة في هذا المشغل ).
template<typename Func> void traverse_widgets( Func func ) { if (window1) { window1->traverse_widgets(std::ref(func)); } func(button1); func(listbox1); if (window2) { window2->traverse_widgets(std::ref(func)); } }
هذا يذكرنا إلى حد ما تباعد القطعة ، ولكن لا يوجد تكرار هنا. إن كتابتها مباشرة ستكون غير فعالة ، وقبل كل شيء ، ستكون صعبة للغاية. في هذه الحالة ، يمكننا القول أن هذه الهياكل تعتبر أيضًا فترات زمنية. ثم في مثل هذه الحالات ، يكون هناك استخدام لأساليب الفاصل الزمني المفيدة ، مثل
any_of :
mouse_hit_any_widget=tc::any_of( [] (auto func) { traverse_widgets(func); }, [] (auto const& widget) { return widget.mouse_hit(); } );
تحاول خلية التفكير تنفيذ الأساليب بحيث يكون لها نفس الواجهة لجميع أنواع الفواصل الزمنية:
namespace tc { template< typename Rng > bool any_of( Rng const& rng ) { bool bResult = false; tc::enumerate( rng, [&](bool_context b) { bResult = bResult || b; } ); return bResult; } }
باستخدام
tc :: enumerate ، يكون الفرق بين الفواصل الزمنية مخفيًا ، لأن هذا التطبيق يتمسك بمفهوم
التكرار الداخلي (ما يتم وصفه بمفاهيم
التكرار الخارجي والداخلي بمزيد من التفاصيل في المحاضرة) ، ومع ذلك ، فإن لهذا التطبيق عيوبه ، وهي: توقف
std :: any_of بمجرد مواجهة
الحقيقة . يحاولون حل هذه المشكلة ، على سبيل المثال ، عن طريق إضافة استثناءات (ما يسمى
بفواصل المولد التي تمت مقاطعتها ).
الخاتمة
أنا أكره النطاق القائم على الحلقة لأنه يحفز الناس على كتابتها أينما دعت الحاجة وحيثما لا تكون هناك حاجة إليها ، نظرًا لتزايد دقة الرمز ، على سبيل المثال ، يكتب الناس هذا:
bool b = false; for (int n : rng) { if ( is_prime(n) ) { b = true; break; } }
بدلا من ذلك:
bool b = tc::any_of( rng, is_prime );