حول معايير C ++ الجديدة

اليوم لدي وظيفة قصيرة جدا. ربما لن أكتبها ، لكن على Habré في التعليقات ، يمكنك أن تجد في كثير من الأحيان الرأي القائل بأن الايجابيات تزداد سوءًا ، واللجنة تجعل من غير الواضح ما هو غير الواضح لماذا ، وأعادني عام 2007 بشكل عام. ثم ظهر هذا المثال الواضح فجأة.


منذ حوالي خمس سنوات تقريبًا ، كتبت عن كيفية ممارسة الكاري في لغة C ++. حسنًا ، إذا تمكنت من كتابة foo(bar, baz, quux) ، فيمكنك كتابة Curry(foo)(bar)(baz)(quux) . ثم خرج C ++ 14 للتو وكان مدعومًا بالكاد من قِبل المترجمين ، لذلك لم يستخدم الكود إلا شرائح C ++ 11 (بالإضافة إلى عكازين لمحاكاة وظائف المكتبة من C ++ 14).


ثم تعثرت على هذا الرمز مرة أخرى ، وأصاب عيني الأذى بمدى المطول. بالإضافة إلى ذلك ، لقد قلبت التقويم منذ وقت ليس ببعيد ، ولاحظت أنه الآن عام 2019 ، ويمكنك أن ترى كيف يمكن لـ C ++ 17 أن تجعل حياتنا أسهل.


يجب أن نرى؟


حسنا ، دعنا نرى.


يبدو التنفيذ الأصلي ، الذي سنرقص منه ، مثل هذا:


 template<typename F, typename... PrevArgs> class CurryImpl { const F m_f; const std::tuple<PrevArgs...> m_prevArgs; public: CurryImpl (F f, const std::tuple<PrevArgs...>& prev) : m_f { f } , m_prevArgs { prev } { } private: template<typename T> std::result_of_t<F (PrevArgs..., T)> invoke (const T& arg, int) const { return invokeIndexed (arg, std::index_sequence_for<PrevArgs...> {}); } template<typename IF> struct Invoke { template<typename... IArgs> auto operator() (IF fr, IArgs... args) -> decltype (fr (args...)) { return fr (args...); } }; template<typename R, typename C, typename... Args> struct Invoke<R (C::*) (Args...)> { R operator() (R (C::*ptr) (Args...), C c, Args... rest) { return (c.*ptr) (rest...); } R operator() (R (C::*ptr) (Args...), C *c, Args... rest) { return (c->*ptr) (rest...); } }; template<typename T, std::size_t... Is> auto invokeIndexed (const T& arg, std::index_sequence<Is...>) const -> decltype (Invoke<F> {} (m_f, std::get<Is> (m_prevArgs)..., arg)) { return Invoke<F> {} (m_f, std::get<Is> (m_prevArgs)..., arg); } template<typename T> auto invoke (const T& arg, ...) const -> CurryImpl<F, PrevArgs..., T> { return { m_f, std::tuple_cat (m_prevArgs, std::tuple<T> { arg }) }; } public: template<typename T> auto operator() (const T& arg) const -> decltype (invoke (arg, 0)) { return invoke (arg, 0); } }; template<typename F> CurryImpl<F> Curry (F f) { return { f, {} }; } 

في m_f ، يوجد المُخزن المُخزَّن ، في m_prevArgs - الوسائط المخزنة على المكالمات السابقة.


هنا يجب أن يحدد operator() ما إذا كان من الممكن بالفعل استدعاء operator() المحفوظ ، أو ما إذا كان من الضروري الاستمرار في تجميع الحجج ، مما يجعل SFINAE قياسيًا إلى حد ما باستخدام مساعد الاستدعاء. بالإضافة إلى ذلك ، من أجل استدعاء المشغل (أو التحقق من قابليته للاتصال) ، نحن نغطي كل ذلك بطبقة SFINAE أخرى لفهم كيفية القيام بذلك (لأننا نحتاج إلى استدعاء المؤشر للعضو ، والدالة المجانية بطرق مختلفة) ، ولهذا نستخدم بنية المساعد Invoke ، والتي ربما تكون غير مكتملة ... باختصار ، هناك الكثير من الأشياء.


