我该如何制作咖喱功能?

Yak*_*ont 25 c++ templates overloading currying c++14

在C++ 14中,什么是一种理解函数或函数对象的好方法?

特别是,我有一个foo带有一些随机数量的重载的重载函数:可以通过ADL找到一些重载,其他的可以在无数个地方定义.

我有一个帮助对象:

static struct {
  template<class...Args>
  auto operator()(Args&&...args)const
  -> decltype(foo(std::forward<Args>(args)...))
    { return (foo(std::forward<Args>(args)...));}
} call_foo;
Run Code Online (Sandbox Code Playgroud)

这让我可以将重载集作为单个对象传递.

如果我想要咖喱foo,我该怎么办呢?

由于curry和部分功能应用程序经常互换使用,curry我的意思是,如果foo(a,b,c,d)是有效的呼叫,那么curry(call_foo)(a)(b)(c)(d)必须是有效的呼叫.

Yak*_*ont 11

这是我目前最好的尝试.

#include <iostream>
#include <utility>
#include <memory>
Run Code Online (Sandbox Code Playgroud)

SFINAE实用助手类:

template<class T>struct voider{using type=void;};
template<class T>using void_t=typename voider<T>::type;
Run Code Online (Sandbox Code Playgroud)

一个traits类,它告诉你Sig是否是一个有效的invokation - 即if std::result_of<Sig>::type是否定义了行为.在一些C++实现中,只需检查即可std::result_of,但标准不要求:

template<class Sig,class=void>
struct is_invokable:std::false_type {};
template<class F, class... Ts>
struct is_invokable<
  F(Ts...),
  void_t<decltype(std::declval<F>()(std::declval<Ts>()...))>
>:std::true_type {
  using type=decltype(std::declval<F>()(std::declval<Ts>()...));
};
template<class Sig>
using invoke_result=typename is_invokable<Sig>::type;
template<class T> using type=T;
Run Code Online (Sandbox Code Playgroud)

咖喱助手是一种手动的lambda.它捕获一个函数和一个参数.它不是作为lambda编写的,所以我们可以在rvalue上下文中使用正确的rvalue转发,这在currying时很重要:

template<class F, class T>
struct curry_helper {
  F f;
  T t;
  template<class...Args>
  invoke_result< type<F const&>(T const&, Args...) >
  operator()(Args&&...args)const&
  {
    return f(t, std::forward<Args>(args)...);
  }
  template<class...Args>
  invoke_result< type<F&>(T const&, Args...) >
  operator()(Args&&...args)&
  {
    return f(t, std::forward<Args>(args)...);
  }
  template<class...Args>
  invoke_result< type<F>(T const&, Args...) >
  operator()(Args&&...args)&&
  {
    return std::move(f)(std::move(t), std::forward<Args>(args)...);
  }
};
Run Code Online (Sandbox Code Playgroud)

肉和土豆:

template<class F>
struct curry_t {
  F f;
  template<class Arg>
  using next_curry=curry_t< curry_helper<F, std::decay_t<Arg> >;
  // the non-terminating cases.  When the signature passed does not evaluate
  // we simply store the value in a curry_helper, and curry the result:
  template<class Arg,class=std::enable_if_t<!is_invokable<type<F const&>(Arg)>::value>>
  auto operator()(Arg&& arg)const&
  {
    return next_curry<Arg>{{ f, std::forward<Arg>(arg) }};
  }
  template<class Arg,class=std::enable_if_t<!is_invokable<type<F&>(Arg)>::value>>
  auto operator()(Arg&& arg)&
  {
    return next_curry<Arg>{{ f, std::forward<Arg>(arg) }};
  }
  template<class Arg,class=std::enable_if_t<!is_invokable<F(Arg)>::value>>
  auto operator()(Arg&& arg)&&
  {
    return next_curry<Arg>{{ std::move(f), std::forward<Arg>(arg) }};
  }
  // These are helper functions that make curry(blah)(a,b,c) somewhat of a shortcut for curry(blah)(a)(b)(c)
  // *if* the latter is valid, *and* it isn't just directly invoked.  Not quite, because this can *jump over*
  // terminating cases...
  template<class Arg,class...Args,class=std::enable_if_t<!is_invokable<type<F const&>(Arg,Args...)>::value>>
  auto operator()(Arg&& arg,Args&&...args)const&
  {
    return next_curry<Arg>{{ f, std::forward<Arg>(arg) }}(std::forward<Args>(args)...);
  }
  template<class Arg,class...Args,class=std::enable_if_t<!is_invokable<type<F&>(Arg,Args...)>::value>>
  auto operator()(Arg&& arg,Args&&...args)&
  {
    return next_curry<Arg>{{ f, std::forward<Arg>(arg) }}(std::forward<Args>(args)...);
  }
  template<class Arg,class...Args,class=std::enable_if_t<!is_invokable<F(Arg,Args...)>::value>>
  auto operator()(Arg&& arg,Args&&...args)&&
  {
    return next_curry<Arg>{{ std::move(f), std::forward<Arg>(arg) }}(std::forward<Args>(args)...);
  }

