Search code examples
c++c++20curryingforwarding-reference

Currying in C++20 and (universal) references


Here is my implementation of currying using C++20:

#include <concepts>
#include <functional>

constexpr auto
curry(std::invocable auto f)
{
  return f();
}

constexpr auto
curry(auto f)
{
  return [=](auto x) { return curry(std::bind_front(f, x)); };
}

constexpr int
f(int a, int b, int c)
{
  return a * b * c;
}

constexpr auto
g(auto... args)
{
  return (1 * ... * args);
}

constexpr int
h()
{
  return 42;
}

int
main()
{
  static_assert(curry(f)(2)(3)(7) == 42);
  static_assert(curry(g<int, int, int, int>)(1)(2)(3)(7) == 42);
  static_assert(curry(h) == 42);
  static_assert(curry([](int n) { return n; })(42) == 42);
}

This code compiles with my installation of GCC 12.2.0 and Clang 15.0.2. Unfortunately, after addition of && to any of the functions f or g it no longer compiles:

constexpr int
f(int&& a, int&& b, int&& c) { /*...*/ }

constexpr auto
g(auto&&... args) { /*...*/ }

Could you please explain the reason of the error and suggest possible corrections?


Solution

  • The problem is here:

    constexpr auto
    curry(std::invocable auto f)
    {
      return f();
    }
    

    This function template isn't actually properly constrained. This is equivalent to:

    template <typename F> requires std::invocable<F>
    constexpr auto curry(F f) {
        return f();
    }
    

    std::invocable<F> is checking to see if F is callable with no arguments... but as an rvalue. But the body is invoking it as an lvalue - which isn't what you checked. Usually, there's not much of a difference between these two cases, so you can get away with writing the wrong constraint.

    But in this case, there very much is a difference. The binders forward their underlying arguments based on the value category of the object. f() passes the bound arguments as lvalues, std::move(f)() passes the bound arguments as rvalues. Since your function only accepts rvalues, f() in this case is invalid but std::move(f)() is fine.

    What you need to write instead is:

    template <typename F> requires std::invocable<F>
    constexpr auto curry(F&& f) {
        return std::forward<F>(f)();
    }
    

    And then the other overload needs to also take a forwarding reference to avoid being ambiguous.