c++lambdatrailing-return-type

Why is the trailing return type necessary in this lambda expression?


Consider the following code:

#include <iostream>
#include <type_traits>
#include <functional>
#include <utility>

template <class F>
constexpr decltype(auto) curry(F&& f)
{
    if constexpr (std::is_invocable_v<decltype(f)>)
    {
        return std::invoke(f);
    }
    else
    {
        return
        [f = std::forward<std::decay_t<F>>(f)]<typename Arg>(Arg&& arg) mutable -> decltype(auto)
        {
            return curry(
            [f = std::forward<std::decay_t<F>>(f), arg = std::forward<Arg>(arg)](auto&& ...args) mutable
            -> std::invoke_result_t<decltype(f), decltype(arg), decltype(args)...>     // #1
            {
                return std::invoke(f, arg, args...);
            });
        };
    }
}

constexpr int add(int a, int b, int c)
{
    return a + b + c;
}

constexpr int nullary()
{
    return 1;
}

void mod(int& a, int& b, int& c)
{
    a = 1;
    b = 2;
    c = 3;
}

int main()
{
    constexpr int u = curry(add)(1)(2)(3);
    constexpr int v = curry(nullary);
    std::cout << u << '\n' << v << std::endl;
    int i{}, j{}, k{};
    curry(mod)(std::ref(i))(std::ref(j))(std::ref(k));
    std::cout << i << ' ' << j << ' ' << k << std::endl;
}

With the trailing return type, the code can compile and outputs: (godbolt)

6
1
1 2 3

If you remove the seemingly redundant trailing return type (line #1 in the above code) and let the compiler to deduce the return type, however, the compiler starts compilaning that error C2672: 'invoke': no matching overloaded function found (godbolt), which surprise me.

So, why is the trailing return type necessary in this lambda expression?


Solution

  • If you explicitly give the return type and std::is_invocable_v<decltype(f), decltype(arg), decltype(args)...> is false SFINAE applies and a test for whether or not the lambda is callable will simply result in false.

    However without explicit return type, the return type will need to be deduced (in this case because of std::is_invocable_v<decltype(f)> being applied to the lambda) and in order to deduce the return type the body of the lambda needs to be instantiated.

    However, this also instantiates the expression

    return std::invoke(f, arg, args...);
    

    which is ill-formed if std::is_invocable_v<decltype(f), decltype(arg), decltype(args)...> is false. Since this substitution error appears in the definition rather than the declaration, it is not in the immediate context and therefore a hard error.