misc-coroutine-hostile-raii¶
Detects when objects of certain hostile RAII types persists across suspension points in a coroutine. Such hostile types include scoped-lockable types and types belonging to a configurable denylist.
Some objects require that they be destroyed on the same thread that created
them. Traditionally this requirement was often phrased as “must be a local
variable”, under the assumption that local variables always work this way.
However this is incorrect with C++20 coroutines, since an intervening
co_await may cause the coroutine to suspend and later be resumed on
another thread.
The lifetime of an object that requires being destroyed on the same thread
must not encompass a co_await or co_yield point. If you create/destroy
an object, you must do so without allowing the coroutine to suspend in the
meantime.
Following types are considered as hostile:
Scoped-lockable types: A scoped-lockable object persisting across a suspension point is problematic as the lock held by this object could be unlocked by a different thread. This would be undefined behaviour. This includes all types annotated with the
scoped_lockableattribute.Types belonging to a configurable denylist.
// Call some async API while holding a lock.
task coro() {
const std::lock_guard l(&mu_);
// Oops! The async Bar function may finish on a different
// thread from the one that created the lock_guard (and called
// Mutex::Lock). After suspension, Mutex::Unlock will be called on the wrong thread.
co_await Bar();
}
Options¶
- RAIITypesList¶
A semicolon-separated list of qualified types which should not be allowed to persist across suspension points. Eg: my::lockable;a::b;::my::other::lockable The default value of this option is std::lock_guard;std::scoped_lock.
- AllowedAwaitablesList¶
A semicolon-separated list of qualified types of awaitables types which can be safely awaited while having hostile RAII objects in scope.
co_await-ing an expression ofawaitabletype is considered safe if theawaitabletype is part of this list. RAII objects persisting across such aco_awaitexpression are considered safe and hence are not flagged.Example usage:
// Consider option AllowedAwaitablesList = "safe_awaitable" struct safe_awaitable { bool await_ready() noexcept { return false; } void await_suspend(std::coroutine_handle<>) noexcept {} void await_resume() noexcept {} }; auto wait() { return safe_awaitable{}; } task coro() { // This persists across both the co_await's but is not flagged // because the awaitable is considered safe to await on. const std::lock_guard l(&mu_); co_await safe_awaitable{}; co_await wait(); }
Eg: my::safe::awaitable;other::awaitable Default is an empty string.
- AllowedCallees¶
A semicolon-separated list of callee function names which can be safely awaited while having hostile RAII objects in scope. Example usage:
// Consider option AllowedCallees = "noop" task noop() { co_return; } task coro() { // This persists across the co_await but is not flagged // because the awaitable is considered safe to await on. const std::lock_guard l(&mu_); co_await noop(); }
Eg: my::safe::await;other::await Default is an empty string.