Skip to content

[Clang] Diagnose unsatisfied std::is_assignable. #144836

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 3 commits into from
Jun 25, 2025

Conversation

rkirsling
Copy link
Contributor

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.

@llvmbot llvmbot added clang Clang issues not falling into any other category clang:frontend Language frontend issues, e.g. anything involving "Sema" labels Jun 19, 2025
@llvmbot
Copy link
Member

llvmbot commented Jun 19, 2025

@llvm/pr-subscribers-clang

Author: Ross Kirsling (rkirsling)

Changes

Part of the work for #141911.

Checking is_assignable&lt;T, U&gt; boils down to checking the well-formedness of declval&lt;T&gt;() = declval&lt;U&gt;(); this PR recycles logic from EvaluateBinaryTypeTrait in order to produce the relevant diagnostics.


Full diff: https://github.com/llvm/llvm-project/pull/144836.diff

3 Files Affected:

  • (modified) clang/lib/Sema/SemaTypeTraits.cpp (+29)
  • (modified) clang/test/SemaCXX/type-traits-unsatisfied-diags-std.cpp (+65)
  • (modified) clang/test/SemaCXX/type-traits-unsatisfied-diags.cpp (+71)
diff --git a/clang/lib/Sema/SemaTypeTraits.cpp b/clang/lib/Sema/SemaTypeTraits.cpp
index 22c690bedc1ed..aa3208fe51271 100644
--- a/clang/lib/Sema/SemaTypeTraits.cpp
+++ b/clang/lib/Sema/SemaTypeTraits.cpp
@@ -1949,6 +1949,7 @@ static std::optional<TypeTrait> StdNameToTypeTrait(StringRef Name) {
       .Case("is_replaceable", TypeTrait::UTT_IsReplaceable)
       .Case("is_trivially_copyable", TypeTrait::UTT_IsTriviallyCopyable)
       .Case("is_constructible", TypeTrait::TT_IsConstructible)
+      .Case("is_assignable", TypeTrait::BTT_IsAssignable)
       .Default(std::nullopt);
 }
 
