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”.