【问题标题】:C++17 and asynchronous member functions calling with move capture lambda expression使用移动捕获 lambda 表达式调用 C++17 和异步成员函数
【发布时间】:2019-11-29 22:07:50
【问题描述】:

the other question 中,我问过,我了解到一些评估顺序自 C++17 以来就已明确定义。诸如a->f(...)a.b(...)之类的后缀表达式是其中的一部分。见https://timsong-cpp.github.io/cppwp/n4659/expr.call#5

Boost.Asio中,以下风格的异步成员函数调用是典型的模式。

auto sp_object = std::make_shared<object>(...);
sp_object->async_func(
    params,
    [sp_object]
    (boost::syste_error_code const&e, ...) {
        if (e) return;
        sp_object->other_async_func(
            params,
            [sp_object]
            (boost::syste_error_code const&e, ...) {
                if (e) return;
                // do some
            }
        );
    }
);

我想澄清以下三种情况的安全性。

案例 1:shared_ptr 移动和成员函数

auto sp_object = std::make_shared<object>(...);
sp_object->async_func(
    params,
    [sp_object = std::move(sp_object)]
    (boost::syste_error_code const&e, ...)  mutable { // mutable is for move
        if (e) return;
        sp_object->other_async_func(
            params,
            [sp_object = std::move(sp_object)]
            (boost::syste_error_code const&e, ...) {
                if (e) return;
                // do some
            }
        );
    }
);

这个模式就像https://www.boost.org/doc/libs/1_70_0/doc/html/boost_asio/reference/basic_stream_socket/async_read_some.html

我认为这是安全的,因为后缀表达式 -&gt;sp_object = std::move(sp_object) 之前被评估。

案例2:价值移动和成员函数

some_type object(...);
object.async_func(
    params,
    [object = std::move(object)]
    (boost::syste_error_code const&e, ...)  mutable { // mutable is for move
        if (e) return;
        object.other_async_func(
            params,
            [object = std::move(object)]
            (boost::syste_error_code const&e, ...) {
                if (e) return;
                // do some
            }
        );
    }
);

我认为这是危险的,因为即使在 object = std::move(object) 之前评估后缀表达式 .async_func 也可以访问 object 的成员。

案例 3:shared_ptr 移动和释放函数

auto sp_object = std::make_shared<object>(...);
async_func(
    *sp_object,
    params,
    [sp_object = std::move(sp_object)]
    (boost::syste_error_code const&e, ...)  mutable { // mutable is for move
        if (e) return;
        other_async_func(
            *sp_object,
            params,
            [sp_object = std::move(sp_object)]
            (boost::syste_error_code const&e, ...) {
                if (e) return;
                // do some
            }
        );
    }
);

这个模式就像https://www.boost.org/doc/libs/1_70_0/doc/html/boost_asio/reference/async_read/overload1.html

我认为这很危险,因为没有后缀表达式。因此,sp_object 可以通过第三个参数移动捕获移动,然后通过第一个参数取消引用为 *sp_object

结论

只有 case1 是安全的,其他是危险的(未定义的行为)。 我需要小心它在 C++14 和更旧的编译器上是不安全的。 它可以加快调用异步成员函数,因为shared_ptr 的原子计数器操作没有发生。见Why would I std::move an std::shared_ptr? 但我也需要考虑到优势可以忽略不计,这取决于应用程序。

我对 C++17 求值顺序变化(精确定义)和异步操作关系的理解是否正确?

【问题讨论】:

  • 首先,为什么你认为Case:1 是安全的?虽然sp_object-&gt;async_funcsp_object = std::move(sp_object) 之前进行了评估....是否未定义完全取决于async_func 的定义,您认为async_func 访问shared_from_this() 会发生什么?
  • 因为sp_object = std::move(sp_object) 保持相同的指针对象。移至sp_object.get() 返回this 的相同地址(对象)为async_func()。所以我相信shared_from_this() 工作正常。这是工作示例wandbox.org/permlink/NooUkn4SUSAOPLDU
  • 在移动 shared_ptr 的情况下,std 必须说 "10) 从 r 移动构造一个 shared_ptr。构造之后,*this 包含之前状态的副本r, r 为空且其存储的指针为 null。如果 Y* 不能隐式转换为 (C++17 前)与 (C++17 起) T* 兼容,则模板重载不参与重载决议."....所以不确定您所看到的是否明确。
  • 我认为timsong-cpp.github.io/cppwp/n4659/expr.call#5的意思是sp_object-&gt;被替换为sp_object.get()的地址。假设地址是addr_object。这意味着它被替换为addr_object-&gt;async_read。替换后,sp_object 变为空。不过没问题。
  • 稍微改变了你的例子(对我来说,它的行为取决于我们调用的函数的作用)wandbox.org/permlink/Tv9pbhvls2ZkHJE7,如果我错过了什么,请告诉我。得到这个“以 std::__1::bad_weak_ptr: bad_weak_ptr 类型的未捕获异常终止”