@@ -2340,6 +2341,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())
@@ -2363,6 +2389,9 @@ void Sema::DiagnoseTypeTraitDetails(const Expr *E) {
   case TT_IsConstructible:
     DiagnoseNonConstructibleReason(*this, E->getBeginLoc(), Args);
     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 a403a0450607a..23391a799282f 100644
--- a/clang/test/SemaCXX/type-traits-unsatisfied-diags-std.cpp
+++ b/clang/test/SemaCXX/type-traits-unsatisfied-diags-std.cpp
@@ -28,6 +28,14 @@ struct is_constructible {
 
 template <typename... Args>
 constexpr bool is_constructible_v = __is_constructible(Args...);
+
+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
@@ -63,6 +71,17 @@ using is_constructible  = __details_is_constructible<Args...>;
 
 template <typename... Args>
 constexpr bool is_constructible_v = __is_constructible(Args...);
+
+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
 
 
@@ -101,6 +120,15 @@ using is_constructible  = __details_is_constructible<Args...>;
 
 template <typename... Args>
 constexpr bool is_constructible_v = is_constructible<Args...>::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
 
 }
@@ -137,6 +165,15 @@ static_assert(std::is_constructible_v<void>);
 // expected-error@-1 {{static assertion failed due to requirement 'std::is_constructible_v<void>'}} \
 // expected-note@-1 {{because it is a cv void type}}
 
+static_assert(std::is_assignable<int&, int>::value);
+
+static_assert(std::is_assignable<int&, void>::value);
+// expected-error-re@-1 {{static assertion failed due to requirement 'std::{{.*}}is_assignable<int &, void>::value'}} \
+// expected-error@-1 {{assigning to 'int' from incompatible type 'void'}}
+static_assert(std::is_assignable_v<int&, void>);
+// expected-error@-1 {{static assertion failed due to requirement 'std::is_assignable_v<int &, void>'}} \
+// expected-error@-1 {{assigning to 'int' from incompatible type 'void'}}
+
 namespace test_namespace {
     using namespace std;
     static_assert(is_trivially_relocatable<int&>::value);
@@ -163,6 +200,13 @@ namespace test_namespace {
     static_assert(is_constructible_v<void>);
     // expected-error@-1 {{static assertion failed due to requirement 'is_constructible_v<void>'}} \
     // expected-note@-1 {{because it is a cv void type}}
+
+    static_assert(is_assignable<int&, void>::value);
+    // expected-error-re@-1 {{static assertion failed due to requirement '{{.*}}is_assignable<int &, void>::value'}} \
+    // expected-error@-1 {{assigning to 'int' from incompatible type 'void'}}
+    static_assert(is_assignable_v<int&, void>);
+    // expected-error@-1 {{static assertion failed due to requirement 'is_assignable_v<int &, void>'}} \
+    // expected-error@-1 {{assigning to 'int' from incompatible type 'void'}}
 }
 
 
@@ -191,6 +235,14 @@ concept C3 = std::is_constructible_v<Args...>; // #concept6
 
 template <C3 T> void g3();  // #cand6
 
+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&>();
@@ -235,6 +287,19 @@ void test() {
     // expected-note@#cand6 {{because 'void' does not satisfy 'C3'}} \
     // expected-note@#concept6 {{because 'std::is_constructible_v<void>' evaluated to false}} \
     // expected-note@#concept6 {{because it is a cv void type}}
+
+    f4<int&, void>();
+    // expected-error@-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@-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 d0b3f294fbcab..adca5938c3759 100644
--- a/clang/test/SemaCXX/type-traits-unsatisfied-diags.cpp
+++ b/clang/test/SemaCXX/type-traits-unsatisfied-diags.cpp
@@ -550,3 +550,74 @@ static_assert(__is_constructible(void (int, float)));
 // expected-error@-1 {{static assertion failed due to requirement '__is_constructible(void (int, float))'}} \
 // expected-note@-1 {{because it is a function type}}
 }
+
+namespace assignable {
+struct S1;
+static_assert(__is_assignable(S1&, const S1&));
+// expected-error@-1 {{static assertion failed due to requirement '__is_assignable(assignable::S1 &, const assignable::S1 &)'}} \
+// expected-error@-1 {{no viable overloaded '='}} \
+// expected-note@-1 {{type 'S1' is incomplete}}
+
+static_assert(__is_assignable(void, int));
+// expected-error@-1 {{static assertion failed due to requirement '__is_assignable(void, int)'}} \
+// expected-error@-1 {{expression is not assignable}}
+
+static_assert(__is_assignable(int, int));
+// expected-error@-1 {{static assertion failed due to requirement '__is_assignable(int, int)'}} \
+// expected-error@-1 {{expression is not assignable}}
+
+static_assert(__is_assignable(int*, int));
+// expected-error@-1 {{static assertion failed due to requirement '__is_assignable(int *, int)'}} \
+// expected-error@-1 {{expression is not assignable}}
+
+static_assert(__is_assignable(int[], int));
+// expected-error@-1 {{static assertion failed due to requirement '__is_assignable(int[], int)'}} \
+// expected-error@-1 {{expression is not assignable}}
+
+static_assert(__is_assignable(int&, void));
+// expected-error@-1 {{static assertion failed due to requirement '__is_assignable(int &, void)'}} \
+// expected-error@-1 {{assigning to 'int' from incompatible type 'void'}}
+
+static_assert(__is_assignable(int*&, float*));
+// expected-error@-1 {{static assertion failed due to requirement '__is_assignable(int *&, float *)'}} \
+// expected-error@-1 {{incompatible pointer types assigning to 'int *' from 'float *'}}
+
+static_assert(__is_assignable(const int&, int));
+// expected-error@-1 {{static assertion failed due to requirement '__is_assignable(const int &, int)'}} \
+// expected-error@-1 {{read-only variable is not assignable}}
+
+struct S2 {}; // #a-S2
+static_assert(__is_assignable(const S2, S2));
+// expected-error@-1 {{static assertion failed due to requirement '__is_assignable(const assignable::S2, assignable::S2)'}} \
+// expected-error@-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@-1 {{static assertion failed due to requirement '__is_assignable(assignable::S3, const assignable::S3 &)'}} \
+// expected-error@-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@-1 {{static assertion failed due to requirement '__is_assignable(assignable::S3, assignable::S3 &&)'}} \
+// expected-error@-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@-1 {{static assertion failed due to requirement '__is_assignable(assignable::C1, assignable::C1)'}} \
+// expected-error@-1 {{'operator=' is a private member of 'assignable::C1'}} \
+// expected-note@#ama-C1 {{implicitly declared private here}} \
+// expected-note@#a-C1 {{'C1' defined here}}
+}

Part of the work for llvm#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.
@rkirsling rkirsling force-pushed the diagnose-unsatisfied-is_assignable branch from ac74e32 to 5807503 Compare June 19, 2025 05:08
@rkirsling
Copy link
Contributor Author

Seems like I don't have the ability to add reviewers yet; pinging @cor3ntin and @AaronBallman for visibility.


static_assert(std::is_assignable<int&, void>::value);
// expected-error-re@-1 {{static assertion failed due to requirement 'std::{{.*}}is_assignable<int &, void>::value'}} \
// expected-error@-1 {{assigning to 'int' from incompatible type 'void'}}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Unlike the other DiagnoseTypeTraitDetails diagnostics, this is an "error", not a "note". I'm not sure that actually causes any serious issues right now, but it's a bit confusing, and could cause issues in the future.

Copy link
Contributor Author

@rkirsling rkirsling Jun 19, 2025

Choose a reason for hiding this comment

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

Right, at first I wondered if I needed to recapitulate all of these existing diagnostic messages somehow, just to ensure that it would all be notes against a single error. But it appears that is_constructible is also incurring a second error (via InitializationSequence::Diagnose), so I took this as precedent.

Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Member

Choose a reason for hiding this comment

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

This is a good candidate for nesting; I’ll make a note of this one somewhere. For now I think it’s fine to just emit the error (I’m very busy this week unfortunately, but I’m planning to open a pr for it next week if nothing gets in the way).

Copy link
Collaborator

@erichkeane erichkeane left a comment

Choose a reason for hiding this comment

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

1 very minor nit on a rename, else LGTM. Please let others make sure they are happy before merging.

@rkirsling
Copy link
Contributor Author

Thanks! Could I also have one of you merge this for me once everybody's happy? 🙏

@erichkeane
Copy link
Collaborator

Thanks! Could I also have one of you merge this for me once everybody's happy? 🙏

Ah, a little late in my day to be doing that (the 'click merge and take off' strat is one fraught with error :) ), but if no one else has by my morning, I'll do so.

Copy link
Contributor

@cor3ntin cor3ntin left a comment

Choose a reason for hiding this comment

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

LGTM, thanks

@cor3ntin cor3ntin merged commit 3ef796b into llvm:main Jun 25, 2025
7 checks passed
@rkirsling rkirsling deleted the diagnose-unsatisfied-is_assignable branch June 25, 2025 18:22
anthonyhatran pushed a commit to anthonyhatran/llvm-project that referenced this pull request Jun 26, 2025
Part of the work for llvm#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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
clang:frontend Language frontend issues, e.g. anything involving "Sema" clang Clang issues not falling into any other category
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants