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
}
);
}
);
我认为这是安全的,因为后缀表达式 ->
在 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_func
和 other_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++ 不保证首先评估哪个函数参数.
在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
}
);
}
);
我认为这是安全的,因为后缀表达式 ->
在 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_func
和 other_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
returnsnullptr
上调用operator->()
,然后调用->async_func
上。 - 只有在 moved-from
some_type
上调用async_func
是安全的,情况 2 才是安全的,但除非类型实际上没有定义移动构造函数。 - 情况 3 不安全,因为虽然可以在取消引用后移动共享指针(因为移动共享指针不会更改它指向的对象),但 C++ 不保证首先评估哪个函数参数.