[clang] [Clang] Diagnose unsatisfied `std::is_assignable`. (PR #144836)

Ross Kirsling via cfe-commits cfe-commits at lists.llvm.org
Tue Jun 24 11:56:02 PDT 2025


https://github.com/rkirsling updated https://github.com/llvm/llvm-project/pull/144836

>From 580750339643b627552a1c726909dd5d12f3c9af Mon Sep 17 00:00:00 2001
From: Ross Kirsling <ross.kirsling at sony.com>
Date: Wed, 18 Jun 2025 21:54:35 -0700
Subject: [PATCH 1/2] [Clang] Diagnose unsatisfied `std::is_assignable`.

Part of the work for #141911.

Checking `is_assignable<T, U>` boils down to checking the well-formedness of `declval<T>() = declval<U>()`; this PR recycles logic from EvaluateBinaryTypeTrait in order to produce the relevant diagnostics.
---
 clang/lib/Sema/SemaTypeTraits.cpp             | 29 ++++++++
 .../type-traits-unsatisfied-diags-std.cpp     | 64 +++++++++++++++++
 .../SemaCXX/type-traits-unsatisfied-diags.cpp | 71 +++++++++++++++++++
 3 files changed, 164 insertions(+)

diff --git a/clang/lib/Sema/SemaTypeTraits.cpp b/clang/lib/Sema/SemaTypeTraits.cpp
index 4dbb2450857e0..1d4b6b7f34775 100644
--- a/clang/lib/Sema/SemaTypeTraits.cpp
+++ b/clang/lib/Sema/SemaTypeTraits.cpp
@@ -1956,6 +1956,7 @@ static std::optional<TypeTrait> StdNameToTypeTrait(StringRef Name) {
             TypeTrait::UTT_IsCppTriviallyRelocatable)
       .Case("is_replaceable", TypeTrait::UTT_IsReplaceable)
       .Case("is_trivially_copyable", TypeTrait::UTT_IsTriviallyCopyable)
+      .Case("is_assignable", TypeTrait::BTT_IsAssignable)
       .Default(std::nullopt);
 }
 
@@ -2285,6 +2286,31 @@ static void DiagnoseNonTriviallyCopyableReason(Sema &SemaRef,
   SemaRef.Diag(D->getLocation(), diag::note_defined_here) << D;
 }
 