标签: c++ boost c++17 move asio


【解决方案1】:

回答

感谢 Explorer_N 的 cmets。我得到了答案。

我问“Case1 是安全的,但 Case2 和 Case3 是不安全的,对吗?”。然而,Case1 是安全的当且仅当我稍后编写的约束 (*1) 得到满足。这意味着 Case1 通常是不安全的

这取决于async_func()

这是一个不安全的案例:

#include <iostream>
#include <memory>
#include <boost/asio.hpp>

struct object : std::enable_shared_from_this<object> {
    object(boost::asio::io_context& ioc):ioc(ioc) {
        std::cout << "object constructor this: " << this << std::endl;
    }

    template <typename Handler>
    void async_func(Handler&& h) {
        std::cout << "this in async_func: " << this << std::endl;
        h(123); // how about here?
        std::cout << "call shared_from_this in async_func: " << this << std::endl;
        auto sp = shared_from_this();
        std::cout << "sp->get() in async_func: " << sp.get() << std::endl;
    }

    template <typename Handler>
    void other_async_func(Handler&& h) {
        std::cout << "this in other_async_func: " << this << std::endl;
        h(123); // how about here?
        std::cout << "call shared_from_this in other_async_func: " << this << std::endl;
        auto sp = shared_from_this();
        std::cout << "sp->get() in other_async_func: " << sp.get() << std::endl;
    }

    boost::asio::io_context& ioc;
};

int main() {
    boost::asio::io_context ioc;
    auto sp_object = std::make_shared<object>(ioc);

    sp_object->async_func(
        [sp_object = std::move(sp_object)]
        (int v) mutable { // mutable is for move
            std::cout << v << std::endl;
            sp_object->other_async_func(
                [sp_object = std::move(sp_object)]
                (int v) {
                    std::cout << v << std::endl;
                }
            );
        }
    );
    ioc.run();
}

运行演示https://wandbox.org/permlink/uk74ACox5EEvt14o

我考虑了为什么第一个shared_from_this() 没问题,但第二个调用在上面的代码中抛出std::bad_weak_ptr。这是因为回调处理程序是直接从async_funcother_async_func 调用的。移动发生两次。这样第一级(async_func)shared_from_this就失败了。

即使不直接从异步函数调用回调处理程序,它在多线程情况下仍然不安全。

这是一个不安全的代码:

#include <iostream>
#include <memory>
#include <boost/asio.hpp>

struct object : std::enable_shared_from_this<object> {
    object(boost::asio::io_context& ioc):ioc(ioc) {
        std::cout << "object constructor this: " << this << std::endl;
    }

    template <typename Handler>
    void async_func(Handler&& h) {
        std::cout << "this in async_func: " << this << std::endl;

        ioc.post(
            [this, h = std::forward<Handler>(h)] () mutable {
                h(123);
                sleep(1);
                auto sp = shared_from_this();
                std::cout << "sp->get() in async_func: " << sp.get() << std::endl;
            }
        );
    }

    template <typename Handler>
    void other_async_func(Handler&& h) {
        std::cout << "this in other_async_func: " << this << std::endl;

        ioc.post(
            [this, h = std::forward<Handler>(h)] () {
                h(456);
                auto sp = shared_from_this();
                std::cout << "sp->get() in other_async_func: " << sp.get() << std::endl;
            }
        );
    }

    boost::asio::io_context& ioc;
};

int main() {
    boost::asio::io_context ioc;
    auto sp_object = std::make_shared<object>(ioc);

    sp_object->async_func(
        [sp_object = std::move(sp_object)]
        (int v) mutable { // mutable is for move
            std::cout << v << std::endl;
            sp_object->other_async_func(
                [sp_object = std::move(sp_object)]
                (int v) {
                    std::cout << v << std::endl;
                }
            );
        }
    );
    std::vector<std::thread> ths;
    ths.reserve(2);
    for (std::size_t i = 0; i != 2; ++i) {
        ths.emplace_back(
            [&ioc] {
                ioc.run();
            }
        );
    }
    for (auto& t : ths) t.join();
}

