[clang] [DebugInfo] Ignore undefined constexpr constructors in constructor homing. (PR #221971)
Clayton Knittel via cfe-commits
cfe-commits at lists.llvm.org
Tue Sep 8 05:04:54 PDT 2026
https://github.com/ClaytonKnittel created https://github.com/llvm/llvm-project/pull/221971
This is a roll-forward of https://github.com/llvm/llvm-project/pull/218165, which inadvertantly caused missing debug info in https://github.com/llvm/llvm-project/issues/221560 due to an existing issue with standard-layout unions and layout-compatible aliasing (see [[class.mem]](https://timsong-cpp.github.io/cppwp/n3337/class.mem#19)).
Constexpr constructors without a visible definition in the TU are not callable in a constexpr context, which means it is not possible to construct a type in constexpr without emitting debug info for the constructor itself. Although undefined constexpr constructors are still callable, they are normal out of line functions, requiring a definition to link against.
>From a6ccaa7ba38544607f3cea39a5c3db21da418d84 Mon Sep 17 00:00:00 2001
From: Clayton Knittel <cknit1999 at gmail.com>
Date: Sun, 6 Sep 2026 23:45:05 +0000
Subject: [PATCH 1/2] Always emit complete debug info for types which appear as
members of a standard-layout union.
[[class.mem]](https://timsong-cpp.github.io/cppwp/n3337/class.mem#19)
states:
> "If a standard-layout union contains two or more standard-layout structs that share a common initial sequence, and if the standard-layout union object currently contains one of these standard-layout structs, it is permitted to inspect the common initial part of any of them. Two standard-layout structs share a common initial sequence if corresponding members have layout-compatible types and either neither member is a bit-field or both are bit-fields with the same width for a sequence of one or more initial members."
This makes it possible to obtain a reference to a type which was never
constructed, which violates the assumption made by constructor homing
that all types that may require debug info must be constructed.
This change takes the conservative approach of always emitting full
debug info for standard-layout types which appear as a member of a
standard-layout union somewhere in the TU. It could be made more
aggressive by only emitting full debug info if there is another type in
the union that shares a "common initial sequence", but that would add
complexity to this exclusion logic and likely isn't worth it.
Signed-off-by: Clayton Knittel <cknit1999 at gmail.com>
---
.../clang/AST/CXXRecordDeclDefinitionBits.def | 3 +
clang/include/clang/AST/DeclCXX.h | 9 +
clang/lib/AST/DeclCXX.cpp | 16 +-
clang/lib/CodeGen/CGDebugInfo.cpp | 11 +
clang/test/DebugInfo/CXX/limited-ctor.cpp | 20 +-
clang/unittests/AST/DeclTest.cpp | 256 ++++++++++++++++++
6 files changed, 313 insertions(+), 2 deletions(-)
diff --git a/clang/include/clang/AST/CXXRecordDeclDefinitionBits.def b/clang/include/clang/AST/CXXRecordDeclDefinitionBits.def
index 97e61aaec7d51..a45a57ecfff20 100644
--- a/clang/include/clang/AST/CXXRecordDeclDefinitionBits.def
+++ b/clang/include/clang/AST/CXXRecordDeclDefinitionBits.def
@@ -64,6 +64,9 @@ FIELD(Abstract, 1, NO_MERGE)
/// language rules (including DRs).
FIELD(IsStandardLayout, 1, NO_MERGE)
+/// True when this class appears as a member of some standard-layout union.
+FIELD(IsStandardLayoutUnionMember, 1, MERGE_OR)
+
/// True when this class was standard-layout under the C++11
/// definition.
///
diff --git a/clang/include/clang/AST/DeclCXX.h b/clang/include/clang/AST/DeclCXX.h
index afe46fae1bceb..c46ab72cb1bc5 100644
--- a/clang/include/clang/AST/DeclCXX.h
+++ b/clang/include/clang/AST/DeclCXX.h
@@ -1233,6 +1233,15 @@ class CXXRecordDecl : public RecordDecl {
/// C++ [class]p7.
bool isStandardLayout() const { return data().IsStandardLayout; }
+ /// Determine whether this class appears as a member of a standard-layout
+ /// union.
+ bool isStandardLayoutUnionMember() const {
+ return data().IsStandardLayoutUnionMember;
+ }
+ void setIsStandardLayoutUnionMember(bool V = true) {
+ data().IsStandardLayoutUnionMember = V;
+ }
+
/// Determine whether this class was standard-layout per
/// C++11 [class]p7, specifically using the C++11 rules without any DRs.
bool isCXX11StandardLayout() const { return data().IsCXX11StandardLayout; }
diff --git a/clang/lib/AST/DeclCXX.cpp b/clang/lib/AST/DeclCXX.cpp
index f0da56542ae7e..476eb0bc2d5f3 100644
--- a/clang/lib/AST/DeclCXX.cpp
+++ b/clang/lib/AST/DeclCXX.cpp
@@ -76,7 +76,8 @@ void LazyASTUnresolvedSet::getFromExternalSource(ASTContext &C) const {
CXXRecordDecl::DefinitionData::DefinitionData(CXXRecordDecl *D)
: UserDeclaredConstructor(false), UserDeclaredSpecialMembers(0),
Aggregate(true), PlainOldData(true), Empty(true), Polymorphic(false),
- Abstract(false), IsStandardLayout(true), IsCXX11StandardLayout(true),
+ Abstract(false), IsStandardLayout(true),
+ IsStandardLayoutUnionMember(false), IsCXX11StandardLayout(true),
HasBasesWithFields(false), HasBasesWithNonStaticDataMembers(false),
HasPrivateFields(false), HasProtectedFields(false),
HasPublicFields(false), HasMutableFields(false), HasVariantMembers(false),
@@ -2321,6 +2322,19 @@ void CXXRecordDecl::completeDefinition(CXXFinalOverriderMap *FinalOverriders) {
data().IsStandardLayout = false;
data().IsCXX11StandardLayout = false;
}
+
+ if (isUnion() && isStandardLayout()) {
+ for (const FieldDecl *FD : fields()) {
+ if (const auto *RT =
+ FD->getType()->getBaseElementTypeUnsafe()->getAs<RecordType>()) {
+ if (auto *CRD =
+ dyn_cast<CXXRecordDecl>(RT->getDecl()->getDefinitionOrSelf())) {
+ if (CRD->hasDefinition() && CRD->isStandardLayout())
+ CRD->setIsStandardLayoutUnionMember(true);
+ }
+ }
+ }
+ }
}
bool CXXRecordDecl::mayBeAbstract() const {
diff --git a/clang/lib/CodeGen/CGDebugInfo.cpp b/clang/lib/CodeGen/CGDebugInfo.cpp
index 02864621d60a3..243961e26c516 100644
--- a/clang/lib/CodeGen/CGDebugInfo.cpp
+++ b/clang/lib/CodeGen/CGDebugInfo.cpp
@@ -3244,6 +3244,17 @@ static bool canUseCtorHoming(const CXXRecordDecl *RD) {
RD->hasConstexprNonCopyMoveConstructor())
return false;
+ // Skip this optimization if this type is standard-layout and is a member of
+ // some standard-layout union in this translation unit. Per the C++ spec, "it
+ // is permitted to inspect the common initial part of any of" the "common
+ // initial sequence" of distinct types in a standard-layout union. This
+ // exception to strict aliasing enables producing a reference to a type
+ // without ever having constructed that type.
+ //
+ // See: https://timsong-cpp.github.io/cppwp/n3337/class.mem#19
+ if (RD->isStandardLayoutUnionMember())
+ return false;
+
for (const CXXConstructorDecl *Ctor : RD->ctors()) {
if (Ctor->isCopyOrMoveConstructor())
continue;
diff --git a/clang/test/DebugInfo/CXX/limited-ctor.cpp b/clang/test/DebugInfo/CXX/limited-ctor.cpp
index e820c0703df4f..3fe1682358af2 100644
--- a/clang/test/DebugInfo/CXX/limited-ctor.cpp
+++ b/clang/test/DebugInfo/CXX/limited-ctor.cpp
@@ -53,7 +53,7 @@ struct DeclaredConstexpr {
template <class A, class B> struct Aliased {
A first;
B second;
- constexpr Aliased(const A &a, const B &b) : first(a), second(b) {}
+ Aliased(const A &a, const B &b) : first(a), second(b) {}
};
union AliasedSlot {
Aliased<const int, int> value;
@@ -66,6 +66,24 @@ int ReadAliasedSlot() {
return TestAliasedSlot.value.first;
}
+// CHECK-DAG: !DICompositeType(tag: DW_TAG_structure_type, name: "ConstexprAliased<int, int>"{{.*}}DIFlagTypePassByValue
+// CHECK-DAG: !DICompositeType(tag: DW_TAG_structure_type, name: "ConstexprAliased<const int, int>"{{.*}}DIFlagTypePassByValue
+template <class A, class B> struct ConstexprAliased {
+ A first;
+ B second;
+ constexpr ConstexprAliased(const A &a, const B &b) : first(a), second(b) {}
+};
+union ConstexprAliasedSlot {
+ ConstexprAliased<const int, int> value;
+ ConstexprAliased<int, int> mutable_value;
+ ConstexprAliasedSlot() {}
+ ~ConstexprAliasedSlot() {}
+} TestConstexprAliasedSlot;
+int ReadConstexprAliasedSlot() {
+ TestConstexprAliasedSlot.mutable_value = ConstexprAliased<int, int>(1, 2);
+ return TestConstexprAliasedSlot.value.first;
+}
+
// Defined out-of-line constexpr constructor should emit full debug info.
// CHECK-DAG: !DICompositeType(tag: DW_TAG_structure_type, name: "OutOfLineConstexpr"{{.*}}DIFlagTypePassByValue
struct OutOfLineConstexpr {
diff --git a/clang/unittests/AST/DeclTest.cpp b/clang/unittests/AST/DeclTest.cpp
index 195b8ab4c4e66..6fa1da7f01f00 100644
--- a/clang/unittests/AST/DeclTest.cpp
+++ b/clang/unittests/AST/DeclTest.cpp
@@ -24,10 +24,12 @@
#include "clang/Basic/LLVM.h"
#include "clang/Basic/Linkage.h"
#include "clang/Basic/TargetInfo.h"
+#include "clang/Frontend/ASTUnit.h"
#include "clang/Lex/Lexer.h"
#include "clang/Tooling/Tooling.h"
#include "llvm/Support/raw_ostream.h"
#include "llvm/Testing/Annotations/Annotations.h"
+#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include <cassert>
#include <memory>
@@ -36,6 +38,14 @@
using namespace clang::ast_matchers;
using namespace clang::tooling;
using namespace clang;
+using ::testing::AllOf;
+using ::testing::Not;
+using ::testing::Pointee;
+
+MATCHER(IsStandardLayout, "") { return arg.isStandardLayout(); }
+MATCHER(IsStandardLayoutUnionMember, "") {
+ return arg.isStandardLayoutUnionMember();
+}
TEST(Decl, CleansUpAPValues) {
MatchFinder Finder;
@@ -1029,3 +1039,249 @@ TEST(Decl, ObjCPropertyDeclNameForDiagnostic) {
/*Qualified=*/true);
EXPECT_EQ(ExtPQualifiedOS.str(), "-[MyClass extensionProp]");
}
+
+class StandardLayoutUnionMemberTest : public ::testing::Test {
+protected:
+ std::unique_ptr<ASTUnit> AST;
+ ASTContext *Ctx = nullptr;
+
+ void buildAST(StringRef Code, ArrayRef<std::string> Args = {"-std=c++11"}) {
+ AST = tooling::buildASTFromCodeWithArgs(Code, Args);
+ ASSERT_NE(AST, nullptr);
+ Ctx = &AST->getASTContext();
+ }
+
+ const CXXRecordDecl *findRecord(StringRef Name) const {
+ assert(Ctx && "ASTContext not initialized");
+ return selectFirst<CXXRecordDecl>(
+ "d",
+ match(cxxRecordDecl(hasName(Name), isDefinition()).bind("d"), *Ctx));
+ }
+
+ const CXXRecordDecl *findTemplate(StringRef Name,
+ StringRef TemplateArg) const {
+ assert(Ctx && "ASTContext not initialized");
+ return selectFirst<CXXRecordDecl>(
+ "d", match(classTemplateSpecializationDecl(
+ hasName(Name), isDefinition(),
+ hasTemplateArgument(
+ 0, refersToType(asString(TemplateArg.str()))))
+ .bind("d"),
+ *Ctx));
+ }
+
+ // Matches a standard-layout union member.
+ auto IsSLUnionMember() {
+ return Pointee(AllOf(IsStandardLayout(), IsStandardLayoutUnionMember()));
+ }
+
+ // Matches a standard-layout type that is not a member of a standard-layout
+ // union.
+ auto IsStandaloneSL() {
+ return Pointee(
+ AllOf(IsStandardLayout(), Not(IsStandardLayoutUnionMember())));
+ }
+
+ // Matches a non-standard-layout type.
+ auto IsNonSL() {
+ return Pointee(
+ AllOf(Not(IsStandardLayout()), Not(IsStandardLayoutUnionMember())));
+ }
+};
+
+TEST_F(StandardLayoutUnionMemberTest, StandaloneStruct) {
+ buildAST(R"cc(
+ struct StandaloneSL {
+ int x;
+ };
+ )cc");
+
+ EXPECT_THAT(findRecord("StandaloneSL"), IsStandaloneSL());
+}
+
+TEST_F(StandardLayoutUnionMemberTest, NonStandardLayoutStruct) {
+ buildAST(R"cc(
+ struct NonSL {
+ virtual void foo();
+ };
+ )cc");
+
+ EXPECT_THAT(findRecord("NonSL"), IsNonSL());
+}
+
+TEST_F(StandardLayoutUnionMemberTest, StandardLayoutUnion) {
+ buildAST(R"cc(
+ struct SLInUnion {
+ int x;
+ };
+
+ union SLUnion {
+ SLInUnion u;
+ };
+ )cc");
+
+ EXPECT_THAT(findRecord("SLUnion"), IsStandaloneSL());
+ EXPECT_THAT(findRecord("SLInUnion"), IsSLUnionMember());
+}
+
+TEST_F(StandardLayoutUnionMemberTest, TemplatedMember) {
+ buildAST(R"cc(
+ template <typename T>
+ struct TemplatedSL {
+ T x;
+ };
+
+ union TemplatedUnion {
+ TemplatedSL<int> a;
+ TemplatedSL<float> b;
+ };
+ )cc");
+
+ EXPECT_THAT(findRecord("TemplatedUnion"), IsStandaloneSL());
+ EXPECT_THAT(findTemplate("TemplatedSL", "int"), IsSLUnionMember());
+ EXPECT_THAT(findTemplate("TemplatedSL", "float"), IsSLUnionMember());
+}
+
+TEST_F(StandardLayoutUnionMemberTest, NonStandardLayoutUnion) {
+ buildAST(R"cc(
+ struct NonSL {
+ virtual void foo();
+ };
+
+ struct SLInNonSLUnion {
+ int x;
+ };
+
+ union NonSLUnion {
+ SLInNonSLUnion s;
+ NonSL n;
+ };
+ )cc");
+
+ EXPECT_THAT(findRecord("NonSLUnion"), Pointee(Not(IsStandardLayout())));
+ EXPECT_THAT(findRecord("NonSL"), IsNonSL());
+ EXPECT_THAT(findRecord("SLInNonSLUnion"), IsStandaloneSL());
+}
+
+TEST_F(StandardLayoutUnionMemberTest, NestedStruct) {
+ buildAST(R"cc(
+ union NestedUnion {
+ struct NestedSL {
+ int a;
+ } n;
+ };
+ )cc");
+
+ EXPECT_THAT(findRecord("NestedUnion"), IsStandaloneSL());
+ EXPECT_THAT(findRecord("NestedSL"), IsSLUnionMember());
+}
+
+TEST_F(StandardLayoutUnionMemberTest, Array) {
+ buildAST(R"cc(
+ struct SLInArray {
+ int x;
+ };
+ struct SLInMultiArray {
+ int y;
+ };
+ union ArrayUnion {
+ SLInArray arr[3];
+ SLInMultiArray multi_arr[2][4];
+ int raw;
+ };
+ )cc");
+
+ EXPECT_THAT(findRecord("ArrayUnion"), IsStandaloneSL());
+ EXPECT_THAT(findRecord("SLInArray"), IsSLUnionMember());
+ EXPECT_THAT(findRecord("SLInMultiArray"), IsSLUnionMember());
+}
+
+TEST_F(StandardLayoutUnionMemberTest, CVQualified) {
+ buildAST(R"cc(
+ struct SLConst {
+ int x;
+ };
+ struct SLVolatile {
+ int y;
+ };
+ union CVUnion {
+ const SLConst c;
+ volatile SLVolatile v;
+ int raw;
+ };
+ )cc");
+
+ EXPECT_THAT(findRecord("CVUnion"), IsStandaloneSL());
+ EXPECT_THAT(findRecord("SLConst"), IsSLUnionMember());
+ EXPECT_THAT(findRecord("SLVolatile"), IsSLUnionMember());
+}
+
+TEST_F(StandardLayoutUnionMemberTest, GenericUnionTemplate) {
+ buildAST(R"cc(
+ template <typename T>
+ union GenericUnion {
+ T val;
+ int raw;
+ };
+ struct SLInGenericUnion {
+ int x;
+ };
+ GenericUnion<SLInGenericUnion> u;
+ )cc");
+
+ EXPECT_THAT(findRecord("SLInGenericUnion"), IsSLUnionMember());
+}
+
+TEST_F(StandardLayoutUnionMemberTest, AnonymousUnion) {
+ buildAST(R"cc(
+ struct SLInAnonUnion {
+ int x;
+ };
+ struct EnclosingStruct {
+ union {
+ SLInAnonUnion a;
+ int b;
+ };
+ };
+ )cc");
+
+ EXPECT_THAT(findRecord("SLInAnonUnion"), IsSLUnionMember());
+}
+
+TEST_F(StandardLayoutUnionMemberTest, Inheritance) {
+ buildAST(R"cc(
+ struct EmptyBase {};
+ struct SLDerived : EmptyBase {
+ int x;
+ };
+ union DerivedUnion {
+ SLDerived d;
+ int raw;
+ };
+ )cc");
+
+ EXPECT_THAT(findRecord("DerivedUnion"), IsStandaloneSL());
+ EXPECT_THAT(findRecord("SLDerived"), IsSLUnionMember());
+ EXPECT_THAT(findRecord("EmptyBase"), IsStandaloneSL());
+}
+
+TEST_F(StandardLayoutUnionMemberTest, AnonymousTypedefStruct) {
+ buildAST(R"cc(
+ typedef struct {
+ int x;
+ } AnonTypedefSL;
+
+ union AnonTypedefUnion {
+ AnonTypedefSL s;
+ int raw;
+ };
+ )cc");
+
+ const auto *TD = selectFirst<TypedefDecl>(
+ "d", match(typedefDecl(hasName("AnonTypedefSL")).bind("d"), *Ctx));
+ ASSERT_NE(TD, nullptr);
+ const auto *AnonRecord = TD->getUnderlyingType()->getAsCXXRecordDecl();
+
+ EXPECT_THAT(findRecord("AnonTypedefUnion"), IsStandaloneSL());
+ EXPECT_THAT(AnonRecord, IsSLUnionMember());
+}
>From fcd577c3ab7de9fec673db5730c185d89b879e07 Mon Sep 17 00:00:00 2001
From: Clayton Knittel <cknit1999 at gmail.com>
Date: Mon, 7 Sep 2026 21:18:37 -0700
Subject: [PATCH 2/2] [DebugInfo] Ignore undefined constexpr constructors in
constructor homing.
This is a roll-forward of
https://github.com/llvm/llvm-project/pull/218165, which inadvertantly
caused missing debug info due to an existing issue with standard-layout
unions and layout-compatible aliasing (see
[[class.mem]](https://timsong-cpp.github.io/cppwp/n3337/class.mem#19)).
Constexpr constructors without a visible definition in the TU are not
callable in a constexpr context, which means it is not possible to
construct a type in constexpr without emitting debug info for the
constructor itself. Although undefined constexpr constructors are still
callable, they are normal out of line functions, requiring a definition
to link against.
---
clang/lib/CodeGen/CGDebugInfo.cpp | 20 +++++++++++++++-----
clang/test/DebugInfo/CXX/limited-ctor.cpp | 7 +++----
2 files changed, 18 insertions(+), 9 deletions(-)
diff --git a/clang/lib/CodeGen/CGDebugInfo.cpp b/clang/lib/CodeGen/CGDebugInfo.cpp
index 243961e26c516..38bb67543ebc6 100644
--- a/clang/lib/CodeGen/CGDebugInfo.cpp
+++ b/clang/lib/CodeGen/CGDebugInfo.cpp
@@ -3239,9 +3239,14 @@ static bool canUseCtorHoming(const CXXRecordDecl *RD) {
if (isClassOrMethodDLLImport(RD))
return false;
- if (RD->isLambda() || RD->isAggregate() ||
- RD->hasTrivialDefaultConstructor() ||
- RD->hasConstexprNonCopyMoveConstructor())
+ if (RD->isLambda() || RD->isAggregate() || RD->hasTrivialDefaultConstructor())
+ return false;
+
+ // Skip this optimization if the class has an implicit constexpr default
+ // constructor, since those constructors can be invoked without emitting type
+ // information for the constructor.
+ if (RD->needsImplicitDefaultConstructor() &&
+ RD->defaultedDefaultConstructorIsConstexpr())
return false;
// Skip this optimization if this type is standard-layout and is a member of
@@ -3255,6 +3260,7 @@ static bool canUseCtorHoming(const CXXRecordDecl *RD) {
if (RD->isStandardLayoutUnionMember())
return false;
+ bool HasNonDeletedCtor = false;
for (const CXXConstructorDecl *Ctor : RD->ctors()) {
if (Ctor->isCopyOrMoveConstructor())
continue;
@@ -3265,11 +3271,15 @@ static bool canUseCtorHoming(const CXXRecordDecl *RD) {
// copy/move constructor, which does not enable homing.
if (CtorDef->isDelegatingConstructor())
continue;
+ // Skip this optimization if we see a defined constexpr constructor, which
+ // can be invoked without emitting type info.
+ if (Ctor->isConstexpr() && !Ctor->isDeleted())
+ return false;
}
if (!Ctor->isDeleted())
- return true;
+ HasNonDeletedCtor = true;
}
- return false;
+ return HasNonDeletedCtor;
}
static bool shouldOmitDefinition(llvm::codegenoptions::DebugInfoKind DebugKind,
diff --git a/clang/test/DebugInfo/CXX/limited-ctor.cpp b/clang/test/DebugInfo/CXX/limited-ctor.cpp
index 3fe1682358af2..6394c01a467c6 100644
--- a/clang/test/DebugInfo/CXX/limited-ctor.cpp
+++ b/clang/test/DebugInfo/CXX/limited-ctor.cpp
@@ -27,10 +27,9 @@ struct E {
constexpr E(){};
} TestE;
-// Restored by this revert: a constexpr constructor that is only declared keeps
-// the class exempt from constructor homing. See Aliased below for a case where
-// narrowing the exemption to defined constructors homes the type nowhere.
-// CHECK-DAG: !DICompositeType(tag: DW_TAG_structure_type, name: "DeclaredConstexpr"{{.*}}DIFlagTypePassByValue
+// Declared but not defined constexpr constructor should not emit full debug
+// info.
+// CHECK-DAG: !DICompositeType(tag: DW_TAG_structure_type, name: "DeclaredConstexpr"{{.*}}flags: DIFlagFwdDecl
struct DeclaredConstexpr {
constexpr DeclaredConstexpr();
} TestDeclaredConstexpr;
More information about the cfe-commits
mailing list