+static void DiagnoseNonAssignableReason(Sema &SemaRef, SourceLocation Loc,
+                                        QualType T, QualType U) {
+  const CXXRecordDecl *D = T->getAsCXXRecordDecl();
+
+  if (T->isObjectType() || T->isFunctionType())
+    T = SemaRef.Context.getRValueReferenceType(T);
+  if (U->isObjectType() || U->isFunctionType())
+    U = SemaRef.Context.getRValueReferenceType(U);
+  OpaqueValueExpr LHS(Loc, T.getNonLValueExprType(SemaRef.Context),
+                      Expr::getValueKindForType(T));
+  OpaqueValueExpr RHS(Loc, U.getNonLValueExprType(SemaRef.Context),
+                      Expr::getValueKindForType(U));
+
+  EnterExpressionEvaluationContext Unevaluated(
+      SemaRef, Sema::ExpressionEvaluationContext::Unevaluated);
+  Sema::ContextRAII TUContext(SemaRef,
+                              SemaRef.Context.getTranslationUnitDecl());
+  SemaRef.BuildBinOp(/*S=*/nullptr, Loc, BO_Assign, &LHS, &RHS);
+
+  if (!D || D->isInvalidDecl())
+    return;
+
+  SemaRef.Diag(D->getLocation(), diag::note_defined_here) << D;
+}
+
 void Sema::DiagnoseTypeTraitDetails(const Expr *E) {
   E = E->IgnoreParenImpCasts();
   if (E->containsErrors())
@@ -2305,6 +2331,9 @@ void Sema::DiagnoseTypeTraitDetails(const Expr *E) {
   case UTT_IsTriviallyCopyable:
     DiagnoseNonTriviallyCopyableReason(*this, E->getBeginLoc(), Args[0]);
     break;
+  case BTT_IsAssignable:
+    DiagnoseNonAssignableReason(*this, E->getBeginLoc(), Args[0], Args[1]);
+    break;
   default:
     break;
   }
diff --git a/clang/test/SemaCXX/type-traits-unsatisfied-diags-std.cpp b/clang/test/SemaCXX/type-traits-unsatisfied-diags-std.cpp
index 329b611110c1d..33b92326aec21 100644
--- a/clang/test/SemaCXX/type-traits-unsatisfied-diags-std.cpp
+++ b/clang/test/SemaCXX/type-traits-unsatisfied-diags-std.cpp
@@ -20,6 +20,14 @@ struct is_trivially_copyable {
 
 template <typename T>
 constexpr bool is_trivially_copyable_v = __is_trivially_copyable(T);
+
+template <typename T, typename U>
+struct is_assignable {
+    static constexpr bool value = __is_assignable(T, U);
+};
+
+template <typename T, typename U>
+constexpr bool is_assignable_v = __is_assignable(T, U);
 #endif
 
 #ifdef STD2
@@ -44,6 +52,17 @@ using is_trivially_copyable  = __details_is_trivially_copyable<T>;
 
 template <typename T>
 constexpr bool is_trivially_copyable_v = __is_trivially_copyable(T);
+
+template <typename T, typename U>
+struct __details_is_assignable {
+    static constexpr bool value = __is_assignable(T, U);
+};
+
+template <typename T, typename U>
+using is_assignable = __details_is_assignable<T, U>;
+
+template <typename T, typename U>
+constexpr bool is_assignable_v = __is_assignable(T, U);
 #endif
 
 
@@ -73,6 +92,15 @@ using is_trivially_copyable  = __details_is_trivially_copyable<T>;
 
 template <typename T>
 constexpr bool is_trivially_copyable_v = is_trivially_copyable<T>::value;
+
+template <typename T, typename U>
+struct __details_is_assignable : bool_constant<__is_assignable(T, U)> {};
+
+template <typename T, typename U>
+using is_assignable  = __details_is_assignable<T, U>;
+
+template <typename T, typename U>
+constexpr bool is_assignable_v = is_assignable<T, U>::value;
 #endif
 
 }
@@ -99,6 +127,14 @@ static_assert(std::is_trivially_copyable_v<int&>);
 // expected-note at -1 {{'int &' is not trivially copyable}} \
 // expected-note at -1 {{because it is a reference type}}
 
+static_assert(std::is_assignable<int&, int>::value);
+
+static_assert(std::is_assignable<int&, void>::value);
+// expected-error-re at -1 {{static assertion failed due to requirement 'std::{{.*}}is_assignable<int &, void>::value'}} \
+// expected-error at -1 {{assigning to 'int' from incompatible type 'void'}}
+static_assert(std::is_assignable_v<int&, void>);
+// expected-error at -1 {{static assertion failed due to requirement 'std::is_assignable_v<int &, void>'}} \
+// expected-error at -1 {{assigning to 'int' from incompatible type 'void'}}
 
 namespace test_namespace {
     using namespace std;
@@ -119,6 +155,13 @@ namespace test_namespace {
     // expected-error at -1 {{static assertion failed due to requirement 'is_trivially_copyable_v<int &>'}} \
     // expected-note at -1 {{'int &' is not trivially copyable}} \
     // expected-note at -1 {{because it is a reference type}}
+
+    static_assert(is_assignable<int&, void>::value);
+    // expected-error-re at -1 {{static assertion failed due to requirement '{{.*}}is_assignable<int &, void>::value'}} \
+    // expected-error at -1 {{assigning to 'int' from incompatible type 'void'}}
+    static_assert(is_assignable_v<int&, void>);
+    // expected-error at -1 {{static assertion failed due to requirement 'is_assignable_v<int &, void>'}} \
+    // expected-error at -1 {{assigning to 'int' from incompatible type 'void'}}
 }
 
 
@@ -139,6 +182,14 @@ concept C2 = std::is_trivially_copyable_v<T>; // #concept4
 
 template <C2 T> void g2();  // #cand4
 
+template <typename T, typename U>
+requires std::is_assignable<T, U>::value void f4();  // #cand7
+
+template <typename T, typename U>
+concept C4 = std::is_assignable_v<T, U>; // #concept8
+
+template <C4<void> T> void g4();  // #cand8
+
 void test() {
     f<int&>();
     // expected-error at -1 {{no matching function for call to 'f'}} \
@@ -169,6 +220,19 @@ void test() {
     // expected-note@#concept4 {{because 'std::is_trivially_copyable_v<int &>' evaluated to false}} \
     // expected-note@#concept4 {{'int &' is not trivially copyable}} \
     // expected-note@#concept4 {{because it is a reference type}}
+
+    f4<int&, void>();
+    // expected-error at -1 {{no matching function for call to 'f4'}} \
+    // expected-note@#cand7 {{candidate template ignored: constraints not satisfied [with T = int &, U = void]}} \
+    // expected-note-re@#cand7 {{because '{{.*}}is_assignable<int &, void>::value' evaluated to false}} \
+    // expected-error@#cand7 {{assigning to 'int' from incompatible type 'void'}}
+
+    g4<int&>();
+    // expected-error at -1 {{no matching function for call to 'g4'}} \
+    // expected-note@#cand8 {{candidate template ignored: constraints not satisfied [with T = int &]}} \
+    // expected-note@#cand8 {{because 'C4<int &, void>' evaluated to false}} \
+    // expected-note@#concept8 {{because 'std::is_assignable_v<int &, void>' evaluated to false}} \
+    // expected-error@#concept8 {{assigning to 'int' from incompatible type 'void'}}
 }
 }
 
diff --git a/clang/test/SemaCXX/type-traits-unsatisfied-diags.cpp b/clang/test/SemaCXX/type-traits-unsatisfied-diags.cpp
index 5210354a66d43..eb81c545525a3 100644
--- a/clang/test/SemaCXX/type-traits-unsatisfied-diags.cpp
+++ b/clang/test/SemaCXX/type-traits-unsatisfied-diags.cpp
@@ -488,3 +488,74 @@ static_assert(__is_trivially_copyable(S12));
 // expected-note at -1 {{'S12' is not trivially copyable}} \
 // expected-note@#tc-S12 {{'S12' defined here}}
 }
+
+namespace assignable {
+struct S1;
+static_assert(__is_assignable(S1&, const S1&));
+// expected-error at -1 {{static assertion failed due to requirement '__is_assignable(assignable::S1 &, const assignable::S1 &)'}} \
+// expected-error at -1 {{no viable overloaded '='}} \
+// expected-note at -1 {{type 'S1' is incomplete}}
+
+static_assert(__is_assignable(void, int));
+// expected-error at -1 {{static assertion failed due to requirement '__is_assignable(void, int)'}} \
+// expected-error at -1 {{expression is not assignable}}
+
+static_assert(__is_assignable(int, int));
+// expected-error at -1 {{static assertion failed due to requirement '__is_assignable(int, int)'}} \
+// expected-error at -1 {{expression is not assignable}}
+
+static_assert(__is_assignable(int*, int));
+// expected-error at -1 {{static assertion failed due to requirement '__is_assignable(int *, int)'}} \
+// expected-error at -1 {{expression is not assignable}}
+
+static_assert(__is_assignable(int[], int));
+// expected-error at -1 {{static assertion failed due to requirement '__is_assignable(int[], int)'}} \
+// expected-error at -1 {{expression is not assignable}}
+
+static_assert(__is_assignable(int&, void));
+// expected-error at -1 {{static assertion failed due to requirement '__is_assignable(int &, void)'}} \
+// expected-error at -1 {{assigning to 'int' from incompatible type 'void'}}
+
+static_assert(__is_assignable(int*&, float*));
+// expected-error at -1 {{static assertion failed due to requirement '__is_assignable(int *&, float *)'}} \
+// expected-error at -1 {{incompatible pointer types assigning to 'int *' from 'float *'}}
+
+static_assert(__is_assignable(const int&, int));
+// expected-error at -1 {{static assertion failed due to requirement '__is_assignable(const int &, int)'}} \
+// expected-error at -1 {{read-only variable is not assignable}}
+
+struct S2 {}; // #a-S2
+static_assert(__is_assignable(const S2, S2));
+// expected-error at -1 {{static assertion failed due to requirement '__is_assignable(const assignable::S2, assignable::S2)'}} \
+// expected-error at -1 {{no viable overloaded '='}} \
+// expected-note@#a-S2 {{candidate function (the implicit copy assignment operator) not viable: 'this' argument has type 'const S2', but method is not marked const}} \
+// expected-note@#a-S2 {{candidate function (the implicit move assignment operator) not viable: 'this' argument has type 'const S2', but method is not marked const}} \
+// expected-note@#a-S2 {{'S2' defined here}}
+
+struct S3 { // #a-S3
+    S3& operator=(const S3&) = delete; // #aca-S3
+    S3& operator=(S3&&) = delete;  // #ama-S3
+};
+static_assert(__is_assignable(S3, const S3&));
+// expected-error at -1 {{static assertion failed due to requirement '__is_assignable(assignable::S3, const assignable::S3 &)'}} \
+// expected-error at -1 {{overload resolution selected deleted operator '='}} \
+// expected-note@#aca-S3 {{candidate function has been explicitly deleted}} \
+// expected-note@#ama-S3 {{candidate function not viable: 1st argument ('const S3') would lose const qualifier}} \
+// expected-note@#a-S3 {{'S3' defined here}}
+static_assert(__is_assignable(S3, S3&&));
+// expected-error at -1 {{static assertion failed due to requirement '__is_assignable(assignable::S3, assignable::S3 &&)'}} \
+// expected-error at -1 {{overload resolution selected deleted operator '='}} \
+// expected-note@#aca-S3 {{candidate function has been explicitly deleted}} \
+// expected-note@#ama-S3 {{candidate function has been explicitly deleted}} \
+// expected-note@#a-S3 {{'S3' defined here}}
+
+class C1 { // #a-C1
+  C1& operator=(const C1&) = default;
+  C1& operator=(C1&&) = default; // #ama-C1
+};
+static_assert(__is_assignable(C1, C1));
+// expected-error at -1 {{static assertion failed due to requirement '__is_assignable(assignable::C1, assignable::C1)'}} \
+// expected-error at -1 {{'operator=' is a private member of 'assignable::C1'}} \
+// expected-note@#ama-C1 {{implicitly declared private here}} \
+// expected-note@#a-C1 {{'C1' defined here}}
+}

>From 08ade60d1923d4bc5935a8fdea79827ca424fce6 Mon Sep 17 00:00:00 2001
From: Ross Kirsling <ross.kirsling at sony.com>
Date: Tue, 24 Jun 2025 11:55:32 -0700
Subject: [PATCH 2/2] Clean up OpaqueValueExpr creation.

---
 clang/lib/Sema/SemaTypeTraits.cpp             | 34 ++++++++++---------
 .../SemaCXX/type-traits-unsatisfied-diags.cpp |  4 +--
 2 files changed, 20 insertions(+), 18 deletions(-)

diff --git a/clang/lib/Sema/SemaTypeTraits.cpp b/clang/lib/Sema/SemaTypeTraits.cpp
index 1d4b6b7f34775..28639bfb6b855 100644
--- a/clang/lib/Sema/SemaTypeTraits.cpp
+++ b/clang/lib/Sema/SemaTypeTraits.cpp
@@ -1725,14 +1725,15 @@ static bool EvaluateBinaryTypeTrait(Sema &Self, TypeTrait BTT,
 
     // Build expressions that emulate the effect of declval<T>() and
     // declval<U>().
-    if (LhsT->isObjectType() || LhsT->isFunctionType())
-      LhsT = Self.Context.getRValueReferenceType(LhsT);
-    if (RhsT->isObjectType() || RhsT->isFunctionType())
-      RhsT = Self.Context.getRValueReferenceType(RhsT);
-    OpaqueValueExpr Lhs(KeyLoc, LhsT.getNonLValueExprType(Self.Context),
-                        Expr::getValueKindForType(LhsT));
-    OpaqueValueExpr Rhs(KeyLoc, RhsT.getNonLValueExprType(Self.Context),
-                        Expr::getValueKindForType(RhsT));
+    auto createOpaqueExpr = [&](QualType Ty) -> OpaqueValueExpr {
+      if (Ty->isObjectType() || Ty->isFunctionType())
+        Ty = Self.Context.getRValueReferenceType(Ty);
+      return {KeyLoc, Ty.getNonLValueExprType(Self.Context),
+              Expr::getValueKindForType(Ty)};
+    };
+
+    auto Lhs = createOpaqueExpr(LhsT);
+    auto Rhs = createOpaqueExpr(RhsT);
 
     // Attempt the assignment in an unevaluated context within a SFINAE
     // trap at translation unit scope.
@@ -2290,14 +2291,15 @@ static void DiagnoseNonAssignableReason(Sema &SemaRef, SourceLocation Loc,
                                         QualType T, QualType U) {
   const CXXRecordDecl *D = T->getAsCXXRecordDecl();
 
-  if (T->isObjectType() || T->isFunctionType())
-    T = SemaRef.Context.getRValueReferenceType(T);
-  if (U->isObjectType() || U->isFunctionType())
-    U = SemaRef.Context.getRValueReferenceType(U);
-  OpaqueValueExpr LHS(Loc, T.getNonLValueExprType(SemaRef.Context),
-                      Expr::getValueKindForType(T));
-  OpaqueValueExpr RHS(Loc, U.getNonLValueExprType(SemaRef.Context),
-                      Expr::getValueKindForType(U));
+  auto createOpaqueExpr = [&](QualType Ty) -> OpaqueValueExpr {
+    if (Ty->isObjectType() || Ty->isFunctionType())
+      Ty = SemaRef.Context.getRValueReferenceType(Ty);
+    return {Loc, Ty.getNonLValueExprType(SemaRef.Context),
+            Expr::getValueKindForType(Ty)};
+  };
+
+  auto LHS = createOpaqueExpr(T);
+  auto RHS = createOpaqueExpr(U);
 
   EnterExpressionEvaluationContext Unevaluated(
       SemaRef, Sema::ExpressionEvaluationContext::Unevaluated);
diff --git a/clang/test/SemaCXX/type-traits-unsatisfied-diags.cpp b/clang/test/SemaCXX/type-traits-unsatisfied-diags.cpp
index eb81c545525a3..975174353a2a1 100644
--- a/clang/test/SemaCXX/type-traits-unsatisfied-diags.cpp
+++ b/clang/test/SemaCXX/type-traits-unsatisfied-diags.cpp
@@ -550,8 +550,8 @@ static_assert(__is_assignable(S3, S3&&));
 // expected-note@#a-S3 {{'S3' defined here}}
 
 class C1 { // #a-C1
-  C1& operator=(const C1&) = default;
-  C1& operator=(C1&&) = default; // #ama-C1
+    C1& operator=(const C1&) = default;
+    C1& operator=(C1&&) = default; // #ama-C1
 };
 static_assert(__is_assignable(C1, C1));
 // expected-error at -1 {{static assertion failed due to requirement '__is_assignable(assignable::C1, assignable::C1)'}} \



More information about the cfe-commits mailing list