为什么此 lambda 表达式中需要尾随 return 类型?
Why is the trailing return type necessary in this lambda expression?
考虑以下代码:
#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;
}
尾随 return 类型,代码可以编译并输出:(godbolt)
6
1
1 2 3
如果去掉看似多余的尾随return类型(上面代码中第#1
行),让编译器推导出return类型,然而,编译器开始编译那
error C2672: 'invoke': no matching overloaded function found
(godbolt),这让我很惊讶。
那么,为什么在此 lambda 表达式中需要尾随 return 类型?
如果您需要为 lambda 显式指定 return 类型,它必须是尾随 return 类型,因为这是语法允许的唯一方式。
[](int* p) -> int& { return *p; } // OK
int& [](int* p) { return *p; } // ill-formed
如果函数模板的 return 类型取决于涉及模板参数类型值的表达式的结果,使用尾随 return 类型可能会使您的代码明显不那么冗长。
// 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);
如果class成员函数的return类型是class的成员类型,使用尾部return类型可以避免冗余限定的需要.
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 {
// ...
}
事实上,在易用性方面,尾随 return 类型语法似乎优于前导 return 类型语法。我也喜欢这样一个事实,它使 C++ 函数声明类似于数学中的函数声明(f:V→R 等)。
我个人的建议是尽可能多地使用尾随 return 类型。不幸的是,Google C++ 风格指南大多不允许使用它们,理由是“有些读者可能会觉得它不熟悉”和“风格统一”。
如果您明确给出 return 类型并且 std::is_invocable_v<decltype(f), decltype(arg), decltype(args)...>
是 false
SFINAE 应用并且测试 lambda 是否可调用将简单地导致 false
.
然而,如果没有明确的 return 类型,则需要推导 return 类型(在这种情况下是因为 std::is_invocable_v<decltype(f)>
被应用于 lambda)并且为了推导return 键入需要实例化的 lambda 主体。
然而,这也实例化了表达式
return std::invoke(f, arg, args...);
如果 std::is_invocable_v<decltype(f), decltype(arg), decltype(args)...>
是 false
,则 ill-formed。由于此替换错误出现在定义中而不是声明中,因此它不在直接上下文中,因此是一个硬错误。
考虑以下代码:
#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;
}
尾随 return 类型,代码可以编译并输出:(godbolt)
6
1
1 2 3
如果去掉看似多余的尾随return类型(上面代码中第#1
行),让编译器推导出return类型,然而,编译器开始编译那
error C2672: 'invoke': no matching overloaded function found
(godbolt),这让我很惊讶。
那么,为什么在此 lambda 表达式中需要尾随 return 类型?
如果您需要为 lambda 显式指定 return 类型,它必须是尾随 return 类型,因为这是语法允许的唯一方式。
[](int* p) -> int& { return *p; } // OK
int& [](int* p) { return *p; } // ill-formed
如果函数模板的 return 类型取决于涉及模板参数类型值的表达式的结果,使用尾随 return 类型可能会使您的代码明显不那么冗长。
// 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);
如果class成员函数的return类型是class的成员类型,使用尾部return类型可以避免冗余限定的需要.
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 {
// ...
}
事实上,在易用性方面,尾随 return 类型语法似乎优于前导 return 类型语法。我也喜欢这样一个事实,它使 C++ 函数声明类似于数学中的函数声明(f:V→R 等)。
我个人的建议是尽可能多地使用尾随 return 类型。不幸的是,Google C++ 风格指南大多不允许使用它们,理由是“有些读者可能会觉得它不熟悉”和“风格统一”。
如果您明确给出 return 类型并且 std::is_invocable_v<decltype(f), decltype(arg), decltype(args)...>
是 false
SFINAE 应用并且测试 lambda 是否可调用将简单地导致 false
.
然而,如果没有明确的 return 类型,则需要推导 return 类型(在这种情况下是因为 std::is_invocable_v<decltype(f)>
被应用于 lambda)并且为了推导return 键入需要实例化的 lambda 主体。
然而,这也实例化了表达式
return std::invoke(f, arg, args...);
如果 std::is_invocable_v<decltype(f), decltype(arg), decltype(args)...>
是 false
,则 ill-formed。由于此替换错误出现在定义中而不是声明中,因此它不在直接上下文中,因此是一个硬错误。