  // The terminating cases.  If we run into a case where the arguments would evaluate, this calls the underlying curried function:
  template<class... Args,class=std::enable_if_t<is_invokable<type<F const&>(Args...)>::value>>
  auto operator()(Args&&... args,...)const&
  {
    return f(std::forward<Args>(args)...);
  }
  template<class... Args,class=std::enable_if_t<is_invokable<type<F&>(Args...)>::value>>
  auto operator()(Args&&... args,...)&
  {
    return f(std::forward<Args>(args)...);
  }
  template<class... Args,class=std::enable_if_t<is_invokable<F(Args...)>::value>>
  auto operator()(Args&&... args,...)&&
  {
    return std::move(f)(std::forward<Args>(args)...);
  }
};

template<class F>
curry_t<typename std::decay<F>::type> curry( F&& f ) { return {std::forward<F>(f)}; }
Run Code Online (Sandbox Code Playgroud)

最后的功能是幽默的.

请注意,没有进行类型擦除.另请注意,理论上手工制作的解决方案可以少得多move,但我认为我不必在任何地方进行复制.

这是一个测试函数对象:

static struct {
  double operator()(double x, int y, std::nullptr_t, std::nullptr_t)const{std::cout << "first\n"; return x*y;}
  char operator()(char c, int x)const{std::cout << "second\n"; return c+x;}
  void operator()(char const*s)const{std::cout << "hello " << s << "\n";}
} foo;
Run Code Online (Sandbox Code Playgroud)

以及一些测试代码,看看它是如何工作的:

int main() {
  auto f = curry(foo);
  // testing the ability to "jump over" the second overload:
  std::cout << f(3.14,10,std::nullptr_t{})(std::nullptr_t{}) << "\n";
  // Call the 3rd overload:
  f("world");
  // call the 2nd overload:
  auto x =  f('a',2);
  std::cout << x << "\n";
  // again:
  x =  f('a')(2);
}
Run Code Online (Sandbox Code Playgroud)

实例

结果代码不仅仅是一团糟.许多方法必须重复3次才能处理&,const&并且&&最佳情况.SFINAE条款冗长而复杂.我最终使用了variardic args varargs,其中的varargs确保了方法中的非重要签名差异(我认为,优先级较低,并不重要,SFINAE确保只有一个重载有效,除了this限定符) .

结果curry(call_foo)是一个对象,一次可以被称为一个参数,或者一次被称为多个参数.您可以使用3个参数调用它,然后是1,然后是1,然后是2,它主要是正确的.没有任何证据可以告诉你它需要多少个参数,除了试图提供它的参数并查看该调用是否有效.这是处理重载情况所必需的.

多参数情况的一个怪癖是它不会将数据包部分传递给一个curry,并将其余部分用作返回值的参数.我可以通过改变来相对容易地改变:

    return curry_t< curry_helper<F, std::decay_t<Arg> >>{{ f, std::forward<Arg>(arg) }}(std::forward<Args>(args)...);
Run Code Online (Sandbox Code Playgroud)

    return (*this)(std::forward<Arg>(arg))(std::forward<Args>(args)...);
