[clang-tools-extra] [clang-tidy] Add `AllowExplicitObjectParameters` option to `avoid-capturing-lambda-coroutines` (PR #182916)
Willem Kaufmann via cfe-commits
cfe-commits at lists.llvm.org
Fri Apr 3 05:20:17 PDT 2026
https://github.com/WillemKauf updated https://github.com/llvm/llvm-project/pull/182916
>From 9d36e0153a4d22b5604d6e127c0b8268c7de3be2 Mon Sep 17 00:00:00 2001
From: Willem Kaufmann <willem.kaufmann at gmail.com>
Date: Mon, 30 Mar 2026 11:15:37 -0400
Subject: [PATCH] [clang-tidy] Add `AllowExplicitObjectParameters` option to
`avoid-capturing-lambda-coroutines`
Add an off-by-default `AllowExplicitObjectParameters` option to the
existing `cppcoreguidelines-avoid-capturing-lambda-coroutines` check.
When enabled, lambda coroutines that use C++23 "deducing this" (explicit
object parameter) are not flagged, since captures are moved into the
coroutine frame ([1], [2], [3]).
The option is off by default to match the current C++ Core Guidelines,
which do not yet recognize explicit object parameters as a solution ([4]).
Once the guidelines adopt the proposal, the default can be flipped.
[1]: https://github.com/scylladb/seastar/blob/master/doc/lambda-coroutine-fiasco.md#solution-c23-and-up
[2]: https://www.scs.stanford.edu/~dm/blog/vexing-capture.html
[3]: https://lists.isocpp.org/std-proposals/2020/05/1391.php
[4]: https://github.com/isocpp/CppCoreGuidelines/pull/2289#issuecomment-3756500251
---
.../AvoidCapturingLambdaCoroutinesCheck.cpp | 25 ++++-
.../AvoidCapturingLambdaCoroutinesCheck.h | 8 +-
clang-tools-extra/docs/ReleaseNotes.rst | 6 ++
.../avoid-capturing-lambda-coroutines.rst | 29 ++++++
...tines-allow-explicit-object-parameters.cpp | 93 +++++++++++++++++++
.../avoid-capturing-lambda-coroutines.cpp | 8 ++
6 files changed, 166 insertions(+), 3 deletions(-)
create mode 100644 clang-tools-extra/test/clang-tidy/checkers/cppcoreguidelines/avoid-capturing-lambda-coroutines-allow-explicit-object-parameters.cpp
diff --git a/clang-tools-extra/clang-tidy/cppcoreguidelines/AvoidCapturingLambdaCoroutinesCheck.cpp b/clang-tools-extra/clang-tidy/cppcoreguidelines/AvoidCapturingLambdaCoroutinesCheck.cpp
index 618554663ab91..8726ba0508dfd 100644
--- a/clang-tools-extra/clang-tidy/cppcoreguidelines/AvoidCapturingLambdaCoroutinesCheck.cpp
+++ b/clang-tools-extra/clang-tidy/cppcoreguidelines/AvoidCapturingLambdaCoroutinesCheck.cpp
@@ -21,12 +21,29 @@ AST_MATCHER(LambdaExpr, hasCoroutineBody) {
}
AST_MATCHER(LambdaExpr, hasCaptures) { return Node.capture_size() != 0U; }
+
+AST_MATCHER(LambdaExpr, hasDeducingThis) {
+ return Node.getCallOperator()->isExplicitObjectMemberFunction();
+}
} // namespace
+AvoidCapturingLambdaCoroutinesCheck::AvoidCapturingLambdaCoroutinesCheck(
+ StringRef Name, ClangTidyContext *Context)
+ : ClangTidyCheck(Name, Context),
+ AllowExplicitObjectParameters(
+ Options.get("AllowExplicitObjectParameters", false)) {}
+
void AvoidCapturingLambdaCoroutinesCheck::registerMatchers(
MatchFinder *Finder) {
+ using LambdaExprMatcher = ast_matchers::internal::Matcher<LambdaExpr>;
+ const auto ExplicitObjectFilter =
+ AllowExplicitObjectParameters
+ ? LambdaExprMatcher(unless(hasDeducingThis()))
+ : LambdaExprMatcher(anything());
Finder->addMatcher(
- lambdaExpr(hasCaptures(), hasCoroutineBody()).bind("lambda"), this);
+ lambdaExpr(hasCaptures(), hasCoroutineBody(), ExplicitObjectFilter)
+ .bind("lambda"),
+ this);
}
bool AvoidCapturingLambdaCoroutinesCheck::isLanguageVersionSupported(
@@ -34,6 +51,12 @@ bool AvoidCapturingLambdaCoroutinesCheck::isLanguageVersionSupported(
return LangOpts.CPlusPlus20;
}
+void AvoidCapturingLambdaCoroutinesCheck::storeOptions(
+ ClangTidyOptions::OptionMap &Opts) {
+ Options.store(Opts, "AllowExplicitObjectParameters",
+ AllowExplicitObjectParameters);
+}
+
void AvoidCapturingLambdaCoroutinesCheck::check(
const MatchFinder::MatchResult &Result) {
const auto *MatchedLambda = Result.Nodes.getNodeAs<LambdaExpr>("lambda");
diff --git a/clang-tools-extra/clang-tidy/cppcoreguidelines/AvoidCapturingLambdaCoroutinesCheck.h b/clang-tools-extra/clang-tidy/cppcoreguidelines/AvoidCapturingLambdaCoroutinesCheck.h
index de59ff189c595..72e5ff3168719 100644
--- a/clang-tools-extra/clang-tidy/cppcoreguidelines/AvoidCapturingLambdaCoroutinesCheck.h
+++ b/clang-tools-extra/clang-tidy/cppcoreguidelines/AvoidCapturingLambdaCoroutinesCheck.h
@@ -21,11 +21,15 @@ namespace clang::tidy::cppcoreguidelines {
/// https://clang.llvm.org/extra/clang-tidy/checks/cppcoreguidelines/avoid-capturing-lambda-coroutines.html
class AvoidCapturingLambdaCoroutinesCheck : public ClangTidyCheck {
public:
- AvoidCapturingLambdaCoroutinesCheck(StringRef Name, ClangTidyContext *Context)
- : ClangTidyCheck(Name, Context) {}
+ AvoidCapturingLambdaCoroutinesCheck(StringRef Name,
+ ClangTidyContext *Context);
void registerMatchers(ast_matchers::MatchFinder *Finder) override;
void check(const ast_matchers::MatchFinder::MatchResult &Result) override;
+ void storeOptions(ClangTidyOptions::OptionMap &Opts) override;
bool isLanguageVersionSupported(const LangOptions &LangOpts) const override;
+
+private:
+ const bool AllowExplicitObjectParameters;
};
} // namespace clang::tidy::cppcoreguidelines
diff --git a/clang-tools-extra/docs/ReleaseNotes.rst b/clang-tools-extra/docs/ReleaseNotes.rst
index 64c8fbbe2f07a..ca5eb1af90469 100644
--- a/clang-tools-extra/docs/ReleaseNotes.rst
+++ b/clang-tools-extra/docs/ReleaseNotes.rst
@@ -191,6 +191,12 @@ Changes in existing checks
the invalidating function in the warning message when a custom invalidation
function is used (via the `InvalidationFunctions` option).
+- Improved :doc:`cppcoreguidelines-avoid-capturing-lambda-coroutines
+ <clang-tidy/checks/cppcoreguidelines/avoid-capturing-lambda-coroutines>`
+ check by adding the `AllowExplicitObjectParameters` option. When enabled,
+ lambda coroutines using C++23 deducing ``this`` (explicit object parameter)
+ are not flagged.
+
- Improved :doc:`cppcoreguidelines-init-variables
<clang-tidy/checks/cppcoreguidelines/init-variables>` check by ensuring that
member pointers are correctly flagged as uninitialized.
diff --git a/clang-tools-extra/docs/clang-tidy/checks/cppcoreguidelines/avoid-capturing-lambda-coroutines.rst b/clang-tools-extra/docs/clang-tidy/checks/cppcoreguidelines/avoid-capturing-lambda-coroutines.rst
index 58bfc35c557dc..4e6326efa04e4 100644
--- a/clang-tools-extra/docs/clang-tidy/checks/cppcoreguidelines/avoid-capturing-lambda-coroutines.rst
+++ b/clang-tools-extra/docs/clang-tidy/checks/cppcoreguidelines/avoid-capturing-lambda-coroutines.rst
@@ -52,3 +52,32 @@ captures or ensuring the lambda closure object has a guaranteed lifetime.
Following these guidelines can help ensure the safe and reliable use of
coroutine lambdas in C++ code.
+
+Options
+-------
+
+.. option:: AllowExplicitObjectParameters
+
+ When set to `true`, lambda coroutines that use C++23 "deducing this"
+ (explicit object parameter, e.g. ``this auto``) are not flagged by this
+ check, because the captures are moved into the coroutine frame, decoupling
+ their lifetime from the lambda object.
+
+ Default is `false`.
+
+ The example from above can be made safe and will pass this check with the
+ following change:
+
+ .. code-block:: c++
+
+ int value = get_value();
+ std::shared_ptr<Foo> sharedFoo = get_foo();
+ {
+ // Pass "this auto" as the first argument to the lambda
+ const auto lambda = [value, sharedFoo](this auto) -> std::future<void>
+ {
+ co_await something();
+ };
+ lambda();
+ } // the lambda closure object has now gone out of scope, but captures are
+ // no longer coupled to its lifetime
diff --git a/clang-tools-extra/test/clang-tidy/checkers/cppcoreguidelines/avoid-capturing-lambda-coroutines-allow-explicit-object-parameters.cpp b/clang-tools-extra/test/clang-tidy/checkers/cppcoreguidelines/avoid-capturing-lambda-coroutines-allow-explicit-object-parameters.cpp
new file mode 100644
index 0000000000000..b90785fa28be5
--- /dev/null
+++ b/clang-tools-extra/test/clang-tidy/checkers/cppcoreguidelines/avoid-capturing-lambda-coroutines-allow-explicit-object-parameters.cpp
@@ -0,0 +1,93 @@
+// RUN: %check_clang_tidy -std=c++23-or-later %s cppcoreguidelines-avoid-capturing-lambda-coroutines %t \
+// RUN: -- -config='{CheckOptions: {cppcoreguidelines-avoid-capturing-lambda-coroutines.AllowExplicitObjectParameters: true}}' \
+// RUN: -- -isystem %S/Inputs/system
+
+#include <coroutines.h>
+
+// --- Cases that SHOULD still trigger the warning ---
+
+void test_capture_coroutine_no_deducing_this() {
+ int x = 42;
+ [&x]() -> task {
+ // CHECK-MESSAGES: :[[@LINE-1]]:3: warning: coroutine lambda may cause use-after-free, avoid captures or ensure lambda closure object has guaranteed lifetime [cppcoreguidelines-avoid-capturing-lambda-coroutines]
+ co_return;
+ };
+}
+
+void test_capture_coroutine_with_params() {
+ int x = 42;
+ [&x](int a) -> task {
+ // CHECK-MESSAGES: :[[@LINE-1]]:3: warning: coroutine lambda may cause use-after-free
+ co_return;
+ };
+}
+
+void test_default_capture_ref() {
+ int x = 42;
+ [&]() -> task {
+ // CHECK-MESSAGES: :[[@LINE-1]]:3: warning: coroutine lambda may cause use-after-free
+ (void)x;
+ co_return;
+ };
+}
+
+void test_default_capture_copy() {
+ int x = 42;
+ [=]() -> task {
+ // CHECK-MESSAGES: :[[@LINE-1]]:3: warning: coroutine lambda may cause use-after-free
+ (void)x;
+ co_return;
+ };
+}
+
+struct S {
+ void test_this_capture() {
+ [this]() -> task {
+ // CHECK-MESSAGES: :[[@LINE-1]]:5: warning: coroutine lambda may cause use-after-free
+ co_return;
+ };
+ }
+};
+
+// --- Cases that should NOT trigger the warning ---
+
+void test_deducing_this_coroutine() {
+ int x = 42;
+ [&x](this auto) -> task { co_return; };
+}
+
+void test_deducing_this_with_params() {
+ int x = 42;
+ [&x](this auto, int a) -> task { co_return; };
+}
+
+void test_deducing_this_with_template() {
+ int x = 42;
+ [&x]<typename T>(this auto, T a) -> task { co_return; };
+}
+
+void test_deducing_this_ref_qualified() {
+ int x = 42;
+ [&x](this auto&&) -> task { co_return; };
+}
+
+template<typename T>
+concept Integral = requires(T t) { t + 1; };
+
+void test_deducing_this_with_requires() {
+ int x = 42;
+ [&x]<typename T>(this auto, T a) -> task requires Integral<T> { co_return; };
+}
+
+void test_no_captures_no_coroutine() {
+ []() { return; };
+}
+
+void test_no_captures_coroutine() {
+ []() -> task { co_return; };
+}
+
+void test_captures_not_coroutine() {
+ int x = 42;
+ [&x]() { (void)x; };
+}
diff --git a/clang-tools-extra/test/clang-tidy/checkers/cppcoreguidelines/avoid-capturing-lambda-coroutines.cpp b/clang-tools-extra/test/clang-tidy/checkers/cppcoreguidelines/avoid-capturing-lambda-coroutines.cpp
index 6670f4a5420be..ab10122347a82 100644
--- a/clang-tools-extra/test/clang-tidy/checkers/cppcoreguidelines/avoid-capturing-lambda-coroutines.cpp
+++ b/clang-tools-extra/test/clang-tidy/checkers/cppcoreguidelines/avoid-capturing-lambda-coroutines.cpp
@@ -1,4 +1,8 @@
// RUN: %check_clang_tidy -std=c++20-or-later %s cppcoreguidelines-avoid-capturing-lambda-coroutines %t -- -- -isystem %S/Inputs/system
+// RUN: %check_clang_tidy -std=c++23-or-later -check-suffix=,CPP23 %s cppcoreguidelines-avoid-capturing-lambda-coroutines %t \
+// RUN: -- -config='{CheckOptions: {cppcoreguidelines-avoid-capturing-lambda-coroutines.AllowExplicitObjectParameters: false}}' \
+// RUN: -- -isystem %S/Inputs/system -DCPP23
+
#include <coroutines.h>
@@ -17,6 +21,10 @@ void Caught() {
// CHECK-MESSAGES: [[@LINE-1]]:5: warning: coroutine lambda may cause use-after-free, avoid captures or ensure lambda closure object has guaranteed lifetime [cppcoreguidelines-avoid-capturing-lambda-coroutines]
[y{v}] () -> task { co_return; };
// CHECK-MESSAGES: [[@LINE-1]]:5: warning: coroutine lambda may cause use-after-free, avoid captures or ensure lambda closure object has guaranteed lifetime [cppcoreguidelines-avoid-capturing-lambda-coroutines]
+#ifdef CPP23
+ [&v](this auto) -> task { co_return; };
+ // CHECK-MESSAGES-CPP23: [[@LINE-1]]:5: warning: coroutine lambda may cause use-after-free, avoid captures or ensure lambda closure object has guaranteed lifetime [cppcoreguidelines-avoid-capturing-lambda-coroutines]
+#endif
}
struct S {
More information about the cfe-commits
mailing list