如何在C++14中编写通用的转发Lambda?

16

我该如何在C++14中编写一个通用的转发lambda函数?

尝试 #1

[](auto&& x) { return x; }

在函数主体中,x是一个左值,所以这并不起作用。

尝试 #2

[](auto&& x) { return std::forward<decltype(x)>(x); }

这个lambda函数内部正确地进行了引用传递,但它总是会返回按值(除非编译器省略了复制操作)。

尝试 #3

[](auto&& x) -> decltype(x) { return std::forward<decltype(x)>(x); }

这将返回与参数相同的类型(可能-> auto&&也可以正常工作),并且似乎可以正常工作。

第四次尝试

[](auto&& x) noexcept -> decltype(x) { return std::forward<decltype(x)>(x); }

添加 noexcept 是否会使得这个lambda表达式更适用,从而比#3严格更好呢?


尝试一次就足够了。如果数据类型有移动构造函数/移动赋值定义,编译器会为您移动它。 - madduci
3个回答

7
< p>使用decltype(x) 作为返回类型是不够的。

本地变量和按值传递的函数参数可以隐式地移动到返回值中,但是按rvalue引用取的函数参数不能(x 是lvalue,即使通过传递rvalue,decltype(x) == rvalue 引用)。委员会的理由是他们想确保在编译器隐式移动时,没有其他人可能拥有它。这就是为什么我们可以从prvalue中移动(临时对象,非参考限定返回值),也可以从函数局部值中移动。然而,有些人可能会做一些愚蠢的事情,例如

std::string str = "Hello, world!";
f(std::move(str));
std::cout << str << '\n';

委员会不想悄悄实施不安全的动作,他们认为应该从更保守的角度开始使用这个新的“move”功能。请注意,在C++20中,这个问题将得到解决,您只需做return x就可以了。详情请见http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0527r0.html 对于一个转发函数,我会尝试使noexcept正确。在这里很容易做到,因为我们只是处理引用(它是无条件的noexcept)。否则,您将破坏关心noexcept的代码。
这使最终理想的转发lambda看起来如下:
auto lambda = [](auto && x) noexcept -> auto && { return std::forward<decltype(x)>(x); };

decltype(auto)的返回类型在这里也能够做到同样的事情,但是auto &&更好地记录了这个函数总是返回一个引用。它还避免了再次提到变量名,我想这使得重命名变量稍微容易了一些。

从C++17开始,转发lambda在可能的情况下也会隐式地成为constexpr。


1
clang没有你描述的那个bug。如果你实例化模板函数,它会正确地诊断错误。但是你的static_assert只检查了compiles(0)的签名,而错误不在签名中。 - user743382
1
实际上,GCC也没有这个错误。如果函数确实被实例化,您的示例将被GCC正确拒绝。存在一个密切相关的错误,但不会被您的测试程序暴露出来。 - user743382
1
是的,没有 bug。Lambda 编译通过了,只是你不能使用右值引用来调用它(无论是在 GCC 还是 Clang 上)。我之前只检查了返回类型,所以从未实例化过函数体。 - Barry
1
我已将错误报告标记为无效。感谢您的关注。 - David Stone

6
你的前两个尝试都行不通,因为返回类型推断会丢弃顶层cv限定符和引用,这使得通用转发成为不可能。你的第三个尝试是完全正确的,只是有点啰嗦,因为强制转换在返回类型中是隐含的。而且,noexcept与转发无关。
以下是几个值得完整讨论的其他选项:
auto v0 = [](auto&& x) -> decltype(x) { return x; };
auto v1 = [](auto&& x) -> auto&& { return x; };
auto v2 = [](auto&& x) -> auto&& { return std::forward<decltype(x)>(x); };
auto v3 = [](auto&& x) -> decltype(auto) { return std::forward<decltype(x)>(x); };
v0将编译并且看起来返回了正确的类型,但是如果你使用一个右值引用调用它,则会在编译时失败,因为需要将左值引用(x)隐式转换为右值引用(decltype(x))。在gcc和clang上都会失败。

v1总是会返回一个左值引用。如果使用临时对象调用它,则会得到一个悬空引用。

v2v3都是正确的泛型转发lambda表达式。因此,在你的尾随返回类型中有三个选项(decltype(auto), auto&&decltype(x)),在lambda表达式主体中只有一种选择(调用std::forward)。


1
这是错误的。OP在问题中指出这将失败是正确的:x作为一个表达式是一个lvalue,如果返回类型是一个rvalue引用,它不能被用作返回表达式。(即使表达式x是一个lvalue,decltype(x)仍然可以是一个rvalue引用类型。) - user743382
@hvd 没有指出这一点,但是没错。如果clang和gcc都做某件事情,那么我就想当然地认为它一定是正确的。我已经用横线更新了答案。 - Barry
OP在问题中写道:“在函数体内,x是一个lvalue,所以这样不行。”但是对于-> decltype(x)版本,他承认自己没有这么写,所以有点令人困惑。无论如何,clang确实拒绝了它:[](auto&& x) -> decltype(x) { return x; } (0);会报错“error: rvalue reference to type 'int' cannot bind to lvalue of type 'int'”。GCC也一样:“error: cannot bind 'int' lvalue to 'int&&'”。 - user743382
1
@hvd 呃,那我怎么测试这个呢?我想答案是“糟糕”。 - Barry

2
我能生成最简洁但功能齐全的版本是:
[](auto&&x)noexcept->auto&&{return decltype(x)(x);}

这里用到了一个我认为很有用的习惯用语 -- 当在lambda中转发auto&&参数时,使用decltype(arg)(arg)。如果你知道arg是引用类型,那么通过forward转发decltype相对无意义。
如果x是值类型,decltype(x)(x)实际上会产生x的副本,而std::forward<decltype(x)>(x)则会产生x的右值引用。因此,在一般情况下,decltype(x)(x)模式比forward要少用:但这并不是常规情况。 auto&&将允许返回引用(匹配传入引用)。不幸的是,参考生命周期扩展在上述代码中不能正常工作--我发现将右值引用转发到右值引用通常不是正确的解决方案。
template<class T>struct tag{using type=T;};
template<class Tag>using type=typename Tag::type;
template<class T> struct R_t:tag<T>{};
template<class T> struct R_t<T&>:tag<T&>{};
template<class T> struct R_t<T&&>:tag<T>{};
template<class T>using R=type<R_t<T>>;

[](auto&&x)noexcept->R<decltype(x)>{return decltype(x)(x);}

这个行为会改变引用的类型。左值引用仍然是左值引用,右值引用则变成了值。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接