Run Code Online (Sandbox Code Playgroud)

和另外两个相似的.这将阻止"跳过"过载的技术,否则该过载将是有效的.思考?这意味着它curry(foo)(a,b,c)在逻辑上与curry(foo)(a)(b)(c)看起来优雅的相同.

感谢@Casey,他的回答很大程度上得到了启发.


最近修订.除非直接调用,否则它会(a,b,c)表现得很像(a)(b)(c).

#include <type_traits>
#include <utility>

template<class...>
struct voider { using type = void; };
template<class...Ts>
using void_t = typename voider<Ts...>::type;

template<class T>
using decay_t = typename std::decay<T>::type;

template<class Sig,class=void>
struct is_invokable:std::false_type {};
template<class F, class... Ts>
struct is_invokable<
  F(Ts...),
  void_t<decltype(std::declval<F>()(std::declval<Ts>()...))>
>:std::true_type {};

#define RETURNS(...) decltype(__VA_ARGS__){return (__VA_ARGS__);}

template<class D>
class rvalue_invoke_support {
  D& self(){return *static_cast<D*>(this);}
  D const& self()const{return *static_cast<D const*>(this);}
public:
  template<class...Args>
  auto operator()(Args&&...args)&->
  RETURNS( invoke( this->self(), std::forward<Args>(args)... ) )

  template<class...Args>
  auto operator()(Args&&...args)const&->
  RETURNS( invoke( this->self(), std::forward<Args>(args)... ) )

  template<class...Args>
  auto operator()(Args&&...args)&&->
  RETURNS( invoke( std::move(this->self()), std::forward<Args>(args)... ) )

  template<class...Args>
  auto operator()(Args&&...args)const&&->
  RETURNS( invoke( std::move(this->self()), std::forward<Args>(args)... ) )
};

namespace curryDetails {
  // Curry helper is sort of a manual lambda.  It captures a function and one argument
  // It isn't written as a lambda so we can enable proper rvalue forwarding when it is
  // used in an rvalue context, which is important when currying:
  template<class F, class T>
  struct curry_helper: rvalue_invoke_support<curry_helper<F,T>> {
    F f;
    T t;

    template<class A, class B>
    curry_helper(A&& a, B&& b):f(std::forward<A>(a)), t(std::forward<B>(b)) {}

    template<class curry_helper, class...Args>
    friend auto invoke( curry_helper&& self, Args&&... args)->
    RETURNS( std::forward<curry_helper>(self).f( std::forward<curry_helper>(self).t, std::forward<Args>(args)... ) )
  };
}

namespace curryNS {
  // the rvalue-ref qualified function type of a curry_t:
  template<class curry>
  using function_type = decltype(std::declval<curry>().f);

  template <class> struct curry_t;

  // the next curry type if we chain given a new arg A0:
  template<class curry, class A0>
  using next_curry = curry_t<::curryDetails::curry_helper<decay_t<function_type<curry>>, decay_t<A0>>>;

  // 3 invoke_ overloads
  // The first is one argument when invoking f with A0 does not work:
  template<class curry, class A0>
  auto invoke_(std::false_type, curry&& self, A0&&a0 )->
  RETURNS(next_curry<curry, A0>{std::forward<curry>(self).f,std::forward<A0>(a0)})

  // This is the 2+ argument overload where invoking with the arguments does not work
  // invoke a chain of the top one:
  template<class curry, class A0, class A1, class... Args>
  auto invoke_(std::false_type, curry&& self, A0&&a0, A1&& a1, Args&&... args )->
  RETURNS(std::forward<curry>(self)(std::forward<A0>(a0))(std::forward<A1>(a1), std::forward<Args>(args)...))

  // This is the any number of argument overload when it is a valid call to f:
  template<class curry, class...Args>
  auto invoke_(std::true_type, curry&& self, Args&&...args )->
  RETURNS(std::forward<curry>(self).f(std::forward<Args>(args)...))

  template<class F>
  struct curry_t : rvalue_invoke_support<curry_t<F>> {
    F f;

