[clang] Skip dllexport of inherited constructors with unsatisfied constraints (#186497)

When a class is marked `__declspec(dllexport)`, Clang eagerly creates
inherited constructors via `findInheritingConstructor` and propagates
the dllexport attribute to all members. This bypasses overload
resolution, which would normally filter out constructors whose requires
clause is not satisfied. As a result, Clang attempted to instantiate
constructor bodies that should never be available, causing spurious
compilation errors.

Add constraint satisfaction checks in `checkClassLevelDLLAttribute` to
match MSVC behavior:

1. Before eagerly creating inherited constructors, verify that the base
constructor's `requires` clause is satisfied. Skip creation otherwise.

2. Before applying dllexport to non-inherited methods of class template
specializations, verify constraint satisfaction. This handles the case
where `dllexport` propagates to a base template specialization whose own
members have unsatisfied constraints.

Inherited constructors skip the second check since their constraints
were already verified at creation time.

Fixes #185924

Followup to https://github.com/llvm/llvm-project/pull/182706

Assisted by: Cursor // Claude Opus 4.6
This commit is contained in:
Chinmay Deshpande 2026-03-15 19:35:04 -07:00 committed by GitHub
parent 655d5e7f69
commit 696e82db33
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 174 additions and 34 deletions

View File

@ -6595,10 +6595,21 @@ void Sema::checkClassLevelDLLAttribute(CXXRecordDecl *Class) {
for (Decl *D : Class->decls())
if (auto *S = dyn_cast<ConstructorUsingShadowDecl>(D))
Shadows.push_back(S);
for (ConstructorUsingShadowDecl *S : Shadows)
if (auto *BC = dyn_cast<CXXConstructorDecl>(S->getTargetDecl());
BC && !BC->isDeleted())
findInheritingConstructor(Class->getLocation(), BC, S);
for (ConstructorUsingShadowDecl *S : Shadows) {
CXXConstructorDecl *BC = dyn_cast<CXXConstructorDecl>(S->getTargetDecl());
if (!BC || BC->isDeleted())
continue;
// Skip constructors whose requires clause is not satisfied.
// Normally overload resolution filters these, but we are bypassing
// it to eagerly create inherited constructors for dllexport.
if (BC->getTrailingRequiresClause()) {
ConstraintSatisfaction Satisfaction;
if (CheckFunctionConstraints(BC, Satisfaction) ||
!Satisfaction.IsSatisfied)
continue;
}
findInheritingConstructor(Class->getLocation(), BC, S);
}
}
// FIXME: MSVC's docs say all bases must be exportable, but this doesn't
@ -6621,38 +6632,53 @@ void Sema::checkClassLevelDLLAttribute(CXXRecordDecl *Class) {
if (MD->isDeleted())
continue;
// Don't export inherited constructors whose parameters prevent ABI-
// compatible forwarding. When canEmitDelegateCallArgs (in CodeGen)
// returns false, Clang inlines the constructor body instead of
// emitting a forwarding thunk, producing code that is not ABI-
// compatible with MSVC. Suppress the export and warn so the user
// gets a linker error rather than a silent runtime mismatch.
if (ClassExported) {
if (auto *CD = dyn_cast<CXXConstructorDecl>(MD)) {
if (CD->getInheritedConstructor()) {
if (CD->isVariadic()) {
CXXConstructorDecl *CD = dyn_cast<CXXConstructorDecl>(MD);
if (CD && CD->getInheritedConstructor()) {
// Inherited constructors already had their base constructor's
// constraints checked before creation via
// findInheritingConstructor, so only ABI-compatibility checks
// are needed here.
//
// Don't export inherited constructors whose parameters prevent
// ABI-compatible forwarding. When canEmitDelegateCallArgs (in
// CodeGen) returns false, Clang inlines the constructor body
// instead of emitting a forwarding thunk, producing code that
// is not ABI-compatible with MSVC. Suppress the export and warn
// so the user gets a linker error rather than a silent runtime
// mismatch.
if (CD->isVariadic()) {
Diag(CD->getLocation(),
diag::warn_dllexport_inherited_ctor_unsupported)
<< /*variadic=*/0;
continue;
}
if (Context.getTargetInfo()
.getCXXABI()
.areArgsDestroyedLeftToRightInCallee()) {
bool HasCalleeCleanupParam = false;
for (const ParmVarDecl *P : CD->parameters())
if (P->needsDestruction(Context)) {
HasCalleeCleanupParam = true;
break;
}
if (HasCalleeCleanupParam) {
Diag(CD->getLocation(),
diag::warn_dllexport_inherited_ctor_unsupported)
<< /*variadic=*/0;
<< /*callee-cleanup=*/1;
continue;
}
if (Context.getTargetInfo()
.getCXXABI()
.areArgsDestroyedLeftToRightInCallee()) {
bool HasCalleeCleanupParam = false;
for (const auto *P : CD->parameters())
if (P->needsDestruction(Context)) {
HasCalleeCleanupParam = true;
break;
}
if (HasCalleeCleanupParam) {
Diag(CD->getLocation(),
diag::warn_dllexport_inherited_ctor_unsupported)
<< /*callee-cleanup=*/1;
continue;
}
}
}
} else if (MD->getTrailingRequiresClause()) {
// Don't export methods whose requires clause is not satisfied.
// For class template specializations, member constraints may
// depend on template arguments and an unsatisfied constraint
// means the member should not be available in this
// specialization.
ConstraintSatisfaction Satisfaction;
if (CheckFunctionConstraints(MD, Satisfaction) ||
!Satisfaction.IsSatisfied)
continue;
}
}

View File

@ -1,7 +1,7 @@
// RUN: %clang_cc1 -no-enable-noundef-analysis -triple x86_64-windows-msvc -emit-llvm -std=c++17 -fms-extensions -O0 -o - %s | FileCheck --check-prefix=MSVC %s
// RUN: %clang_cc1 -no-enable-noundef-analysis -triple i686-windows-msvc -emit-llvm -std=c++17 -fms-extensions -O0 -o - %s | FileCheck --check-prefix=M32 %s
// RUN: %clang_cc1 -no-enable-noundef-analysis -triple x86_64-windows-gnu -emit-llvm -std=c++17 -fms-extensions -O0 -o - %s | FileCheck --check-prefix=GNU %s
// RUN: %clang_cc1 -no-enable-noundef-analysis -triple x86_64-windows-msvc -emit-llvm -std=c++17 -fms-extensions -fno-dllexport-inlines -O0 -o - %s | FileCheck --check-prefix=NOINLINE %s
// RUN: %clang_cc1 -no-enable-noundef-analysis -triple x86_64-windows-msvc -emit-llvm -std=c++20 -fms-extensions -O0 -o - %s | FileCheck --check-prefix=MSVC %s
// RUN: %clang_cc1 -no-enable-noundef-analysis -triple i686-windows-msvc -emit-llvm -std=c++20 -fms-extensions -O0 -o - %s | FileCheck --check-prefix=M32 %s
// RUN: %clang_cc1 -no-enable-noundef-analysis -triple x86_64-windows-gnu -emit-llvm -std=c++20 -fms-extensions -O0 -o - %s | FileCheck --check-prefix=GNU %s
// RUN: %clang_cc1 -no-enable-noundef-analysis -triple x86_64-windows-msvc -emit-llvm -std=c++20 -fms-extensions -fno-dllexport-inlines -O0 -o - %s | FileCheck --check-prefix=NOINLINE %s
// Test that inherited constructors via 'using Base::Base' in a dllexport
// class are properly exported (https://github.com/llvm/llvm-project/issues/162640).
@ -241,3 +241,77 @@ struct __declspec(dllexport) CalleeCleanupChild : CalleeCleanupBase {
// The implicit default ctor is a regular inline method, NOT an inherited
// constructor, so -fno-dllexport-inlines correctly suppresses it.
// NOINLINE-NOT: define {{.*}}dllexport{{.*}} @"??0AllDefChild@@QEAA@XZ"
//===----------------------------------------------------------------------===//
// Constrained constructors: inherited constructors whose requires clause is
// not satisfied should not be exported.
// Regression test for https://github.com/llvm/llvm-project/issues/185924
//===----------------------------------------------------------------------===//
template <bool B>
struct ConstrainedBase {
struct Enabler {};
ConstrainedBase(Enabler) requires(B) {}
ConstrainedBase() requires(B) : ConstrainedBase(Enabler{}) {}
ConstrainedBase(int);
};
// B=false: both the default ctor and the Enabler ctor have requires(B) which
// is not satisfied. Only the inherited ConstrainedChild(int) should be
// exported.
struct __declspec(dllexport) ConstrainedChild : ConstrainedBase<false> {
using ConstrainedBase::ConstrainedBase;
};
// MSVC-DAG: define weak_odr dso_local dllexport {{.*}} @"??0ConstrainedChild@@QEAA@H@Z"
// M32-DAG: define weak_odr dso_local dllexport {{.*}} @"??0ConstrainedChild@@QAE@H@Z"
// GNU-DAG: define {{.*}}dso_local dllexport {{.*}} @_ZN16ConstrainedChildCI115ConstrainedBaseILb0EEEi(
// The constrained constructors should NOT be exported.
// MSVC-NOT: dllexport{{.*}}ConstrainedChild@@QEAA@XZ
// M32-NOT: dllexport{{.*}}ConstrainedChild@@QAE@XZ
// GNU-NOT: dllexport{{.*}}ConstrainedBaseILb0EEEv
// Constrained non-default constructor: only export when the constraint is met.
template <typename T>
struct SelectiveBase {
SelectiveBase(int) requires(sizeof(T) > 1) {}
SelectiveBase(double);
};
// sizeof(char)==1, so SelectiveBase(int) requires(sizeof(char)>1) is not
// satisfied. Only the SelectiveChild(double) constructor should be exported.
struct __declspec(dllexport) SelectiveChild : SelectiveBase<char> {
using SelectiveBase::SelectiveBase;
};
// MSVC-DAG: define weak_odr dso_local dllexport {{.*}} @"??0SelectiveChild@@QEAA@N@Z"
// M32-DAG: define weak_odr dso_local dllexport {{.*}} @"??0SelectiveChild@@QAE@N@Z"
// GNU-DAG: define {{.*}}dso_local dllexport {{.*}} @_ZN14SelectiveChildCI113SelectiveBaseIcEEd(
// The constrained int constructor should NOT be exported.
// MSVC-NOT: dllexport{{.*}}SelectiveChild@@QEAA@H@Z
// M32-NOT: dllexport{{.*}}SelectiveChild@@QAE@H@Z
// GNU-NOT: dllexport{{.*}}SelectiveBaseIcEEi
//===----------------------------------------------------------------------===//
// Non-constructor constrained method: when dllexport propagates to a base
// template specialization, methods with unsatisfied constraints should not
// be exported.
//===----------------------------------------------------------------------===//
template <typename T>
struct BaseWithConstrainedMethod {
void foo() requires(sizeof(T) > 100) { T::nonexistent(); }
void bar() {}
};
struct __declspec(dllexport) MethodChild : BaseWithConstrainedMethod<int> {};
// bar() should be exported (no constraint).
// MSVC-DAG: define {{.*}}dllexport {{.*}} @"?bar@?$BaseWithConstrainedMethod@H@@QEAAXXZ"
// M32-DAG: define {{.*}}dllexport {{.*}} @"?bar@?$BaseWithConstrainedMethod@H@@QAEXXZ"
// foo() should NOT be exported (constraint not satisfied).
// MSVC-NOT: dllexport{{.*}}foo@?$BaseWithConstrainedMethod@H
// M32-NOT: dllexport{{.*}}foo@?$BaseWithConstrainedMethod@H

View File

@ -0,0 +1,40 @@
// RUN: %clang_cc1 -triple x86_64-windows-msvc -fsyntax-only -fms-extensions -verify -std=c++20 %s
// RUN: %clang_cc1 -triple x86_64-windows-gnu -fsyntax-only -fms-extensions -verify -std=c++20 %s
// expected-no-diagnostics
// Regression test for https://github.com/llvm/llvm-project/issues/185924
// dllexport should not attempt to instantiate inherited constructors whose
// requires clause is not satisfied.
//
// This exercises two paths in checkClassLevelDLLAttribute:
// 1) findInheritingConstructor must skip constrained-out base ctors
// 2) dllexport propagated to the base template specialization must not
// export members whose requires clause is not satisfied
//
// The constructor/method bodies are intentionally ill-formed when the
// constraint is not satisfied, so that forced instantiation via dllexport
// would produce an error without the correct fix.
template <bool B>
struct ConstrainedBase {
struct Enabler {};
ConstrainedBase(Enabler) requires(B) {}
ConstrainedBase() requires(B) : ConstrainedBase(Enabler{}) {}
ConstrainedBase(int);
};
struct __declspec(dllexport) ConstrainedChild : ConstrainedBase<false> {
using ConstrainedBase::ConstrainedBase;
};
// Non-constructor constrained method on a base template specialization.
// When dllexport propagates to the base, methods whose requires clause
// is not satisfied must be skipped.
template <typename T>
struct BaseWithConstrainedMethod {
void foo() requires(sizeof(T) > 100) { T::nonexistent(); }
void bar() {}
};
struct __declspec(dllexport) MethodChild : BaseWithConstrainedMethod<int> {};