【问题标题】:How to write a generic forwarding lambda in C++14?如何在 C++14 中编写通用转发 lambda?
【发布时间】:2015-07-17 10:46:17
【问题描述】:

如何在 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); }

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

尝试#4

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

添加noexcept 是否会使这个 lambda 更适用,从而严格优于 #3?

【问题讨论】:

  • Try 1 已经足够了。如果数据类型定义了移动构造函数/移动赋值,编译器将为您移动它

标签: c++ lambda c++14 perfect-forwarding


【解决方案1】:

decltype(x) 的返回类型不足。

按值获取的局部变量和函数参数可以隐式移入返回值,但不能将右值引用获取的函数参数(x 是左值,即使 decltype(x) == 右值引用,如果您传递右值)。委员会给出的理由是,他们想确定当编译器隐式移动时,其他人不可能拥有它。这就是为什么我们可以从纯右值(一个临时的、非引用限定的返回值)和函数局部值转移。但是,有人可以做一些愚蠢的事情,比如

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

并且委员会不想默默地调用一个不安全的举动,认为他们应该从这个新的“举动”功能开始更加保守。请注意,在 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 &amp;&amp; 更好地证明了这个函数总是返回一个引用。它还避免了再次提及变量名称,我想这会使重命名变量稍微容易一些。

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

【讨论】:

  • clang 没有您描述的错误。如果您实际实例化模板函数,它会正确诊断错误。但是你的static_assert只检查compiles(0)的签名,错误不在签名中。
  • 其实GCC也没有这个bug。如果该函数实际上也被实例化,则您的示例被 GCC 正确拒绝。有一个密切相关的错误,但不会被您的测试程序暴露出来。
  • 是的,没有错误。 lambda 编译,您不能使用右值引用(在 gcc 或 clang 上)调用它。我只是检查返回类型,所以从未实际实例化实体。
  • 我已将错误报告视为无效而关闭。感谢您的调查
【解决方案2】:

您的前两次尝试不起作用,因为返回类型推导丢弃了顶级 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) 到右值引用 (@ 987654325@)。这在 gcc 和 clang 上都失败了。

v1 将始终返回一个左值引用。如果你用临时调用它,这会给你一个悬空引用。

v2v3 都是正确的通用转发 lambda。因此,您有三个用于尾随返回类型的选项(decltype(auto)auto&amp;&amp;decltype(x))和一个用于 lambda 主体的选项(调用 std::forward)。

【讨论】:

  • 这是错误的。 OP 在问题中指出这将失败是正确的:x 因为表达式是左值,如果返回类型是右值引用,则不能用作返回表达式。 (即使表达式x 是左值,decltype(x) 仍然可以是右值引用类型。)
  • @hvd OP 没有指出这一点,但是是的。帮助我确定 clang 和 gcc 是否都做了一些它必须正确的事情。用删除线更新了答案。
  • OP 把“在函数体内,x 是一个左值,所以这不起作用。”在问题中,但不可否认-&gt; decltype(x) 版本,所以是的,这有点令人困惑。 :) 无论如何,clang 确实正确地拒绝了它:[](auto&amp;&amp; x) -&gt; decltype(x) { return x; } (0); 失败并出现“错误:对类型'int'的右值引用不能绑定到类型'int'的左值”。 GCC 也这样做:“错误:无法将 'int' 左值绑定到 'int&&'”。
  • @hvd 嗯。我当时是如何测试的。我猜答案是“很糟糕”
【解决方案3】:

我能生成的字符最少但功能齐全的版本是:

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

这使用了一个我觉得有用的习语——在 lambda 中转发 auto&amp;&amp; 参数时,执行 decltype(arg)(arg)。如果您知道 arg 是引用类型,则通过 forward 转发 decltype 相对没有意义。

如果x 是值类型,decltype(x)(x) 实际上会生成x 的副本,而std::forward&lt;decltype(x)&gt;(x) 会生成对x 的右值引用。所以decltype(x)(x) 模式在一般情况下不如forward 有用:但这不是一般情况。

auto&amp;&amp; 将允许返回引用(匹配传入的引用)。遗憾的是,引用生命周期延长不适用于上述代码——我发现将右值引用转发到右值引用通常是错误的解决方案。

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);}

给你这种行为。左值引用变成左值引用,右值引用变成值。

【讨论】:

    猜你喜欢
    • 2013-06-18
    • 1970-01-01
    • 2015-09-30
    • 2014-10-26
    • 2010-10-30
    • 2014-08-23
    • 1970-01-01
    相关资源
    最近更新 更多