حسنًا ، هذا الشيء يعمل بشكل مثير للاشمئزاز تمامًا مع دلالات الحركة ، وإعادة توجيه مثالية ، وكلمات أخرى حلوة في قلب علامة الجمع في عصرنا. سيكون إصلاح هذا أصعب قليلاً من اللازم ، لأنه بالإضافة إلى المهمة التي تم حلها مباشرةً ، هناك أيضًا مجموعة من التعليمات البرمجية غير المرتبطة بها تمامًا.


حسنًا ومرة ​​أخرى ، في C ++ 11 ، لا توجد أشياء مثل std::index_sequence والأشياء ذات الصلة ، أو الاسم المستعار std::result_of_t ، لذلك سيكون رمز C ++ 11 النقي أكثر صعوبة.


أخيرًا ، دعنا ننتقل إلى C ++ 17.


أولاً ، لا نحتاج إلى تحديد operator() نوع الإرجاع operator() ، يمكننا أن نكتب ببساطة:


 template<typename T> auto operator() (const T& arg) const { return invoke (arg, 0); } 

من الناحية الفنية ، هذا ليس هو نفسه تمامًا (يتم عرض "الربط" بطرق مختلفة) ، ولكن هذا ليس ضروريًا في إطار مهمتنا.


بالإضافة إلى ذلك ، لا نحتاج إلى القيام SFINAE بأيدينا للتحقق من m_f باستخدام الوسائط المخزنة. يمنحنا C ++ 17 constexpr if : constexpr if و std::is_invocable . تخلص من كل ما كان لدينا من قبل واكتب الهيكل العظمي operator() الجديد operator() :


 template<typename T> auto operator() (const T& arg) const { if constexpr (std::is_invocable_v<F, PrevArgs..., T>) //   else //       arg } 

الفرع الثاني تافه ، يمكنك نسخ الكود الذي كان بالفعل:


 template<typename T> auto operator() (const T& arg) const { if constexpr (std::is_invocable_v<F, PrevArgs..., T>) //   else return CurryImpl<F, PrevArgs..., T> { m_f, std::tuple_cat (m_prevArgs, std::tuple<T> { arg }) }; } 

الفرع الأول سيكون أكثر إثارة للاهتمام. نحتاج إلى استدعاء m_f ، لتمرير جميع الوسائط المخزنة في m_prevArgs ، بالإضافة إلى arg . لحسن الحظ ، لم نعد بحاجة إلى أي integer_sequence : في C ++ 17 ، توجد دالة مكتبة قياسية std::apply integer_sequence لاستدعاء دالة مع الوسائط المخزنة في tuple . نحتاج فقط إلى وضع وسيطة أخرى ( arg ) في نهاية الدمية ، لذلك يمكننا إما إجراء std::tuple_cat ، أو فقط فك std::tuple_cat 'يمكننا استخدام lambda عام موجود (ميزة أخرى ظهرت بعد C ++ 11 ، وإن لم يكن في 17!). في تجربتي ، يكون إنشاء الدمى البطيئة بطيئًا (في وقت الحساب ، بالطبع) ، لذلك سأختار الخيار الثاني. في lambda نفسها ، أحتاج إلى استدعاء m_f ، ولكي أقوم بذلك بشكل صحيح ، يمكنني استخدام وظيفة المكتبة التي ظهرت في C ++ 17 ، std::invoke ، عن طريق رمي مساعد Invoke المكتوب بخط اليد:


 template<typename T> auto operator() (const T& arg) const { if constexpr (std::is_invocable_v<F, PrevArgs..., T>) { auto wrapper = [this, &arg] (auto&&... args) { return std::invoke (m_f, std::forward<decltype (args)> (args)..., arg); }; return std::apply (std::move (wrapper), m_prevArgs); } else return CurryImpl<F, PrevArgs..., T> { m_f, std::tuple_cat (m_prevArgs, std::tuple<T> { arg }) }; } 

من المفيد أن نلاحظ كيف يتيح لك نوع المرتجعات auto if constexpr إرجاع قيم الأنواع المختلفة في الفروع المختلفة if constexpr .


في أي حال ، هذا هو الأساس. أو جنبا إلى جنب مع تسخير اللازمة:


 template<typename F, typename... PrevArgs> class CurryImpl { const F m_f; const std::tuple<PrevArgs...> m_prevArgs; public: CurryImpl (F f, const std::tuple<PrevArgs...>& prev) : m_f { f } , m_prevArgs { prev } { } template<typename T> auto operator() (const T& arg) const { if constexpr (std::is_invocable_v<F, PrevArgs..., T>) { auto wrapper = [this, &arg] (auto&&... args) { return std::invoke (m_f, std::forward<decltype (args)> (args)..., arg); }; return std::apply (std::move (wrapper), m_prevArgs); } else return CurryImpl<F, PrevArgs..., T> { m_f, std::tuple_cat (m_prevArgs, std::tuple<T> { arg }) }; } }; template<typename F, typename... Args> CurryImpl<F, Args...> Curry (F f, Args&&... args) { return { f, std::forward_as_tuple (std::forward<Args> (args)...) }; } 

أعتقد أن هذا يمثل تحسنا ملحوظا مقارنة بالإصدار الأصلي. وهو أسهل في القراءة. حتى مملة بطريقة أو بأخرى ، لا يوجد تحد .


بالإضافة إلى ذلك ، يمكننا أيضًا التخلص من وظيفة Curry واستخدام CurryImpl مباشرة ، بالاعتماد على أدلة الاستنباط ، ولكن من الأفضل القيام بذلك عندما نتعامل مع إعادة توجيه مثالية وما شابه ذلك. الذي يجلب لنا بسلاسة ...


الآن أصبح من الواضح تمامًا كم هو فظيع بالنسبة لنسخ الحجج ، هذا التوجيه المثالي المؤسف وما شابه ذلك. ولكن الأهم من ذلك ، إصلاحه الآن أسهل بكثير. لكننا سنفعل ذلك بطريقة ما في المنشور التالي.


بدلا من الاستنتاج


أولاً ، في C ++ 20 ، ستظهر std::bind_front ، والتي ستغطي حصة الأسد من حالات المستخدم التي أحتاج إلى شيء من هذا القبيل. يمكنك عموما رميها بعيدا. حزين.


ثانياً ، تصبح الكتابة على المحترفين أكثر سهولة ، حتى إذا قمت بكتابة نوع من التعليمات البرمجية للقالب باستخدام metaprogramming. لم تعد مضطرًا للتفكير في خيار SFINAE الذي يمكنك اختياره ، وكيفية فك الدمية ، وكيفية استدعاء الوظيفة. تأخذ فقط والكتابة if constexpr ، std::apply ، std::invoke . من ناحية ، هذا أمر جيد ؛ لا أريد العودة إلى الإصدار C ++ 14 أو 11 بشكل خاص. من ناحية أخرى ، يبدو أن طبقة مهارات الأسد أصبحت غير ضرورية. لا ، لا يزال من المفيد أن تكون قادرًا على ربط شيء من هذا القبيل في القوالب وفهم كيفية عمل كل سحر المكتبة في داخلك ، ولكن إذا كان عليك القيام بذلك طوال الوقت ، فهو الآن أقل شيوعًا. يسبب بعض العواطف الغريبة.

Source: https://habr.com/ru/post/ar466985/


All Articles