运行演示:https://wandbox.org/permlink/xjLZWoLdn8xL89QJ

case1 的约束是安全的

*1 但是,在 case1 中,当且仅当 struct object 不期望它被 shared_ptr 持有时,它是安全的。也就是说,只要struct object不使用shared_from_this机制,就是安全的。

另一种控制顺序的方法。 (支持 C++14)

当且仅当满足上述约束时,我们可以在没有 C++17 序列定义的情况下控制评估序列。 它同时支持 case1 和 case3。只需获取由 shared_ptr 持有的指针对象的引用。关键是即使 shared_ptr 被移动,指针对象也会被保留。所以在shared_ptr移动之前获取pointee对象的引用,然后shared_ptr被移动,pointee对象不受影响。

但是,shared_from_this 是例外情况。它直接使用 shared_ptr 机制。所以这会受到 shared_ptr 移动的影响。因此它是不安全的。这就是约束的原因。

案例1

// The class of sp_object class doesn't use shared_from_this mechanism
auto sp_object = std::make_shared<object>(...);
auto& r = *sp_object;
r.async_func(
    params,
    [sp_object]
    (boost::syste_error_code const&e, ...) {
        if (e) return;
        auto& r = *sp_object;
        r.other_async_func(
            params,
            [sp_object]
            (boost::syste_error_code const&e, ...) {
                if (e) return;
                // do some
            }
        );
    }
);

案例3

// The class of sp_object class doesn't use shared_from_this mechanism
auto sp_object = std::make_shared<object>(...);
auto& r = *sp_object;
async_func(
    r,
    params,
    [sp_object = std::move(sp_object)]
    (boost::syste_error_code const&e, ...)  mutable { // mutable is for move
        if (e) return;
        auto& r = *sp_object;
        other_async_func(
            r,
            params,
            [sp_object = std::move(sp_object)]
            (boost::syste_error_code const&e, ...) {
                if (e) return;
                // do some
            }
        );
    }
);

【讨论】:

    【解决方案2】:

    您的问题可以大大简化为“以下是否安全”:

    some_object.foo([bound_object = std::move(some_object)]() {
        bound_object.bar()
    });
    

    来自您的链接问题,the standard says

    参数求值的所有副作用在输入函数之前排序

    其中一个副作用是来自 some_object 的 move - 所以这相当于:

    auto callback = [bound_object = std::move(some_object)]() {
        bound_object.bar()
    }
    some_object.foo(std::move(callback));
    

    很明显,在调用foo 方法之前,这会移出some_object。当且仅当在移出对象上调用 foo 时这是安全的。


    运用这些知识:

    • 案例 1 可能会出现段错误,并且绝对不安全,因为在已移动的 shared_ptr 上调用 operator-&gt;() 会返回 nullptr,然后您在其上调用 -&gt;async_func
    • 仅当在已移动的 some_type 上调用 async_func 是安全的,情况 2 才是安全的,但除非该类型实际上没有定义移动构造函数,否则它不太可能达到您的预期。
    • 案例 3 不安全,因为虽然可以在取消引用后移动共享指针(因为移动共享指针不会更改它指向的对象),但 C++ 不保证首先计算哪个函数参数。

    【讨论】:

    • 你的意思是第一个代码和第二个代码是等价的吗?
    • 已更新以明确涵盖您的案例
    • @TakatoshiKondo 你可以接受这个作为答案,它清楚地表明评估是在执行之前进行的,对我来说这听起来很关键。
    • 在您的第一个示例中,foo() 被调用并从对象移动。这是正在运行的代码:wandbox.org/permlink/tDxm3LMiVyzAeT9J 如果 foo() 从对象移动调用时不安全,则它是不安全的。我同意这一点。这是我的问题的案例2。在 case1 中,some_object 是 shared_ptr。请参阅wandbox.org/permlink/domM8hS1JvuxqUs0 当且仅当 some_class 不期望“我被 shared_ptr 持有”时,我认为这是安全的。我认为具有上述约束的 case1 是安全的。这就是我的观点。
    • @TakatoshiKondo:你说得对,基于这种推理,案例 1 看起来毕竟是安全的
    猜你喜欢
    • 1970-01-01
    • 2015-03-04
    • 1970-01-01
    • 1970-01-01
    • 2011-12-15
    • 2017-05-28
    • 2014-03-20
    • 2021-01-07
    • 1970-01-01
    相关资源
    最近更新 更多