[clang] [Sema] Fix lifetime extension for temporaries in range-based for loops in C++23 (PR #145164)
Marco Vitale via cfe-commits
cfe-commits at lists.llvm.org
Tue Jul 1 12:49:44 PDT 2025
https://github.com/mrcvtl updated https://github.com/llvm/llvm-project/pull/145164
>From 37c57131c397d4aeaef7fe5b860d1983f1e4bdc2 Mon Sep 17 00:00:00 2001
From: Marco Vitale <mar.vitale at icloud.com>
Date: Sat, 21 Jun 2025 14:01:53 +0200
Subject: [PATCH 1/3] [Sema] Fix lifetime extension for temporaries in
range-based for loops in C++23
C++23 mandates that temporaries used in range-based for loops are lifetime-extended
to cover the full loop. This patch adds a check for loop variables and compiler-
generated `__range` bindings to apply the correct extension.
Includes test cases based on examples from CWG900/P2644R1.
---
clang/docs/ReleaseNotes.rst | 4 +++
clang/include/clang/AST/Decl.h | 19 +++++++++++++
clang/lib/Sema/CheckExprLifetime.cpp | 8 ++++++
clang/lib/Sema/SemaStmt.cpp | 4 +++
clang/lib/Serialization/ASTReaderDecl.cpp | 1 +
clang/lib/Serialization/ASTWriterDecl.cpp | 2 ++
.../test/SemaCXX/range-for-lifetime-cxx23.cpp | 27 +++++++++++++++++++
7 files changed, 65 insertions(+)
create mode 100644 clang/test/SemaCXX/range-for-lifetime-cxx23.cpp
diff --git a/clang/docs/ReleaseNotes.rst b/clang/docs/ReleaseNotes.rst
index 89d86c3371247..184d1f0b188be 100644
--- a/clang/docs/ReleaseNotes.rst
+++ b/clang/docs/ReleaseNotes.rst
@@ -643,6 +643,10 @@ Improvements to Clang's diagnostics
#GH142457, #GH139913, #GH138850, #GH137867, #GH137860, #GH107840, #GH93308,
#GH69470, #GH59391, #GH58172, #GH46215, #GH45915, #GH45891, #GH44490,
#GH36703, #GH32903, #GH23312, #GH69874.
+
+- Clang no longer emits a spurious -Wdangling-gsl warning in C++23 when
+ iterating over an element of a temporary container in a range-based
+ for loop.(#GH109793, #GH145164)
Improvements to Clang's time-trace
diff --git a/clang/include/clang/AST/Decl.h b/clang/include/clang/AST/Decl.h
index 0da940883b6f5..ab23346cc2fad 100644
--- a/clang/include/clang/AST/Decl.h
+++ b/clang/include/clang/AST/Decl.h
@@ -1085,6 +1085,11 @@ class VarDecl : public DeclaratorDecl, public Redeclarable<VarDecl> {
LLVM_PREFERRED_TYPE(bool)
unsigned IsCXXCondDecl : 1;
+
+ /// Whether this variable is the implicit __range variable in a for-range
+ /// loop.
+ LLVM_PREFERRED_TYPE(bool)
+ unsigned IsCXXForRangeImplicitVar : 1;
};
union {
@@ -1584,6 +1589,20 @@ class VarDecl : public DeclaratorDecl, public Redeclarable<VarDecl> {
NonParmVarDeclBits.IsCXXCondDecl = true;
}
+ /// Determine whether this variable is the compiler-generated '__range'
+ /// variable used to hold the range expression in a C++11 and later for-range
+ /// statement.
+ bool isCXXForRangeImplicitVar() const {
+ return isa<ParmVarDecl>(this) ? false
+ : NonParmVarDeclBits.IsCXXForRangeImplicitVar;
+ }
+
+ void setCXXForRangeImplicitVar(bool FRV) {
+ assert(!isa<ParmVarDecl>(this) &&
+ "Cannot set IsCXXForRangeImplicitVar on ParmVarDecl");
+ NonParmVarDeclBits.IsCXXForRangeImplicitVar = FRV;
+ }
+
/// Determines if this variable's alignment is dependent.
bool hasDependentAlignment() const;
diff --git a/clang/lib/Sema/CheckExprLifetime.cpp b/clang/lib/Sema/CheckExprLifetime.cpp
index 060ba31660556..fc52de1e6bd89 100644
--- a/clang/lib/Sema/CheckExprLifetime.cpp
+++ b/clang/lib/Sema/CheckExprLifetime.cpp
@@ -57,6 +57,7 @@ enum LifetimeKind {
};
using LifetimeResult =
llvm::PointerIntPair<const InitializedEntity *, 3, LifetimeKind>;
+
} // namespace
/// Determine the declaration which an initialized entity ultimately refers to,
@@ -1341,6 +1342,13 @@ checkExprLifetimeImpl(Sema &SemaRef, const InitializedEntity *InitEntity,
}
if (IsGslPtrValueFromGslTempOwner && DiagLoc.isValid()) {
+
+ if (SemaRef.getLangOpts().CPlusPlus23) {
+ if (const VarDecl *VD = cast<VarDecl>(InitEntity->getDecl());
+ VD && VD->isCXXForRangeImplicitVar())
+ return false;
+ }
+
SemaRef.Diag(DiagLoc, diag::warn_dangling_lifetime_pointer)
<< DiagRange;
return false;
diff --git a/clang/lib/Sema/SemaStmt.cpp b/clang/lib/Sema/SemaStmt.cpp
index 923a9e81fbd6a..ef0aff1b2838f 100644
--- a/clang/lib/Sema/SemaStmt.cpp
+++ b/clang/lib/Sema/SemaStmt.cpp
@@ -2374,6 +2374,9 @@ static bool FinishForRangeVarDecl(Sema &SemaRef, VarDecl *Decl, Expr *Init,
SemaRef.ObjC().inferObjCARCLifetime(Decl))
Decl->setInvalidDecl();
+ if (SemaRef.getLangOpts().CPlusPlus23)
+ SemaRef.currentEvaluationContext().InLifetimeExtendingContext = true;
+
SemaRef.AddInitializerToDecl(Decl, Init, /*DirectInit=*/false);
SemaRef.FinalizeDeclaration(Decl);
SemaRef.CurContext->addHiddenDecl(Decl);
@@ -2423,6 +2426,7 @@ VarDecl *BuildForRangeVarDecl(Sema &SemaRef, SourceLocation Loc,
VarDecl *Decl = VarDecl::Create(SemaRef.Context, DC, Loc, Loc, II, Type,
TInfo, SC_None);
Decl->setImplicit();
+ Decl->setCXXForRangeImplicitVar(true);
return Decl;
}
diff --git a/clang/lib/Serialization/ASTReaderDecl.cpp b/clang/lib/Serialization/ASTReaderDecl.cpp
index 0ffd78424be0d..e6a8a0a243401 100644
--- a/clang/lib/Serialization/ASTReaderDecl.cpp
+++ b/clang/lib/Serialization/ASTReaderDecl.cpp
@@ -1637,6 +1637,7 @@ RedeclarableResult ASTDeclReader::VisitVarDeclImpl(VarDecl *VD) {
VarDeclBits.getNextBits(/*Width*/ 3);
VD->NonParmVarDeclBits.ObjCForDecl = VarDeclBits.getNextBit();
+ VD->NonParmVarDeclBits.IsCXXForRangeImplicitVar = VarDeclBits.getNextBit();
}
// If this variable has a deduced type, defer reading that type until we are
diff --git a/clang/lib/Serialization/ASTWriterDecl.cpp b/clang/lib/Serialization/ASTWriterDecl.cpp
index 2e390dbe79ec6..1d4c3e4e7b88a 100644
--- a/clang/lib/Serialization/ASTWriterDecl.cpp
+++ b/clang/lib/Serialization/ASTWriterDecl.cpp
@@ -1319,6 +1319,7 @@ void ASTDeclWriter::VisitVarDecl(VarDecl *D) {
VarDeclBits.addBits(0, /*Width=*/3);
VarDeclBits.addBit(D->isObjCForDecl());
+ VarDeclBits.addBit(D->isCXXForRangeImplicitVar());
}
Record.push_back(VarDeclBits);
@@ -2740,6 +2741,7 @@ void ASTWriter::WriteDeclAbbrevs() {
// isInline, isInlineSpecified, isConstexpr,
// isInitCapture, isPrevDeclInSameScope, hasInitWithSideEffects,
// EscapingByref, HasDeducedType, ImplicitParamKind, isObjCForDecl
+ // IsCXXForRangeImplicitVar
Abv->Add(BitCodeAbbrevOp(0)); // VarKind (local enum)
// Type Source Info
Abv->Add(BitCodeAbbrevOp(BitCodeAbbrevOp::Array));
diff --git a/clang/test/SemaCXX/range-for-lifetime-cxx23.cpp b/clang/test/SemaCXX/range-for-lifetime-cxx23.cpp
new file mode 100644
index 0000000000000..c36fd6c246347
--- /dev/null
+++ b/clang/test/SemaCXX/range-for-lifetime-cxx23.cpp
@@ -0,0 +1,27 @@
+// RUN: %clang_cc1 -std=c++23 -fsyntax-only -verify %s
+
+using size_t = decltype(sizeof(void *));
+
+namespace std {
+template <typename T> struct vector {
+ T &operator[](size_t I);
+};
+
+struct string {
+ const char *begin();
+ const char *end();
+};
+
+} // namespace std
+
+std::vector<std::string> getData();
+
+void foo() {
+ // Verifies we don't trigger a diagnostic from -Wdangling-gsl
+ // when iterating over a temporary in C++23.
+ for (auto c : getData()[0]) {
+ (void)c;
+ }
+}
+
+// expected-no-diagnostics
>From d9a88d556f78193b345313a141508231127b8bfa Mon Sep 17 00:00:00 2001
From: Marco Vitale <mar.vitale at icloud.com>
Date: Sat, 28 Jun 2025 20:26:24 +0200
Subject: [PATCH 2/3] Address feedbacks
---
clang/include/clang/AST/Decl.h | 5 ++---
clang/lib/Sema/CheckExprLifetime.cpp | 4 ++--
clang/lib/Sema/SemaStmt.cpp | 3 ---
3 files changed, 4 insertions(+), 8 deletions(-)
diff --git a/clang/include/clang/AST/Decl.h b/clang/include/clang/AST/Decl.h
index ab23346cc2fad..e5f4b81437b24 100644
--- a/clang/include/clang/AST/Decl.h
+++ b/clang/include/clang/AST/Decl.h
@@ -1589,9 +1589,8 @@ class VarDecl : public DeclaratorDecl, public Redeclarable<VarDecl> {
NonParmVarDeclBits.IsCXXCondDecl = true;
}
- /// Determine whether this variable is the compiler-generated '__range'
- /// variable used to hold the range expression in a C++11 and later for-range
- /// statement.
+ /// Whether this variable is the implicit '__range' variable in C++
+ /// range-based for loops.
bool isCXXForRangeImplicitVar() const {
return isa<ParmVarDecl>(this) ? false
: NonParmVarDeclBits.IsCXXForRangeImplicitVar;
diff --git a/clang/lib/Sema/CheckExprLifetime.cpp b/clang/lib/Sema/CheckExprLifetime.cpp
index fc52de1e6bd89..28c52e0fe27d1 100644
--- a/clang/lib/Sema/CheckExprLifetime.cpp
+++ b/clang/lib/Sema/CheckExprLifetime.cpp
@@ -57,7 +57,6 @@ enum LifetimeKind {
};
using LifetimeResult =
llvm::PointerIntPair<const InitializedEntity *, 3, LifetimeKind>;
-
} // namespace
/// Determine the declaration which an initialized entity ultimately refers to,
@@ -1344,7 +1343,8 @@ checkExprLifetimeImpl(Sema &SemaRef, const InitializedEntity *InitEntity,
if (IsGslPtrValueFromGslTempOwner && DiagLoc.isValid()) {
if (SemaRef.getLangOpts().CPlusPlus23) {
- if (const VarDecl *VD = cast<VarDecl>(InitEntity->getDecl());
+ if (const VarDecl *VD =
+ dyn_cast_if_present<VarDecl>(InitEntity->getDecl());
VD && VD->isCXXForRangeImplicitVar())
return false;
}
diff --git a/clang/lib/Sema/SemaStmt.cpp b/clang/lib/Sema/SemaStmt.cpp
index ef0aff1b2838f..f85826aecadf3 100644
--- a/clang/lib/Sema/SemaStmt.cpp
+++ b/clang/lib/Sema/SemaStmt.cpp
@@ -2374,9 +2374,6 @@ static bool FinishForRangeVarDecl(Sema &SemaRef, VarDecl *Decl, Expr *Init,
SemaRef.ObjC().inferObjCARCLifetime(Decl))
Decl->setInvalidDecl();
- if (SemaRef.getLangOpts().CPlusPlus23)
- SemaRef.currentEvaluationContext().InLifetimeExtendingContext = true;
-
SemaRef.AddInitializerToDecl(Decl, Init, /*DirectInit=*/false);
SemaRef.FinalizeDeclaration(Decl);
SemaRef.CurContext->addHiddenDecl(Decl);
>From b40d507dcb876ab987aac5fa188ef4261c03c09f Mon Sep 17 00:00:00 2001
From: Marco Vitale <mar.vitale at icloud.com>
Date: Tue, 1 Jul 2025 21:45:04 +0200
Subject: [PATCH 3/3] Move check to support clang::lifetimebound and boost
performance
---
clang/lib/Sema/CheckExprLifetime.cpp | 15 +++++++--------
clang/test/SemaCXX/attr-lifetimebound.cpp | 5 +++--
2 files changed, 10 insertions(+), 10 deletions(-)
diff --git a/clang/lib/Sema/CheckExprLifetime.cpp b/clang/lib/Sema/CheckExprLifetime.cpp
index 28c52e0fe27d1..aa8fdc05e8ee7 100644
--- a/clang/lib/Sema/CheckExprLifetime.cpp
+++ b/clang/lib/Sema/CheckExprLifetime.cpp
@@ -1304,6 +1304,13 @@ checkExprLifetimeImpl(Sema &SemaRef, const InitializedEntity *InitEntity,
if (LK == LK_FullExpression)
return;
+ if (LK == LK_Extended && SemaRef.getLangOpts().CPlusPlus23) {
+ if (const auto *VD = dyn_cast_if_present<VarDecl>(InitEntity->getDecl())) {
+ if (VD->isCXXForRangeImplicitVar())
+ return;
+ }
+ }
+
// FIXME: consider moving the TemporaryVisitor and visitLocalsRetained*
// functions to a dedicated class.
auto TemporaryVisitor = [&](const IndirectLocalPath &Path, Local L,
@@ -1341,14 +1348,6 @@ checkExprLifetimeImpl(Sema &SemaRef, const InitializedEntity *InitEntity,
}
if (IsGslPtrValueFromGslTempOwner && DiagLoc.isValid()) {
-
- if (SemaRef.getLangOpts().CPlusPlus23) {
- if (const VarDecl *VD =
- dyn_cast_if_present<VarDecl>(InitEntity->getDecl());
- VD && VD->isCXXForRangeImplicitVar())
- return false;
- }
-
SemaRef.Diag(DiagLoc, diag::warn_dangling_lifetime_pointer)
<< DiagRange;
return false;
diff --git a/clang/test/SemaCXX/attr-lifetimebound.cpp b/clang/test/SemaCXX/attr-lifetimebound.cpp
index 2bb88171bdfe4..80f1301bf1cee 100644
--- a/clang/test/SemaCXX/attr-lifetimebound.cpp
+++ b/clang/test/SemaCXX/attr-lifetimebound.cpp
@@ -192,8 +192,9 @@ namespace p0936r0_examples {
std::vector make_vector();
void use_reversed_range() {
- // FIXME: Don't expose the name of the internal range variable.
- for (auto x : reversed(make_vector())) {} // expected-warning {{temporary implicitly bound to local reference will be destroyed at the end of the full-expression}}
+ // No warning here because C++23 extends the lifetime of the temporary
+ // in a range-based for loop.
+ for (auto x : reversed(make_vector())) {}
}
template <typename K, typename V>
More information about the cfe-commits
mailing list