C++17 和使用移动捕获 lambda 表达式调用的异步成员函数

C++17 and asynchronous member functions calling with move capture lambda expression

I've asked, I've learned some of evaluation orders are well defined since C++17. postfix-expression such as a->f(...)and a.b(...) are the part of them. See 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

我认为这是安全的,因为后缀表达式 ->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.[=29 的成员=]

案例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的原子计数器操作没有发生。参见 但我还需要考虑到优势可以忽略,这取决于应用程序。

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

回答

感谢 Explorer_N 的评论。我得到了答案。

我问的是"Case1 is safe but Case2 and Case3 are unsafe is that rgiht?"。然而,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失败

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

这是一个不安全的代码:

#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 被移动,pointee 对象也会被保留。所以在 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
            }
        );
    }
);

你的问题可以大大简化为"is the following safe":

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

根据您的链接问题,the standard says

All side effects of argument evaluations are sequenced before the function is entered

其中一个 side-effect 是 some_object 中的 move - 所以这等同于:

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

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


利用这些知识:

  • 情况 1 可能会出现段错误并且绝对不安全,因为在 moved-from shared_ptr returns nullptr 上调用 operator->(),然后调用->async_func 上。
  • 只有在 moved-from some_type 上调用 async_func 是安全的,情况 2 才是安全的,但除非类型实际上没有定义移动构造函数。
  • 情况 3 不安全,因为虽然可以在取消引用后移动共享指针(因为移动共享指针不会更改它指向的对象),但 C++ 不保证首先评估哪个函数参数.