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

Time:03-30

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?

CodePudding user response:

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.

CodePudding user response:

If you need to explicitly specify a return type for a lambda, it must be a trailing return type, since that’s the only way the grammar allows it.

[](int* p) -> int& { return *p; }  // OK 
int& [](int* p) { return *p; }     // ill-formed 

If the return type of a function template depends on the result of an expression involving values of the template parameter types, using trailing return types might make your code significantly less verbose.

// with trailing return type 
template <class T, class U> 
auto sum(T t, U u) -> decltype(t   u); 
// with leading return type 
template <class T, class U> 
decltype(std::declval<T>()   std::declval<U>()) sum(T t, U u); 

If the return type of a class member function is a member type of the class, using a trailing return type can avoid the need for redundant qualification.

class AReallyLongClassName { 
  // ... 
  class iterator { /* ... */ }; 
  iterator begin(); 
  // ... 
}; 
// ill-formed; `iterator` has not been declared 
iterator AReallyLongClassName::begin() { 
  // ... 
} 
// OK but verbose 
AReallyLongClassName::iterator AReallyLongClassName::begin() { 
  // ... 
} 
// OK, and much less verbose 
auto AReallyLongClassName::begin() -> iterator { 
  // ... 
} 

In fact, it appears that trailing return type syntax is superior to leading return type syntax in ease of use. I also like the fact that it makes C function declarations resemble function declarations in math ( f:V→R and such).

My personal recommendation is to use trailing return types as much as you want. Unfortunately, the Google C Style Guide mostly disallows them, citing the rather weak justifications of “some readers may find it unfamiliar” and “uniformity of style”.

  • Related