今天我有一个简短的帖子。 我可能不会写它,但是在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()
必须确定是否已经可以调用已保存的函子,或者是否有必要继续累积参数,因此它使用invoke
助手来创建相当标准的SFINAE。 此外,为了调用函子(或检查其可调用性),我们在另一个SFINAE层上覆盖了所有内容,以了解如何执行此操作(因为我们需要以不同的方式调用指向该成员的指针,例如,调用free函数),为此,我们使用了Invoke
helper结构,这可能是不完整的。简而言之,很多事情。
好吧,这件事绝对令人作呕,它与移动语义,完美转发和其他对我们时代正负号至关重要的词一起使用。 修复此问题将比必要的困难一点,因为除了直接解决的任务之外,还有一堆与其完全不相关的代码。
很好,同样,在C ++ 11中,没有诸如std::index_sequence
和相关内容之类的东西,也没有别名std::result_of_t
,因此纯C ++ 11代码将更加困难。
所以,最后,让我们继续C ++ 17。
首先,我们不需要指定返回类型operator()
,我们可以简单地编写:
template<typename T> auto operator() (const T& arg) const { return invoke (arg, 0); }
从技术上讲,这并不完全相同(“链接”以不同的方式显示),但是在我们的任务框架内,这不是必需的。
另外,我们不需要用手进行SFINAE检查带有存储的参数m_f
的可调用性。 C ++ 17为我们提供了两个很酷的功能: constexpr if
和std::is_invocable
。 扔掉我们之前拥有的所有内容,并编写新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
来调用带有存储在tuple
参数的函数。 只需要在虚拟变量的末尾添加另一个参数( arg
),这样我们就可以使std::tuple_cat
或解压缩std::apply
'我们可以使用现有的虚拟通用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 }) }; }
请注意, if constexpr
, auto
推断的返回类型如何允许您在不同的分支中返回不同类型的值。
无论如何,基本上就是全部。 或连同必要的安全带:
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
,这将覆盖我需要std::bind_front
这种东西的大部分用户案例。 您通常可以将其丢弃。 伤心
其次,即使您使用元编程编写某种模板代码,编写专家的方法也变得越来越容易。 您不再需要考虑要选择哪个SFINAE选项,如何解压缩虚拟对象,如何调用函数。 if constexpr
, std::apply
, std::invoke
只需编写即可。 一方面,这很好;我不想返回C ++ 14,尤其是11。 另一方面,感觉像狮子的技能层变得不必要了。 不,将类似的内容拧紧到模板上并了解所有这些库魔术如何在您内部发挥作用仍然很有用,但是如果您过去一直都需要它,那么现在它已经不那么普遍了。 它引起一些奇怪的情绪。