I've read this Q&A. Lambda lifetime explanation for C++20 coroutines
If I understand correctly, lambda expression with capture something is not safe if the lambda expression is used as coroutine function.
Now I come up with the following question.
#include <iostream>
#include <boost/asio.hpp>
#include <boost/asio/experimental/co_composed.hpp>
namespace asio = boost::asio;
struct foo {
template <typename CompletionToken>
auto mf1(
CompletionToken&& token
) {
return asio::async_initiate<
CompletionToken,
void()
>(
asio::experimental::co_composed<
void()
>(
[]( // capture nothing
auto /*state*/,
foo& self
) -> void {
// co_await some_async_function(asio::deferred);
self.val_++; // accesses member variables via self
co_return {};
}
),
token,
std::ref(*this)
);
}
template <typename CompletionToken>
auto mf2(
CompletionToken&& token
) {
return asio::async_initiate<
CompletionToken,
void()
>(
asio::experimental::co_composed<
void()
>(
[this]( // capture this.
auto /*state*/
) -> void {
// co_await some_async_function(asio::deferred);
val_++; // accesses member variables
co_return {};
}
),
token
);
}
int val_ = 0;
};
asio::awaitable<void> proc() {
foo f;
co_await f.mf1(asio::deferred);
co_await f.mf2(asio::deferred);
std::cout << f.val_ << std::endl;
co_return;
}
int main() {
asio::io_context ioc;
asio::co_spawn(
ioc.get_executor(),
proc(),
asio::detached
);
ioc.run();
}
godbolt link: https://godbolt.org/z/d7snvYYqb
class foo
has two member functions. Both member functions do the same thing. They are async function implemented by co_composed
.
mf1()
's lambda expression captures nothing, and passed *this
as self
. val_
is accessed through self
. I think that it is safe.
mf2()
's lambda expression captures this pointer. val_
is accessed through this pointer. Is it safe?
Before accessing val_
, co_await could be placed, so the context could be switched and switched back. In this case, captured this pointer is treated well?
I suspect that it could be unsafe.
What makes it safe to use a reference? The reference must point to a valid object of the expected type.
When would that cease to be the case? When the lifetime of the referred-to object ends, or when the object is otherwise invalidated (e.g. by being moved from).
In your code none of the two are applicable. foo
lives in Asio's coro frame, which is stable.
Your capture is safe.
It's critical to observe here that this
refers to the foo
instance, not the lambda. The lambda instance is likely moved along the async call chain, so can not be considered stable. This might be relevant when you have value-captures and accidentally pass references to them around. Luckily you would probably not have a need for much state in the lambda because the foo
instance basically
That Side-tangent proves to be prophetic. In the comments we analyzed down to this case:
#include <boost/asio.hpp>
namespace asio = boost::asio;
int main() {
asio::io_context ioc;
auto bar = [v = 42]() -> asio::awaitable<void> { assert(v); co_return; };
co_spawn(ioc, bar, asio::detached); // fine
co_spawn(ioc, bar(), asio::detached); // fine
co_spawn(ioc,
[v = 42]() -> asio::awaitable<void> { assert(v); co_return; },
asio::detached); // fine
// OOPS: this one is broken
co_spawn(ioc,
[p = 42] -> asio::awaitable<void> { assert(p); co_return; }(),
asio::detached);
ioc.run();
}
You'll note that when the lambda is a temporary, it leads to an awaitable<>
with a dangling reference to the lambda instance. This makes it a tiny bit clearer, maybe:
// OOPS: this one is broken
// aw stores a reference to a temporary lambda instance
asio::awaitable<void> aw = [p = 42] -> asio::awaitable<void> { assert(p); co_return; }();
// the lambda instance is gone, but the coroutine is going to invoke its `operator()() const` still:
co_spawn(ioc, std::move(aw), asio::detached);
It's extremely subtle, and you can avoid it by not invoking the lambda, but instead passing it by value.