[Sema] Fix lifetime extension for temporaries in range-based for loops in C++23 (#145164)

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.

Fixes https://github.com/llvm/llvm-project/issues/109793
This commit is contained in:
Marco Vitale 2025-07-10 03:57:07 +02:00 committed by GitHub
parent f1c4df5b7b
commit c86c815fc5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 70 additions and 8 deletions

View File

@ -663,6 +663,10 @@ Improvements to Clang's diagnostics
#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)
- Fixed false positives in ``-Wformat-truncation`` and ``-Wformat-overflow``
diagnostics when floating-point numbers had both width field and plus or space
prefix specified. (#GH143951)

View File

@ -1090,6 +1090,11 @@ protected:
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 {
@ -1591,6 +1596,19 @@ public:
NonParmVarDeclBits.IsCXXCondDecl = true;
}
/// Whether this variable is the implicit '__range' variable in C++
/// range-based for loops.
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;

View File

@ -1340,12 +1340,6 @@ checkExprLifetimeImpl(Sema &SemaRef, const InitializedEntity *InitEntity,
return false;
}
if (IsGslPtrValueFromGslTempOwner && DiagLoc.isValid()) {
SemaRef.Diag(DiagLoc, diag::warn_dangling_lifetime_pointer)
<< DiagRange;
return false;
}
switch (shouldLifetimeExtendThroughPath(Path)) {
case PathLifetimeKind::Extend:
// Update the storage duration of the materialized temporary.
@ -1356,6 +1350,20 @@ checkExprLifetimeImpl(Sema &SemaRef, const InitializedEntity *InitEntity,
return true;
case PathLifetimeKind::NoExtend:
if (SemaRef.getLangOpts().CPlusPlus23 && InitEntity) {
if (const VarDecl *VD =
dyn_cast_if_present<VarDecl>(InitEntity->getDecl());
VD && VD->isCXXForRangeImplicitVar()) {
return false;
}
}
if (IsGslPtrValueFromGslTempOwner && DiagLoc.isValid()) {
SemaRef.Diag(DiagLoc, diag::warn_dangling_lifetime_pointer)
<< DiagRange;
return false;
}
// If the path goes through the initialization of a variable or field,
// it can't possibly reach a temporary created in this full-expression.
// We will have already diagnosed any problems with the initializer.

View File

@ -2423,6 +2423,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;
}

View File

@ -1634,6 +1634,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

View File

@ -1317,6 +1317,7 @@ void ASTDeclWriter::VisitVarDecl(VarDecl *D) {
VarDeclBits.addBits(0, /*Width=*/3);
VarDeclBits.addBit(D->isObjCForDecl());
VarDeclBits.addBit(D->isCXXForRangeImplicitVar());
}
Record.push_back(VarDeclBits);
@ -2738,6 +2739,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));

View File

@ -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>

View File

@ -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