[clang] [LifetimeSafety] Track origins for lifetimebound calls returning record types (PR #187917)
via cfe-commits
cfe-commits at lists.llvm.org
Sun Mar 22 00:13:17 PDT 2026
llvmbot wrote:
<!--LLVM PR SUMMARY COMMENT-->
@llvm/pr-subscribers-clang-analysis
Author: Zhijie Wang (aeft)
<details>
<summary>Changes</summary>
- Move `hasOrigins` from free function to `OriginManager` method
- Add pre-scan (`collectLifetimeboundOriginTypes`) to register return types of `[[clang::lifetimebound]]` calls before fact generation
- Generalize copy/move constructor origin propagation from lambda-only to all types with `isDefaulted()` and `hasOrigins()` guard
- Guard `operator=` origin propagation: pointer-like types always propagate; other tracked types only when defaulted
- Defer `ThisOrigins` construction until after the pre-scan to avoid origin list depth mismatch
- Fix `IsArgLifetimeBound` to exclude constructors from the instance-method branch (latent bug exposed by this change)
Fixes #<!-- -->163600
---
Patch is 22.37 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/187917.diff
7 Files Affected:
- (modified) clang/include/clang/Analysis/Analyses/LifetimeSafety/Facts.h (+1-1)
- (modified) clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h (+3)
- (modified) clang/include/clang/Analysis/Analyses/LifetimeSafety/Origins.h (+19-3)
- (modified) clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp (+30-11)
- (modified) clang/lib/Analysis/LifetimeSafety/LifetimeSafety.cpp (+3)
- (modified) clang/lib/Analysis/LifetimeSafety/Origins.cpp (+79-9)
- (modified) clang/test/Sema/warn-lifetime-safety.cpp (+245-4)
``````````diff
diff --git a/clang/include/clang/Analysis/Analyses/LifetimeSafety/Facts.h b/clang/include/clang/Analysis/Analyses/LifetimeSafety/Facts.h
index fdcf317c69cbf..93bfae4079e47 100644
--- a/clang/include/clang/Analysis/Analyses/LifetimeSafety/Facts.h
+++ b/clang/include/clang/Analysis/Analyses/LifetimeSafety/Facts.h
@@ -313,7 +313,7 @@ class TestPointFact : public Fact {
class FactManager {
public:
FactManager(const AnalysisDeclContext &AC, const CFG &Cfg)
- : OriginMgr(AC.getASTContext(), AC.getDecl()) {
+ : OriginMgr(AC.getASTContext()) {
BlockToFacts.resize(Cfg.getNumBlockIDs());
}
diff --git a/clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h b/clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h
index dfcbdc7d73007..775b51afa0237 100644
--- a/clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h
+++ b/clang/include/clang/Analysis/Analyses/LifetimeSafety/FactsGenerator.h
@@ -57,6 +57,9 @@ class FactsGenerator : public ConstStmtVisitor<FactsGenerator> {
OriginList *getOriginsList(const ValueDecl &D);
OriginList *getOriginsList(const Expr &E);
+ bool hasOrigins(QualType QT) const;
+ bool hasOrigins(const Expr *E) const;
+
void flow(OriginList *Dst, OriginList *Src, bool Kill);
void handleAssignment(const Expr *LHSExpr, const Expr *RHSExpr);
diff --git a/clang/include/clang/Analysis/Analyses/LifetimeSafety/Origins.h b/clang/include/clang/Analysis/Analyses/LifetimeSafety/Origins.h
index 8c638bdcace3f..12148d9b29c9c 100644
--- a/clang/include/clang/Analysis/Analyses/LifetimeSafety/Origins.h
+++ b/clang/include/clang/Analysis/Analyses/LifetimeSafety/Origins.h
@@ -20,6 +20,7 @@
#include "clang/AST/TypeBase.h"
#include "clang/Analysis/Analyses/LifetimeSafety/LifetimeStats.h"
#include "clang/Analysis/Analyses/LifetimeSafety/Utils.h"
+#include "clang/Analysis/AnalysisDeclContext.h"
#include "llvm/Support/raw_ostream.h"
namespace clang::lifetimes::internal {
@@ -117,15 +118,17 @@ class OriginList {
OriginList *InnerList = nullptr;
};
-bool hasOrigins(QualType QT);
-bool hasOrigins(const Expr *E);
bool doesDeclHaveStorage(const ValueDecl *D);
/// Manages the creation, storage, and retrieval of origins for pointer-like
/// variables and expressions.
class OriginManager {
public:
- explicit OriginManager(ASTContext &AST, const Decl *D);
+ explicit OriginManager(ASTContext &AST);
+
+ /// Must be called after collectLifetimeboundOriginTypes() to ensure
+ /// ThisOrigins reflects the complete set of tracked types.
+ void initializeThisOrigins(const Decl *D);
/// Gets or creates the OriginList for a given ValueDecl.
///
@@ -155,11 +158,20 @@ class OriginManager {
unsigned getNumOrigins() const { return NextOriginID.Value; }
+ bool hasOrigins(QualType QT) const;
+ bool hasOrigins(const Expr *E) const;
+
void dump(OriginID OID, llvm::raw_ostream &OS) const;
/// Collects statistics about expressions that lack associated origins.
void collectMissingOrigins(Stmt &FunctionBody, LifetimeSafetyStats &LSStats);
+ /// Pre-scans the function body (and constructor init lists) to discover
+ /// return types of [[clang::lifetimebound]] calls, registering them for
+ /// origin tracking.
+ void collectLifetimeboundOriginTypes(AnalysisDeclContext &AC);
+ void registerLifetimeboundOriginType(QualType QT);
+
private:
OriginID getNextOriginID() { return NextOriginID++; }
@@ -178,6 +190,10 @@ class OriginManager {
llvm::DenseMap<const clang::ValueDecl *, OriginList *> DeclToList;
llvm::DenseMap<const clang::Expr *, OriginList *> ExprToList;
std::optional<OriginList *> ThisOrigins;
+ /// Types that are not inherently pointer-like but require origin tracking
+ /// because they are returned from functions with [[clang::lifetimebound]]
+ /// parameters.
+ llvm::DenseSet<const Type *> LifetimeboundOriginTypes;
};
} // namespace clang::lifetimes::internal
diff --git a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
index 3259505584c9f..890f1d2ba3a7b 100644
--- a/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp
@@ -36,6 +36,14 @@ OriginList *FactsGenerator::getOriginsList(const Expr &E) {
return FactMgr.getOriginMgr().getOrCreateList(&E);
}
+bool FactsGenerator::hasOrigins(QualType QT) const {
+ return FactMgr.getOriginMgr().hasOrigins(QT);
+}
+
+bool FactsGenerator::hasOrigins(const Expr *E) const {
+ return FactMgr.getOriginMgr().hasOrigins(E);
+}
+
/// Propagates origin information from Src to Dst through all levels of
/// indirection, creating OriginFlowFacts at each level.
///
@@ -182,14 +190,13 @@ void FactsGenerator::VisitCXXConstructExpr(const CXXConstructExpr *CCE) {
handleGSLPointerConstruction(CCE);
return;
}
- // Implicit copy/move constructors of lambda closures lack
- // [[clang::lifetimebound]], so `handleFunctionCall` cannot propagate origins.
- // Handle them directly to keep the origin chain intact (e.g., `return
- // lambda;` copies the closure).
- if (const auto *RD = CCE->getType()->getAsCXXRecordDecl();
- RD && RD->isLambda() &&
- CCE->getConstructor()->isCopyOrMoveConstructor() &&
- CCE->getNumArgs() == 1) {
+ // For defaulted (implicit or `= default`) copy/move constructors, propagate
+ // origins directly. User-defined copy/move constructors have opaque semantics
+ // and fall through to `handleFunctionCall`, where [[clang::lifetimebound]] is
+ // needed to propagate origins.
+ if (CCE->getConstructor()->isCopyOrMoveConstructor() &&
+ CCE->getConstructor()->isDefaulted() && CCE->getNumArgs() == 1 &&
+ hasOrigins(CCE->getType())) {
const Expr *Arg = CCE->getArg(0);
if (OriginList *ArgList = getRValueOrigins(Arg, getOriginsList(*Arg))) {
flow(getOriginsList(*CCE), ArgList, /*Kill=*/true);
@@ -398,8 +405,20 @@ void FactsGenerator::VisitCXXOperatorCallExpr(const CXXOperatorCallExpr *OCE) {
// and are handled separately.
if (OCE->getOperator() == OO_Equal && OCE->getNumArgs() == 2 &&
hasOrigins(OCE->getArg(0)->getType())) {
- handleAssignment(OCE->getArg(0), OCE->getArg(1));
- return;
+ // Pointer-like types: assignment inherently propagates origins.
+ QualType LHSTy = OCE->getArg(0)->getType();
+ if (LHSTy->isPointerOrReferenceType() || isGslPointerType(LHSTy)) {
+ handleAssignment(OCE->getArg(0), OCE->getArg(1));
+ return;
+ }
+ // Other tracked types: only defaulted operator= propagates origins.
+ // User-defined operator= has opaque semantics, so don't handle them now.
+ if (const auto *MD =
+ dyn_cast_or_null<CXXMethodDecl>(OCE->getDirectCallee());
+ MD && MD->isDefaulted()) {
+ handleAssignment(OCE->getArg(0), OCE->getArg(1));
+ return;
+ }
}
VisitCallExpr(OCE);
}
@@ -662,7 +681,7 @@ void FactsGenerator::handleFunctionCall(const Expr *Call,
auto IsArgLifetimeBound = [FD](unsigned I) -> bool {
const ParmVarDecl *PVD = nullptr;
if (const auto *Method = dyn_cast<CXXMethodDecl>(FD);
- Method && Method->isInstance()) {
+ Method && Method->isInstance() && !isa<CXXConstructorDecl>(FD)) {
if (I == 0)
// For the 'this' argument, the attribute is on the method itself.
return implicitObjectParamIsLifetimeBound(Method) ||
diff --git a/clang/lib/Analysis/LifetimeSafety/LifetimeSafety.cpp b/clang/lib/Analysis/LifetimeSafety/LifetimeSafety.cpp
index 714f979fa5ee7..56a187202d8fa 100644
--- a/clang/lib/Analysis/LifetimeSafety/LifetimeSafety.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/LifetimeSafety.cpp
@@ -71,6 +71,9 @@ void LifetimeSafetyAnalysis::run() {
FactMgr = std::make_unique<FactManager>(AC, Cfg);
+ FactMgr->getOriginMgr().collectLifetimeboundOriginTypes(AC);
+ FactMgr->getOriginMgr().initializeThisOrigins(AC.getDecl());
+
FactsGenerator FactGen(*FactMgr, AC);
FactGen.run();
diff --git a/clang/lib/Analysis/LifetimeSafety/Origins.cpp b/clang/lib/Analysis/LifetimeSafety/Origins.cpp
index 0122f7a734541..2655835317d9b 100644
--- a/clang/lib/Analysis/LifetimeSafety/Origins.cpp
+++ b/clang/lib/Analysis/LifetimeSafety/Origins.cpp
@@ -18,6 +18,7 @@
#include "clang/AST/TypeBase.h"
#include "clang/Analysis/Analyses/LifetimeSafety/LifetimeAnnotations.h"
#include "clang/Analysis/Analyses/LifetimeSafety/LifetimeStats.h"
+#include "clang/Analysis/AnalysisDeclContext.h"
#include "llvm/ADT/StringMap.h"
namespace clang::lifetimes::internal {
@@ -29,10 +30,10 @@ class MissingOriginCollector
public:
MissingOriginCollector(
const llvm::DenseMap<const clang::Expr *, OriginList *> &ExprToOriginList,
- LifetimeSafetyStats &LSStats)
- : ExprToOriginList(ExprToOriginList), LSStats(LSStats) {}
+ const OriginManager &OM, LifetimeSafetyStats &LSStats)
+ : ExprToOriginList(ExprToOriginList), OM(OM), LSStats(LSStats) {}
bool VisitExpr(Expr *E) {
- if (!hasOrigins(E))
+ if (!OM.hasOrigins(E))
return true;
// Check if we have an origin for this expression.
if (!ExprToOriginList.contains(E)) {
@@ -46,13 +47,60 @@ class MissingOriginCollector
private:
const llvm::DenseMap<const clang::Expr *, OriginList *> &ExprToOriginList;
+ const OriginManager &OM;
LifetimeSafetyStats &LSStats;
};
+
+class LifetimeboundOriginTypeCollector
+ : public RecursiveASTVisitor<LifetimeboundOriginTypeCollector> {
+public:
+ LifetimeboundOriginTypeCollector(OriginManager &OM) : OM(OM) {}
+
+ bool VisitCallExpr(const CallExpr *CE) {
+ if (const auto *FD = CE->getDirectCallee())
+ collect(FD, FD->getReturnType());
+ return true;
+ }
+
+ bool VisitCXXConstructExpr(const CXXConstructExpr *CCE) {
+ collect(CCE->getConstructor(), CCE->getType());
+ return true;
+ }
+
+ bool shouldVisitLambdaBody() const { return false; }
+
+private:
+ OriginManager &OM;
+
+ void collect(const FunctionDecl *FD, QualType RetType) {
+ if (!FD)
+ return;
+ FD = getDeclWithMergedLifetimeBoundAttrs(FD);
+
+ if (const auto *MD = dyn_cast<CXXMethodDecl>(FD);
+ MD && MD->isInstance() && !isa<CXXConstructorDecl>(MD) &&
+ implicitObjectParamIsLifetimeBound(MD)) {
+ OM.registerLifetimeboundOriginType(RetType);
+ return;
+ }
+
+ for (const auto *Param : FD->parameters()) {
+ if (Param->hasAttr<LifetimeBoundAttr>()) {
+ OM.registerLifetimeboundOriginType(RetType);
+ return;
+ }
+ }
+ }
+};
+
} // namespace
-bool hasOrigins(QualType QT) {
+bool OriginManager::hasOrigins(QualType QT) const {
if (QT->isPointerOrReferenceType() || isGslPointerType(QT))
return true;
+ if (LifetimeboundOriginTypes.contains(
+ QT->getCanonicalTypeUnqualified().getTypePtr()))
+ return true;
const auto *RD = QT->getAsCXXRecordDecl();
if (!RD)
return false;
@@ -70,7 +118,9 @@ bool hasOrigins(QualType QT) {
///
/// An expression has origins if:
/// - It's a glvalue (has addressable storage), OR
-/// - Its type is pointer-like (pointer, reference, or gsl::Pointer)
+/// - Its type is pointer-like (pointer, reference, or gsl::Pointer), OR
+/// - Its type is registered for origin tracking (e.g., return type of a
+/// [[clang::lifetimebound]] function)
///
/// Examples:
/// - `int x; x` : has origin (glvalue)
@@ -78,7 +128,7 @@ bool hasOrigins(QualType QT) {
/// - `std::string_view{}` : has 1 origin (prvalue of pointer type)
/// - `42` : no origin (prvalue of non-pointer type)
/// - `x + y` : (where x, y are int) → no origin (prvalue of non-pointer type)
-bool hasOrigins(const Expr *E) {
+bool OriginManager::hasOrigins(const Expr *E) const {
return E->isGLValue() || hasOrigins(E->getType());
}
@@ -99,8 +149,9 @@ bool doesDeclHaveStorage(const ValueDecl *D) {
return !D->getType()->isReferenceType();
}
-OriginManager::OriginManager(ASTContext &AST, const Decl *D) : AST(AST) {
- // Create OriginList for 'this' expr.
+OriginManager::OriginManager(ASTContext &AST) : AST(AST) {}
+
+void OriginManager::initializeThisOrigins(const Decl *D) {
const auto *MD = llvm::dyn_cast_or_null<CXXMethodDecl>(D);
if (!MD || !MD->isInstance())
return;
@@ -232,8 +283,27 @@ const Origin &OriginManager::getOrigin(OriginID ID) const {
void OriginManager::collectMissingOrigins(Stmt &FunctionBody,
LifetimeSafetyStats &LSStats) {
- MissingOriginCollector Collector(this->ExprToList, LSStats);
+ MissingOriginCollector Collector(this->ExprToList, *this, LSStats);
Collector.TraverseStmt(const_cast<Stmt *>(&FunctionBody));
}
+void OriginManager::collectLifetimeboundOriginTypes(AnalysisDeclContext &AC) {
+ LifetimeboundOriginTypeCollector Collector(*this);
+ if (Stmt *Body = AC.getBody())
+ Collector.TraverseStmt(Body);
+ if (const auto *CD = dyn_cast<CXXConstructorDecl>(AC.getDecl()))
+ for (const auto *Init : CD->inits())
+ Collector.TraverseStmt(Init->getInit());
+}
+
+void OriginManager::registerLifetimeboundOriginType(QualType QT) {
+ // TODO: Support [[gsl::Owner]] return types. For now, skip them because they
+ // change owner origin-list shape and can break GSL construction flow.
+ if (!QT->getAsCXXRecordDecl() || isGslOwnerType(QT) || hasOrigins(QT))
+ return;
+
+ LifetimeboundOriginTypes.insert(
+ QT->getCanonicalTypeUnqualified().getTypePtr());
+}
+
} // namespace clang::lifetimes::internal
diff --git a/clang/test/Sema/warn-lifetime-safety.cpp b/clang/test/Sema/warn-lifetime-safety.cpp
index bd09bb70e9a11..a8c0efeb875c0 100644
--- a/clang/test/Sema/warn-lifetime-safety.cpp
+++ b/clang/test/Sema/warn-lifetime-safety.cpp
@@ -909,7 +909,6 @@ void lifetimebound_return_reference() {
(void)*ptr; // expected-note {{later used here}}
}
-// FIXME: No warning for non gsl::Pointer types. Origin tracking is only supported for pointer types.
struct LifetimeBoundCtor {
LifetimeBoundCtor();
LifetimeBoundCtor(const MyObj& obj [[clang::lifetimebound]]);
@@ -919,9 +918,9 @@ void lifetimebound_ctor() {
LifetimeBoundCtor v;
{
MyObj obj;
- v = obj;
- }
- (void)v;
+ v = obj; // expected-warning {{object whose reference is captured does not live long enough}}
+ } // expected-note {{destroyed here}}
+ (void)v; // expected-note {{later used here}}
}
View lifetimebound_return_of_local() {
@@ -2102,3 +2101,245 @@ void pointer_in_array_use_after_scope() {
}
} // namespace array
+
+namespace track_origins_for_lifetimebound_record_type {
+
+template <class T> void use(T);
+
+struct S {
+ S();
+ S(const std::string &s [[clang::lifetimebound]]);
+
+ S return_self_after_registration() const;
+};
+
+S getS(const std::string &s [[clang::lifetimebound]]);
+
+void from_free_function() {
+ S s = getS(std::string("temp")); // expected-warning {{object whose reference is captured does not live long enough}} \
+ // expected-note {{destroyed here}}
+ use(s); // expected-note {{later used here}}
+}
+
+void from_constructor() {
+ S s(std::string("temp")); // expected-warning {{object whose reference is captured does not live long enough}} \
+ // expected-note {{destroyed here}}
+ use(s); // expected-note {{later used here}}
+}
+
+struct Factory {
+ S make(const std::string &s [[clang::lifetimebound]]);
+ static S create(const std::string &s [[clang::lifetimebound]]);
+ S makeThis() const [[clang::lifetimebound]];
+};
+
+void from_method() {
+ Factory f;
+ S s = f.make(std::string("temp")); // expected-warning {{object whose reference is captured does not live long enough}} \
+ // expected-note {{destroyed here}}
+ use(s); // expected-note {{later used here}}
+}
+
+void from_static_method() {
+ S s = Factory::create(std::string("temp")); // expected-warning {{object whose reference is captured does not live long enough}} \
+ // expected-note {{destroyed here}}
+ use(s); // expected-note {{later used here}}
+}
+
+void from_lifetimebound_this_method() {
+ S value;
+ {
+ Factory f;
+ value = f.makeThis(); // expected-warning {{object whose reference is captured does not live long enough}}
+ } // expected-note {{destroyed here}}
+ use(value); // expected-note {{later used here}}
+}
+
+void across_scope() {
+ S s{};
+ {
+ std::string str{"abc"};
+ s = getS(str); // expected-warning {{object whose reference is captured does not live long enough}}
+ } // expected-note {{destroyed here}}
+ use(s); // expected-note {{later used here}}
+}
+
+void same_scope() {
+ std::string str{"abc"};
+ S s = getS(str);
+ use(s);
+}
+
+S copy_propagation() {
+ std::string str{"abc"};
+ S a = getS(str); // expected-warning {{address of stack memory is returned later}}
+ S b = a;
+ return b; // expected-note {{returned here}}
+}
+
+void assignment_propagation() {
+ S a, b;
+ {
+ std::string str{"abc"};
+ a = getS(str); // expected-warning {{object whose reference is captured does not live long enough}}
+ b = a;
+ } // expected-note {{destroyed here}}
+ use(b); // expected-note {{later used here}}
+}
+
+S getSNoAnnotation(const std::string &s);
+
+void no_annotation() {
+ S s = getSNoAnnotation(std::string("temp"));
+ use(s);
+}
+
+void mix_annotated_and_not() {
+ S s1 = getS(std::string("temp")); // expected-warning {{object whose reference is captured does not live long enough}} \
+ // expected-note {{destroyed here}}
+ S s2 = getSNoAnnotation(std::string("temp"));
+ use(s1); // expected-note {{later used here}}
+ use(s2);
+}
+
+S getS2(const std::string &a [[clang::lifetimebound]], const std::string &b [[clang::lifetimebound]]);
+
+S multiple_lifetimebound_params() {
+ std::string str{"abc"};
+ S s = getS2(str, std::string("temp")); // expected-warning {{address of stack memory is returned later}} \
+ // expected-warning {{object whose reference is captured does not live long enough}} \
+ // expected-note {{destroyed here}}
+ return s; // expected-note {{returned here}} \
+ // expected-note {{later used here}}
+}
+
+int getInt(const std::string &s [[clang::lifetimebound]]);
+
+void primitive_return() {
+ int i = getInt(std::string("temp"));
+ use(i);
+}
+
+template <class T>
+T make(const std::string &s [[clang::lifetimebound]]);
+
+void from_template_instantiation() {
+ S s = make<S>(std::string("temp")); // expected-warning {{object whose reference is captured does not live long enough}} \
+ // expected-note {{destroyed here}}
+ use(s); // expected-note {{later used here}}
+}
+
+struct FieldInitFromLifetimebound {
+ S value; // function-note {{this field dangles}}
+ FieldInitFromLifetimebound() : value(getS(std::string("temp"))) {} // function-warning {{address of stack memory escapes to a field}}
+};
+
+S S::return_self_after_registration() const {
+ std::string s{"abc"};
+ getS(s);
+ return *this;
+}
+
+struct SWithUserDefinedCopyLikeOps {
+ SWithUserDefinedCopyLikeOps();
+ SWithUserDefinedCopyLikeOps(const std::string &s [[clang::lifetimebound]]) : owned(s), data(s) {}
+
+ SWithUserDefinedCopyLikeOps(const SWithUserDefinedCopyLikeOps &other) : owned("copy"), data(owned) {}
+
+ SWithUserDefinedCopyLikeOps &operator=(const SWithUserDefinedCopyLikeOps &) {
+ owned = "copy";
+ data = owned;
+ return *this;
+ }
+
+ std::string owned;
+ std::string_view data;
+};
+
+SWithUserDefinedCopyLikeOps getSWithUserDefinedCopyLikeOps(const std::string &s [[clang::lifetimebound]]);
+
+SWithUserDefinedCopyLikeOps user_defined_copy_ctor_should_not_assume_origin_propagation() {
+ std::string str{"abc"};
+ SWithUserDefinedCopyLikeOps s = getSWithUserDefinedCopyLikeOps(str);
+ SWithUserDefinedCopyLikeOps copy = s; // Copy is rescued by user-defined copy constructor, so should not warn.
+ return copy;
+}
+
+void user_defined_assignment_should_not_assume_origin_propagation() {
+ SWithUserDefinedCopyLikeOps dst;
+ {
+ std::string str{"abc"...
[truncated]
``````````
</details>
https://github.com/llvm/llvm-project/pull/187917
More information about the cfe-commits
mailing list