【问题标题】:How can a std::reference_wrapper to a rvalue lambda work?右值 lambda 的 std::reference_wrapper 如何工作?
【发布时间】:2013-11-30 21:15:09
【问题描述】:

this article 中,它说以下代码是有效的 C++11 并且可以与 GNU 的 libstdc++ 一起使用:

int n;
std::vector<int> v;
...
std::function<bool(int)> f(std::cref([n](int i) {return i%n == 0));
std::count_if(v.begin(), v.end(), f);

问题是我一直相信 lambda 对象是在调用站点创建的,是什么使它成为这个 sn-p 中的临时对象,因为它没有存储在任何变量上,而是一个 const 引用它正在被创建并传递给std::function。如果是这样,lambda 对象应该一直被销毁,在f 中留下一个悬空引用,这将导致std::count_if 使用时未定义的行为。

假设文章没有错,我的心智模型有什么问题? lambda对象什么时候被销毁?

【问题讨论】:

  • 在我看来无效。
  • [expr.prim.lambda]/2 "对 lambda 表达式的计算会产生一个临时的纯右值。"在这种情况下,它甚至不能转换为函数指针,因为它的生命周期可能更长。

标签: c++ c++11 lambda


【解决方案1】:

好的,让我们从基础开始:上面的代码肯定不合法,因为它在一些相当基本的方面是错误的。线

std::function<bool(int)> f(std::cref([n](int i) {return i%n == 0));

至少需要写成

std::function<bool(int)> f(std::cref([n](int i) {return i%n == 0;}));

请注意,代码是在 Dr.Dobb 的文章中写的,因为它在问题中,即任何关于代码合法的声明都已经很值得怀疑了。

一旦简单的语法错误得到解决,下一个问题是std::cref() 是否真的可以用于绑定到右值。根据 5.1.2 [expr.prim.lambda] 第 2 段,lambda exrpession 显然是临时的(感谢 DyP 的参考)。由于将引用绑定到临时对象通常是一个相当糟糕的主意,并且在其他地方被禁止,std::cref() 将是规避此限制的一种方法。事实证明,根据 20.10 [function.objects] 第 2 段 std::cref() 被声明为

template <class T> reference_wrapper<const T> cref(const T&) noexcept;
template <class T> void cref(const T&&) = delete;
template <class T> reference_wrapper<const T> cref(reference_wrapper<T>) noexcept;

也就是说,即使更正了语法错误,语句也不正确。 gccclang 都没有编译这段代码(我使用了这两个编译器的最新版本以及它们各自的标准 C++ 库)。也就是说,根据上面的声明,这段代码显然是非法的!

最后,在上面的表达式中没有什么可以延长临时的寿命。临时对象的生命周期延长的唯一原因是它或其数据成员之一立即绑定到 [const] 引用。围绕临时包裹一个函数调用会禁止此生命周期扩展。

综上所述:文中引用的代码在很多不同层面都是不合法的!

【讨论】:

    【解决方案2】:

    我是上述文章的作者,我为我的错误道歉。在这种情况下,没有人应该受到责备。我只是要求编辑添加勘误表:

    1) 替换

    std::count_if(v.begin(), v.end(), std::cref(is_multiple_of(n)));
    

    is_multiple_of f(n);
    std::count_if(v.begin(), v.end(), std::cref(f));
    

    2) 替换

    std::count_if(v.begin(), v.end(), std::cref([n](int i){return i%n == 0;}));
    

    auto f([n](int i){return i%n == 0;});
    std::count_if(v.begin(), v.end(), std::cref(f));
    

    3) 替换

    std::function<bool(int)> f(std::cref([n](int i) {return i%n == 0));
    

    auto f1([n](int i){return i%n == 0;});
    std::function<bool(int)> f(std::cref(f1));
    

    在所有情况下,问题都是一样的(Dietmar Kühl 很好地解释了,+1 给他):我们暂时打电话给std::cref。这个函数返回一个std::reference_wrapper 存储一个指向临时的指针,如果std::reference_wrapper 比临时的寿命长,这个指针将悬空。基本上这就是上面案例 3 中发生的情况(其中也包含一个错字)。

    在第 1 和第 2 种情况下,std::reference_wrapper 的寿命不会超过临时值。但是,由于std::cref 接受临时变量(右值)的重载被删除,因此代码不应编译(包括案例 3)。在发布时,这些实施并未像今天那样与标准保持同步。用于编译的代码,但在与标准库的较新实现一起使用时不会。不过,这不是我犯错的借口。

    无论如何,我相信本文的主要观点,即使用std::reference_wrapperstd::crefstd::ref 来避免昂贵的副本和动态分配仍然有效,当然前提是被引用对象的生命周期足够长。

    再次,对于给您带来的不便,我深表歉意。

    更新:文章已修复。感谢 uk4321、DyP,尤其是 lvella 和 Dietmar Kühl 提出和讨论这个问题。

    【讨论】:

    • 如果我理解正确,问题是你错误地传递了 lambda,要么不先存储它,然后再使用它(所以它充当右值,将在调用站点删除) .此外,重点是表明可以通过 const 引用将 lambda 传递给算法,例如 count_if 充当 UnaryPredicate 以避免昂贵的副本?
    • @Nik-Lz 1) 是的。初始版本错误地将 lambda 作为右值传递给cref,该右值将在创建reference_wrapper 后立即销毁。因此,后者将指代一个死对象。 (谢天谢地,这不再编译。) 2)是的。 cref/reference_wrapper 是一种避免昂贵的 lambda 副本的方法。 reference_wrapper(持有对 lambda 的“引用”)被复制,这很便宜。这也避免了堆分配,因为reference_wrapper 小到可以容纳在function 对象内(小对象优化)。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-07-04
    • 2013-08-10
    • 2020-02-09
    • 2015-07-27
    • 1970-01-01
    相关资源
    最近更新 更多