Skip to content

[Clang] Explain why a type trait evaluated to false. #141238

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
May 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions clang/include/clang/Basic/DiagnosticSemaKinds.td
Original file line number Diff line number Diff line change
Expand Up @@ -1762,6 +1762,29 @@ def err_user_defined_msg_constexpr : Error<
"%sub{subst_user_defined_msg}0 must be produced by a "
"constant expression">;

// Type traits explanations
def note_unsatisfied_trait : Note<"%0 is not %enum_select<TraitName>{"
"%TriviallyRelocatable{trivially relocatable}"
"}1">;

def note_unsatisfied_trait_reason
: Note<"because it "
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My word... I'm VERY glad we have enum_select :D

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, in these cases it's super useful, thanks :D

"%enum_select<TraitNotSatisfiedReason>{"
"%Ref{is a reference type}|"
"%HasArcLifetime{has an ARC lifetime qualifier}|"
"%VLA{is a variably-modified type}|"
"%VBase{has a virtual base %1}|"
"%NRBase{has a non-trivially-relocatable base %1}|"
"%NRField{has a non-trivially-relocatable member %1 of type %2}|"
"%DeletedDtr{has a %select{deleted|user-provided}1 destructor}|"
"%UserProvidedCtr{has a user provided %select{copy|move}1 "
"constructor}|"
"%UserProvidedAssign{has a user provided %select{copy|move}1 "
"assignment operator}|"
"%UnionWithUserDeclaredSMF{is a union with a user-declared "
"%sub{select_special_member_kind}1}"
"}0">;

def warn_consteval_if_always_true : Warning<
"consteval if is always true in an %select{unevaluated|immediate}0 context">,
InGroup<DiagGroup<"redundant-consteval-if">>;
Expand Down
5 changes: 5 additions & 0 deletions clang/include/clang/Sema/Sema.h
Original file line number Diff line number Diff line change
Expand Up @@ -5910,6 +5910,11 @@ class Sema final : public SemaBase {
/// with expression \E
void DiagnoseStaticAssertDetails(const Expr *E);

/// If E represents a built-in type trait, or a known standard type trait,
/// try to print more information about why the type type-trait failed.
/// This assumes we already evaluated the expression to a false boolean value.
void DiagnoseTypeTraitDetails(const Expr *E);

/// Handle a friend type declaration. This works in tandem with
/// ActOnTag.
///
Expand Down
1 change: 1 addition & 0 deletions clang/lib/Sema/SemaConcept.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1320,6 +1320,7 @@ static void diagnoseWellFormedUnsatisfiedConstraintExpr(Sema &S,
S.Diag(SubstExpr->getSourceRange().getBegin(),
diag::note_atomic_constraint_evaluated_to_false)
<< (int)First << SubstExpr;
S.DiagnoseTypeTraitDetails(SubstExpr);
}

template <typename SubstitutionDiagnostic>
Expand Down
2 changes: 2 additions & 0 deletions clang/lib/Sema/SemaDeclCXX.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17694,6 +17694,8 @@ void Sema::DiagnoseStaticAssertDetails(const Expr *E) {
<< DiagSide[0].ValueString << Op->getOpcodeStr()
<< DiagSide[1].ValueString << Op->getSourceRange();
}
} else {
DiagnoseTypeTraitDetails(E);
}
}

Expand Down
185 changes: 185 additions & 0 deletions clang/lib/Sema/SemaTypeTraits.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1917,3 +1917,188 @@ ExprResult Sema::BuildExpressionTrait(ExpressionTrait ET, SourceLocation KWLoc,
return new (Context)
ExpressionTraitExpr(KWLoc, ET, Queried, Value, RParen, Context.BoolTy);
}

static std::optional<TypeTrait> StdNameToTypeTrait(StringRef Name) {
return llvm::StringSwitch<std::optional<TypeTrait>>(Name)
.Case("is_trivially_relocatable",
TypeTrait::UTT_IsCppTriviallyRelocatable)
.Default(std::nullopt);
}

using ExtractedTypeTraitInfo =
std::optional<std::pair<TypeTrait, llvm::SmallVector<QualType, 1>>>;

// Recognize type traits that are builting type traits, or known standard
// type traits in <type_traits>. Note that at this point we assume the
// trait evaluated to false, so we need only to recognize the shape of the
// outer-most symbol.
static ExtractedTypeTraitInfo ExtractTypeTraitFromExpression(const Expr *E) {
llvm::SmallVector<QualType, 1> Args;
std::optional<TypeTrait> Trait;

// builtins
if (const auto *TraitExpr = dyn_cast<TypeTraitExpr>(E)) {
Trait = TraitExpr->getTrait();
for (const auto *Arg : TraitExpr->getArgs())
Args.push_back(Arg->getType());
return {{Trait.value(), std::move(Args)}};
}
const auto *Ref = dyn_cast<DeclRefExpr>(E);
if (!Ref)
return std::nullopt;

// std::is_xxx_v<>
if (const auto *VD =
dyn_cast<VarTemplateSpecializationDecl>(Ref->getDecl())) {
if (!VD->isInStdNamespace())
return std::nullopt;
StringRef Name = VD->getIdentifier()->getName();
if (!Name.consume_back("_v"))
return std::nullopt;
Trait = StdNameToTypeTrait(Name);
if (!Trait)
return std::nullopt;
for (const auto &Arg : VD->getTemplateArgs().asArray())
Args.push_back(Arg.getAsType());
return {{Trait.value(), std::move(Args)}};
}

// std::is_xxx<>::value
if (const auto *VD = dyn_cast<VarDecl>(Ref->getDecl());
Ref->hasQualifier() && VD && VD->getIdentifier()->isStr("value")) {
const Type *T = Ref->getQualifier()->getAsType();
if (!T)
return std::nullopt;
const TemplateSpecializationType *Ts =
T->getAs<TemplateSpecializationType>();
if (!Ts)
return std::nullopt;
const TemplateDecl *D = Ts->getTemplateName().getAsTemplateDecl();
if (!D || !D->isInStdNamespace())
return std::nullopt;
Trait = StdNameToTypeTrait(D->getIdentifier()->getName());
if (!Trait)
return std::nullopt;
for (const auto &Arg : Ts->template_arguments())
Args.push_back(Arg.getAsType());
return {{Trait.value(), std::move(Args)}};
}
return std::nullopt;
}

static void DiagnoseNonTriviallyRelocatableReason(Sema &SemaRef,
SourceLocation Loc,
const CXXRecordDecl *D) {
for (const CXXBaseSpecifier &B : D->bases()) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do a if (D->isInvalidDecl()) return; here. Else this can result in some weirdness at times I think?

const auto *BaseDecl = B.getType()->getAsCXXRecordDecl();
assert(BaseDecl && "invalid base?");
if (B.isVirtual())
SemaRef.Diag(Loc, diag::note_unsatisfied_trait_reason)
<< diag::TraitNotSatisfiedReason::VBase << B.getType()
<< B.getSourceRange();
if (!SemaRef.IsCXXTriviallyRelocatableType(B.getType()))
SemaRef.Diag(Loc, diag::note_unsatisfied_trait_reason)
<< diag::TraitNotSatisfiedReason::NRBase << B.getType()
<< B.getSourceRange();
}
for (const FieldDecl *Field : D->fields()) {
if (!Field->getType()->isReferenceType() &&
!SemaRef.IsCXXTriviallyRelocatableType(Field->getType()))
SemaRef.Diag(Loc, diag::note_unsatisfied_trait_reason)
<< diag::TraitNotSatisfiedReason::NRField << Field << Field->getType()
<< Field->getSourceRange();
}
if (D->hasDeletedDestructor())
SemaRef.Diag(Loc, diag::note_unsatisfied_trait_reason)
<< diag::TraitNotSatisfiedReason::DeletedDtr << /*Deleted*/ 0
<< D->getDestructor()->getSourceRange();

if (D->hasAttr<TriviallyRelocatableAttr>())
return;

if (D->isUnion()) {
auto DiagSPM = [&](CXXSpecialMemberKind K, bool Has) {
if (Has)
SemaRef.Diag(Loc, diag::note_unsatisfied_trait_reason)
<< diag::TraitNotSatisfiedReason::UnionWithUserDeclaredSMF << K;
};
DiagSPM(CXXSpecialMemberKind::CopyConstructor,
D->hasUserDeclaredCopyConstructor());
DiagSPM(CXXSpecialMemberKind::CopyAssignment,
D->hasUserDeclaredCopyAssignment());
DiagSPM(CXXSpecialMemberKind::MoveConstructor,
D->hasUserDeclaredMoveConstructor());
DiagSPM(CXXSpecialMemberKind::MoveAssignment,
D->hasUserDeclaredMoveAssignment());
return;
}

if (!D->hasSimpleMoveConstructor() && !D->hasSimpleCopyConstructor()) {
const auto *Decl = cast<CXXConstructorDecl>(
LookupSpecialMemberFromXValue(SemaRef, D, /*Assign=*/false));
if (Decl && Decl->isUserProvided())
SemaRef.Diag(Loc, diag::note_unsatisfied_trait_reason)
<< diag::TraitNotSatisfiedReason::UserProvidedCtr
<< Decl->isMoveConstructor() << Decl->getSourceRange();
}
if (!D->hasSimpleMoveAssignment() && !D->hasSimpleCopyAssignment()) {
CXXMethodDecl *Decl =
LookupSpecialMemberFromXValue(SemaRef, D, /*Assign=*/true);
if (Decl && Decl->isUserProvided())
SemaRef.Diag(Loc, diag::note_unsatisfied_trait_reason)
<< diag::TraitNotSatisfiedReason::UserProvidedAssign
<< Decl->isMoveAssignmentOperator() << Decl->getSourceRange();
}
CXXDestructorDecl *Dtr = D->getDestructor();
if (Dtr && Dtr->isUserProvided() && !Dtr->isDefaulted())
SemaRef.Diag(Loc, diag::note_unsatisfied_trait_reason)
<< diag::TraitNotSatisfiedReason::DeletedDtr << /*User Provided*/ 1
<< Dtr->getSourceRange();
}

static void DiagnoseNonTriviallyRelocatableReason(Sema &SemaRef,
SourceLocation Loc,
QualType T) {
SemaRef.Diag(Loc, diag::note_unsatisfied_trait)
<< T << diag::TraitName::TriviallyRelocatable;
if (T->isVariablyModifiedType())
SemaRef.Diag(Loc, diag::note_unsatisfied_trait_reason)
<< diag::TraitNotSatisfiedReason::VLA;

if (T->isReferenceType())
SemaRef.Diag(Loc, diag::note_unsatisfied_trait_reason)
<< diag::TraitNotSatisfiedReason::Ref;
T = T.getNonReferenceType();

if (T.hasNonTrivialObjCLifetime())
SemaRef.Diag(Loc, diag::note_unsatisfied_trait_reason)
<< diag::TraitNotSatisfiedReason::HasArcLifetime;

const CXXRecordDecl *D = T->getAsCXXRecordDecl();
if (!D || D->isInvalidDecl())
return;

if (D->hasDefinition())
DiagnoseNonTriviallyRelocatableReason(SemaRef, Loc, D);

SemaRef.Diag(D->getLocation(), diag::note_defined_here) << D;
}

void Sema::DiagnoseTypeTraitDetails(const Expr *E) {
E = E->IgnoreParenImpCasts();
if (E->containsErrors())
return;

ExtractedTypeTraitInfo TraitInfo = ExtractTypeTraitFromExpression(E);
if (!TraitInfo)
return;

const auto &[Trait, Args] = TraitInfo.value();
switch (Trait) {
case UTT_IsCppTriviallyRelocatable:
DiagnoseNonTriviallyRelocatableReason(*this, E->getBeginLoc(), Args[0]);
break;
default:
break;
}
}
101 changes: 101 additions & 0 deletions clang/test/SemaCXX/type-traits-unsatisfied-diags-std.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// RUN: %clang_cc1 -fsyntax-only -verify -std=c++20 -DSTD1 %s
// RUN: %clang_cc1 -fsyntax-only -verify -std=c++20 -DSTD2 %s
// RUN: %clang_cc1 -fsyntax-only -verify -std=c++20 -DSTD3 %s