    template<class... U>curry_t(U&&...u):f(std::forward<U>(u)...){}

    template<class curry, class...Args>
    friend auto invoke( curry&& self, Args&&...args )->
    RETURNS(invoke_(is_invokable<function_type<curry>(Args...)>{}, std::forward<curry>(self), std::forward<Args>(args)...))
  };
}

template<class F>
curryNS::curry_t<decay_t<F>> curry( F&& f ) { return {std::forward<F>(f)}; }

#include <iostream>

static struct foo_t {
  double operator()(double x, int y, std::nullptr_t, std::nullptr_t)const{std::cout << "first\n"; return x*y;}
  char operator()(char c, int x)const{std::cout << "second\n"; return c+x;}
  void operator()(char const*s)const{std::cout << "hello " << s << "\n";}
} foo;

int main() {

  auto f = curry(foo);
  using C = decltype((f));
  std::cout << is_invokable<curryNS::function_type<C>(const char(&)[5])>{} << "\n";
  invoke( f, "world" );
  // Call the 3rd overload:
  f("world");
  // testing the ability to "jump over" the second overload:
  std::cout << f(3.14,10,nullptr,nullptr) << "\n";
  // call the 2nd overload:
  auto x = f('a',2);
  std::cout << x << "\n";
  // again:
  x =  f('a')(2);
  std::cout << x << "\n";
  std::cout << is_invokable<decltype(foo)(double, int)>{} << "\n";
  std::cout << is_invokable<decltype(foo)(double)>{} << "\n";
  std::cout << is_invokable<decltype(f)(double, int)>{} << "\n";
  std::cout << is_invokable<decltype(f)(double)>{} << "\n";
  std::cout << is_invokable<decltype(f(3.14))(int)>{} << "\n";
  decltype(std::declval<decltype((foo))>()(std::declval<double>(), std::declval<int>())) y = {3};
  (void)y;
  // std::cout << << "\n";
}
Run Code Online (Sandbox Code Playgroud)

现场版


Cas*_*sey 7

这是我尝试使用急切语义,即,只要为原始函数的有效调用(Coliru演示)累积了足够的参数,就返回:

namespace detail {
template <unsigned I>
struct priority_tag : priority_tag<I-1> {};
template <> struct priority_tag<0> {};

// High priority overload.
// If f is callable with zero args, return f()
template <typename F>
auto curry(F&& f, priority_tag<1>) -> decltype(std::forward<F>(f)()) {
    return std::forward<F>(f)();
}

// Low priority overload.
// Return a partial application
template <typename F>
auto curry(F f, priority_tag<0>) {
    return [f](auto&& t) {
        return curry([f,t=std::forward<decltype(t)>(t)](auto&&...args) ->
          decltype(f(t, std::forward<decltype(args)>(args)...)) {
            return f(t, std::forward<decltype(args)>(args)...);
        }, priority_tag<1>{});
    };
}
} // namespace detail

// Dispatch to the implementation overload set in namespace detail.
template <typename F>
auto curry(F&& f) {
    return detail::curry(std::forward<F>(f), detail::priority_tag<1>{});
}
Run Code Online (Sandbox Code Playgroud)

和一个没有急切语义的替代实现,需要一个额外的()来调用部分应用程序,从而可以访问例如两个f(int)f(int, int)来自相同的重载集:

template <typename F>
class partial_application {
    F f_;
public:
    template <typename T>
    explicit partial_application(T&& f) :
        f_(std::forward<T>(f)) {}

    auto operator()() { return f_(); }

    template <typename T>
    auto operator()(T&&);
};

template <typename F>
auto curry_explicit(F&& f) {
    return partial_application<F>{std::forward<F>(f)};
}

template <typename F>
template <typename T>
auto partial_application<F>::operator()(T&& t) {
    return curry_explicit([f=f_,t=std::forward<T>(t)](auto&&...args) ->
      decltype(f_(t, std::forward<decltype(args)>(args)...)) {
        return f(t, std::forward<decltype(args)>(args)...);
    });
}
Run Code Online (Sandbox Code Playgroud)