اليوم لدي وظيفة قصيرة جدا. ربما لن أكتبها ، لكن على 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>)
الفرع الثاني تافه ، يمكنك نسخ الكود الذي كان بالفعل:
template<typename T> auto operator() (const T& arg) const { if constexpr (std::is_invocable_v<F, PrevArgs..., T>)
الفرع الأول سيكون أكثر إثارة للاهتمام. نحتاج إلى استدعاء 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 بشكل خاص. من ناحية أخرى ، يبدو أن طبقة مهارات الأسد أصبحت غير ضرورية. لا ، لا يزال من المفيد أن تكون قادرًا على ربط شيء من هذا القبيل في القوالب وفهم كيفية عمل كل سحر المكتبة في داخلك ، ولكن إذا كان عليك القيام بذلك طوال الوقت ، فهو الآن أقل شيوعًا. يسبب بعض العواطف الغريبة.