namespace std {

#ifdef STD1
template <typename T>
struct is_trivially_relocatable {
static constexpr bool value = __builtin_is_cpp_trivially_relocatable(T);
};

template <typename T>
constexpr bool is_trivially_relocatable_v = __builtin_is_cpp_trivially_relocatable(T);
#endif

#ifdef STD2
template <typename T>
struct __details_is_trivially_relocatable {
static constexpr bool value = __builtin_is_cpp_trivially_relocatable(T);
};

template <typename T>
using is_trivially_relocatable = __details_is_trivially_relocatable<T>;

template <typename T>
constexpr bool is_trivially_relocatable_v = __builtin_is_cpp_trivially_relocatable(T);
#endif


#ifdef STD3
template< class T, T v >
struct integral_constant {
static constexpr T value = v;
};

template< bool B >
using bool_constant = integral_constant<bool, B>;

template <typename T>
struct __details_is_trivially_relocatable : bool_constant<__builtin_is_cpp_trivially_relocatable(T)> {};

template <typename T>
using is_trivially_relocatable = __details_is_trivially_relocatable<T>;

template <typename T>
constexpr bool is_trivially_relocatable_v = is_trivially_relocatable<T>::value;
#endif

}

static_assert(std::is_trivially_relocatable<int>::value);

static_assert(std::is_trivially_relocatable<int&>::value);
// expected-error-re@-1 {{static assertion failed due to requirement 'std::{{.*}}is_trivially_relocatable<int &>::value'}} \
// expected-note@-1 {{'int &' is not trivially relocatable}} \
// expected-note@-1 {{because it is a reference type}}
static_assert(std::is_trivially_relocatable_v<int&>);
// expected-error@-1 {{static assertion failed due to requirement 'std::is_trivially_relocatable_v<int &>'}} \
// expected-note@-1 {{'int &' is not trivially relocatable}} \
// expected-note@-1 {{because it is a reference type}}

namespace test_namespace {
using namespace std;
static_assert(is_trivially_relocatable<int&>::value);
// expected-error-re@-1 {{static assertion failed due to requirement '{{.*}}is_trivially_relocatable<int &>::value'}} \
// expected-note@-1 {{'int &' is not trivially relocatable}} \
// expected-note@-1 {{because it is a reference type}}
static_assert(is_trivially_relocatable_v<int&>);
// expected-error@-1 {{static assertion failed due to requirement 'is_trivially_relocatable_v<int &>'}} \
// expected-note@-1 {{'int &' is not trivially relocatable}} \
// expected-note@-1 {{because it is a reference type}}
}


namespace concepts {
template <typename T>
requires std::is_trivially_relocatable<T>::value void f(); // #cand1

template <typename T>
concept C = std::is_trivially_relocatable_v<T>; // #concept2

template <C T> void g(); // #cand2

void test() {
f<int&>();
// expected-error@-1 {{no matching function for call to 'f'}} \
// expected-note@#cand1 {{candidate template ignored: constraints not satisfied [with T = int &]}} \
// expected-note-re@#cand1 {{because '{{.*}}is_trivially_relocatable<int &>::value' evaluated to false}} \
// expected-note@#cand1 {{'int &' is not trivially relocatable}} \
// expected-note@#cand1 {{because it is a reference type}}

g<int&>();
// expected-error@-1 {{no matching function for call to 'g'}} \
// expected-note@#cand2 {{candidate template ignored: constraints not satisfied [with T = int &]}} \
// expected-note@#cand2 {{because 'int &' does not satisfy 'C'}} \
// expected-note@#concept2 {{because 'std::is_trivially_relocatable_v<int &>' evaluated to false}} \
// expected-note@#concept2 {{'int &' is not trivially relocatable}} \
// expected-note@#concept2 {{because it is a reference type}}
}
}
Loading