【问题标题】:Timing of lambda expression move capturelambda 表达式移动捕获的时序
【发布时间】:2019-07-19 08:12:22
【问题描述】:

当我使用 Boost.Asio 时,将 ip::tcp::socket 或 deadline_timer 等对象创建为 std::shared_ptr 并将其作为 lambda 表达式复制到完成处理程序。

我很好奇如果我使用移动捕获而不是复制捕获会发生什么。我认为这很危险。在下面的例子中,我认为tim = std::move(tim) 在tim->async_wait 之前被评估。所以 tim 不再有有效的指针。这是我的猜测。为了追踪std::shared_ptr 的行为,我创建了std::shared_ptr 包装器shared_ptr。

#include <iostream>

#include <boost/asio.hpp>

namespace as = boost::asio;

template <typename... Args>
struct shared_ptr : std::shared_ptr<Args...> {
    using base = std::shared_ptr<Args...>;
    using base::base; // inheriting constructor

    shared_ptr(shared_ptr&& other) : base(std::move(other)) {
        std::cout << "move" << std::endl;
    }
    typename base::element_type* operator->() {
        std::cout << "->" << std::endl;
        return base::get();
    }
};

int main() {
    as::io_context ioc;

    ioc.post( 
        [&] {
            shared_ptr<as::deadline_timer> tim(new as::deadline_timer(ioc));
            tim->expires_from_now(boost::posix_time::seconds(1));
            tim->async_wait( 
                // I think that it is dangerous because tim has been moved before tim->async_wait()
                [&, tim = std::move(tim)] 
                    std::cout << ec.message() << std::endl;
                }
            );
        }
    );

    ioc.run();
}

我在几个环境中运行代码:

所有选项都是-std=c++14

g++ 7.1.0 或更高版本:https://wandbox.org/permlink/rgJLp66vH7jKodQ8A

g++ 6.3.0:https://wandbox.org/permlink/XTIBhjJSqmkQHN4PB

clang++ 4.0.1~ : https://wandbox.org/permlink/nEZpFV874pKstjHAA

输出 A

->
->
move
move
move
move
Success

输出 B

->
move
->
Segmentation fault

clang++ 和 g++ 7.1.0 或更高版本似乎首先评估tim-&gt;async_wait()。

g++ 6.3.0 首先评估tim = std::move(tim)。

这是未定义的行为吗?还是在某个时候定义了评估顺序?

【问题讨论】:

  • 我目前没有标准的措辞,但我很确定这是未定义的行为,因为您的 tim-&gt;async_wait 调用作用于已被移动的对象。

标签: c++ c++14 sequence undefined-behavior evaluation


【解决方案1】:

C++17 中的求值顺序是明确定义的,例如 the expression leading to the function call (tim-&gt;async_wait) is sequenced before any of its arguments。

C++14 然而,由于lack of such sequencing rules,这是未指定的。也就是说,它可能有效,也可能无效,并且不需要实现告诉您它选择哪种方式,甚至不需要在一次调用到另一次调用时保持一致。

【讨论】:

  • 感谢您的回答。这意味着如果编译器完全支持 C++17,tim = std::move(tim) 是更好的选择。由于共享计数未更改,因此它比复制实现具有更高的性能(速度)。对吗?
  • @TakatoshiKondo:在这种情况下,性能有点无关紧要,因为无论如何您都将执行异步调度操作。我怀疑原子增量/减量是否会在性能方面产生差异。
  • 您的意思是您认为原子增量/减量通常会产生性能差异,但在这种情况下它不是主要因素。我理解正确吗?
  • 如果tim-&gt;async_wait 是一个成员函数,它在它的参数之前排序并不重要——它仍然会在move 之后使用预移动this 指针调用发生了。
  • @Eric:这与之前排序的工作方式无关。实际上,它需要在工作之前进行排序。问题是它是否在 之后排序。评估参数后,tim 不再具有有效指针。所以你必须在评估参数之前获取指针,否则事情就不起作用了。
猜你喜欢
  • 2015-03-04
  • 2018-03-15
  • 2015-11-23
  • 1970-01-01
相关资源
最近更新 更多