[c++20] Compute exception specifications for defaulted comparisons.
authorRichard Smith <richard@metafoo.co.uk>
Fri, 13 Dec 2019 22:11:04 +0000 (14:11 -0800)
committerRichard Smith <richard@metafoo.co.uk>
Mon, 16 Dec 2019 06:02:31 +0000 (22:02 -0800)
This requires us to essentially fully form the body of the defaulted
comparison, but from an unevaluated context. Naively this would require
generating the function definition twice; instead, we ensure that the
function body is implicitly defined before performing the check, and
walk the actual body where possible.

clang/include/clang/Sema/Sema.h
clang/lib/Sema/SemaDeclCXX.cpp
clang/lib/Sema/SemaExceptionSpec.cpp
clang/test/CXX/class/class.compare/class.compare.default/p3.cpp
clang/test/CXX/class/class.compare/class.compare.default/p4.cpp
clang/test/CXX/except/except.spec/p11-2a.cpp [new file with mode: 0644]

index 6b561dc..a4a4c2a 100644 (file)
@@ -5232,7 +5232,10 @@ public:
     void CalledDecl(SourceLocation CallLoc, const CXXMethodDecl *Method);
 
     /// Integrate an invoked expression into the collected data.
-    void CalledExpr(Expr *E);
+    void CalledExpr(Expr *E) { CalledStmt(E); }
+
+    /// Integrate an invoked statement into the collected data.
+    void CalledStmt(Stmt *S);
 
     /// Overwrite an EPI's exception specification with this
     /// computed exception specification.
@@ -5294,7 +5297,7 @@ public:
 
   /// Evaluate the implicit exception specification for a defaulted
   /// special member function.
-  void EvaluateImplicitExceptionSpec(SourceLocation Loc, CXXMethodDecl *MD);
+  void EvaluateImplicitExceptionSpec(SourceLocation Loc, FunctionDecl *FD);
 
   /// Check the given noexcept-specifier, convert its expression, and compute
   /// the appropriate ExceptionSpecificationType.
index 239aaf6..5dba1c1 100644 (file)
@@ -217,8 +217,8 @@ Sema::ImplicitExceptionSpecification::CalledDecl(SourceLocation CallLoc,
       Exceptions.push_back(E);
 }
 
-void Sema::ImplicitExceptionSpecification::CalledExpr(Expr *E) {
-  if (!E || ComputedEST == EST_MSAny)
+void Sema::ImplicitExceptionSpecification::CalledStmt(Stmt *S) {
+  if (!S || ComputedEST == EST_MSAny)
     return;
 
   // FIXME:
@@ -242,7 +242,7 @@ void Sema::ImplicitExceptionSpecification::CalledExpr(Expr *E) {
   // implicit definition. For now, we assume that any non-nothrow expression can
   // throw any exception.
 
-  if (Self->canThrow(E))
+  if (Self->canThrow(S))
     ComputedEST = EST_None;
 }
 
@@ -6814,20 +6814,50 @@ static bool defaultedSpecialMemberIsConstexpr(
   return true;
 }
 
+namespace {
+/// RAII object to register a defaulted function as having its exception
+/// specification computed.
+struct ComputingExceptionSpec {
+  Sema &S;
+
+  ComputingExceptionSpec(Sema &S, FunctionDecl *FD, SourceLocation Loc)
+      : S(S) {
+    Sema::CodeSynthesisContext Ctx;
+    Ctx.Kind = Sema::CodeSynthesisContext::ExceptionSpecEvaluation;
+    Ctx.PointOfInstantiation = Loc;
+    Ctx.Entity = FD;
+    S.pushCodeSynthesisContext(Ctx);
+  }
+  ~ComputingExceptionSpec() {
+    S.popCodeSynthesisContext();
+  }
+};
+}
+
 static Sema::ImplicitExceptionSpecification
 ComputeDefaultedSpecialMemberExceptionSpec(
     Sema &S, SourceLocation Loc, CXXMethodDecl *MD, Sema::CXXSpecialMember CSM,
     Sema::InheritedConstructorInfo *ICI);
 
 static Sema::ImplicitExceptionSpecification
-computeImplicitExceptionSpec(Sema &S, SourceLocation Loc, CXXMethodDecl *MD) {
-  auto CSM = S.getSpecialMember(MD);
-  if (CSM != Sema::CXXInvalid)
-    return ComputeDefaultedSpecialMemberExceptionSpec(S, Loc, MD, CSM, nullptr);
+ComputeDefaultedComparisonExceptionSpec(Sema &S, SourceLocation Loc,
+                                        FunctionDecl *FD,
+                                        Sema::DefaultedComparisonKind DCK);
 
-  auto *CD = cast<CXXConstructorDecl>(MD);
+static Sema::ImplicitExceptionSpecification
+computeImplicitExceptionSpec(Sema &S, SourceLocation Loc, FunctionDecl *FD) {
+  auto DFK = S.getDefaultedFunctionKind(FD);
+  if (DFK.isSpecialMember())
+    return ComputeDefaultedSpecialMemberExceptionSpec(
+        S, Loc, cast<CXXMethodDecl>(FD), DFK.asSpecialMember(), nullptr);
+  if (DFK.isComparison())
+    return ComputeDefaultedComparisonExceptionSpec(S, Loc, FD,
+                                                   DFK.asComparison());
+
+  auto *CD = cast<CXXConstructorDecl>(FD);
   assert(CD->getInheritedConstructor() &&
-         "only special members have implicit exception specs");
+         "only defaulted functions and inherited constructors have implicit "
+         "exception specs");
   Sema::InheritedConstructorInfo ICI(
       S, Loc, CD->getInheritedConstructor().getShadowDecl());
   return ComputeDefaultedSpecialMemberExceptionSpec(
@@ -6849,25 +6879,17 @@ static FunctionProtoType::ExtProtoInfo getImplicitMethodEPI(Sema &S,
   return EPI;
 }
 
-void Sema::EvaluateImplicitExceptionSpec(SourceLocation Loc, CXXMethodDecl *MD) {
-  const FunctionProtoType *FPT = MD->getType()->castAs<FunctionProtoType>();
+void Sema::EvaluateImplicitExceptionSpec(SourceLocation Loc, FunctionDecl *FD) {
+  const FunctionProtoType *FPT = FD->getType()->castAs<FunctionProtoType>();
   if (FPT->getExceptionSpecType() != EST_Unevaluated)
     return;
 
   // Evaluate the exception specification.
-  auto IES = computeImplicitExceptionSpec(*this, Loc, MD);
+  auto IES = computeImplicitExceptionSpec(*this, Loc, FD);
   auto ESI = IES.getExceptionSpec();
 
   // Update the type of the special member to use it.
-  UpdateExceptionSpec(MD, ESI);
-
-  // A user-provided destructor can be defined outside the class. When that
-  // happens, be sure to update the exception specification on both
-  // declarations.
-  const FunctionProtoType *CanonicalFPT =
-    MD->getCanonicalDecl()->getType()->castAs<FunctionProtoType>();
-  if (CanonicalFPT->getExceptionSpecType() == EST_Unevaluated)
-    UpdateExceptionSpec(MD->getCanonicalDecl(), ESI);
+  UpdateExceptionSpec(FD, ESI);
 }
 
 void Sema::CheckExplicitlyDefaultedFunction(Scope *S, FunctionDecl *FD) {
@@ -8092,12 +8114,20 @@ bool Sema::CheckExplicitlyDefaultedComparison(Scope *S, FunctionDecl *FD,
   //   declaration, it is implicitly considered to be constexpr.
   // FIXME: Only applying this to the first declaration seems problematic, as
   // simple reorderings can affect the meaning of the program.
-  if (First) {
-    if (!FD->isConstexpr() && Info.Constexpr)
-      FD->setConstexprKind(CSK_constexpr);
-
-    // FIXME: Set up an implicit exception specification, or if given an
-    // explicit one, check that it matches.
+  if (First && !FD->isConstexpr() && Info.Constexpr)
+    FD->setConstexprKind(CSK_constexpr);
+
+  // C++2a [except.spec]p3:
+  //   If a declaration of a function does not have a noexcept-specifier
+  //   [and] is defaulted on its first declaration, [...] the exception
+  //   specification is as specified below
+  if (FD->getExceptionSpecType() == EST_None) {
+    auto *FPT = FD->getType()->castAs<FunctionProtoType>();
+    FunctionProtoType::ExtProtoInfo EPI = FPT->getExtProtoInfo();
+    EPI.ExceptionSpec.Type = EST_Unevaluated;
+    EPI.ExceptionSpec.SourceDecl = FD;
+    FD->setType(Context.getFunctionType(FPT->getReturnType(),
+                                        FPT->getParamTypes(), EPI));
   }
 
   return false;
@@ -8126,17 +8156,11 @@ void Sema::DefineDefaultedComparison(SourceLocation UseLoc, FunctionDecl *FD,
 
   SynthesizedFunctionScope Scope(*this, FD);
 
-  // The exception specification is needed because we are defining the
-  // function.
-  // FIXME: Handle this better. Computing the exception specification will
-  // eventually need the function body.
-  ResolveExceptionSpec(UseLoc, FD->getType()->castAs<FunctionProtoType>());
-
   // Add a context note for diagnostics produced after this point.
   Scope.addContextNote(UseLoc);
 
-  // Build and set up the function body.
   {
+    // Build and set up the function body.
     CXXRecordDecl *RD = cast<CXXRecordDecl>(FD->getLexicalParent());
     SourceLocation BodyLoc =
         FD->getEndLoc().isValid() ? FD->getEndLoc() : FD->getLocation();
@@ -8150,10 +8174,58 @@ void Sema::DefineDefaultedComparison(SourceLocation UseLoc, FunctionDecl *FD,
     FD->markUsed(Context);
   }
 
+  // The exception specification is needed because we are defining the
+  // function. Note that this will reuse the body we just built.
+  ResolveExceptionSpec(UseLoc, FD->getType()->castAs<FunctionProtoType>());
+
   if (ASTMutationListener *L = getASTMutationListener())
     L->CompletedImplicitDefinition(FD);
 }
 
+static Sema::ImplicitExceptionSpecification
+ComputeDefaultedComparisonExceptionSpec(Sema &S, SourceLocation Loc,
+                                        FunctionDecl *FD,
+                                        Sema::DefaultedComparisonKind DCK) {
+  ComputingExceptionSpec CES(S, FD, Loc);
+  Sema::ImplicitExceptionSpecification ExceptSpec(S);
+
+  if (FD->isInvalidDecl())
+    return ExceptSpec;
+
+  // The common case is that we just defined the comparison function. In that
+  // case, just look at whether the body can throw.
+  if (FD->hasBody()) {
+    ExceptSpec.CalledStmt(FD->getBody());
+  } else {
+    // Otherwise, build a body so we can check it. This should ideally only
+    // happen when we're not actually marking the function referenced. (This is
+    // only really important for efficiency: we don't want to build and throw
+    // away bodies for comparison functions more than we strictly need to.)
+
+    // Pretend to synthesize the function body in an unevaluated context.
+    // Note that we can't actually just go ahead and define the function here:
+    // we are not permitted to mark its callees as referenced.
+    Sema::SynthesizedFunctionScope Scope(S, FD);
+    EnterExpressionEvaluationContext Context(
+        S, Sema::ExpressionEvaluationContext::Unevaluated);
+
+    CXXRecordDecl *RD = cast<CXXRecordDecl>(FD->getLexicalParent());
+    SourceLocation BodyLoc =
+        FD->getEndLoc().isValid() ? FD->getEndLoc() : FD->getLocation();
+    StmtResult Body =
+        DefaultedComparisonSynthesizer(S, RD, FD, DCK, BodyLoc).build();
+    if (!Body.isInvalid())
+      ExceptSpec.CalledStmt(Body.get());
+
+    // FIXME: Can we hold onto this body and just transform it to potentially
+    // evaluated when we're asked to define the function rather than rebuilding
+    // it? Either that, or we should only build the bits of the body that we
+    // need (the expressions, not the statements).
+  }
+
+  return ExceptSpec;
+}
+
 void Sema::CheckDelayedMemberExceptionSpecs() {
   decltype(DelayedOverridingExceptionSpecChecks) Overriding;
   decltype(DelayedEquivalentExceptionSpecChecks) Equivalent;
@@ -12336,25 +12408,6 @@ void SpecialMemberExceptionSpecInfo::visitSubobjectCall(
     ExceptSpec.CalledDecl(getSubobjectLoc(Subobj), MD);
 }
 
-namespace {
-/// RAII object to register a special member as being currently declared.
-struct ComputingExceptionSpec {
-  Sema &S;
-
-  ComputingExceptionSpec(Sema &S, CXXMethodDecl *MD, SourceLocation Loc)
-      : S(S) {
-    Sema::CodeSynthesisContext Ctx;
-    Ctx.Kind = Sema::CodeSynthesisContext::ExceptionSpecEvaluation;
-    Ctx.PointOfInstantiation = Loc;
-    Ctx.Entity = MD;
-    S.pushCodeSynthesisContext(Ctx);
-  }
-  ~ComputingExceptionSpec() {
-    S.popCodeSynthesisContext();
-  }
-};
-}
-
 bool Sema::tryResolveExplicitSpecifier(ExplicitSpecifier &ExplicitSpec) {
   llvm::APSInt Result;
   ExprResult Converted = CheckConvertedConstantExpression(
index 38b9ceb..959cdad 100644 (file)
@@ -205,7 +205,7 @@ Sema::ResolveExceptionSpec(SourceLocation Loc, const FunctionProtoType *FPT) {
 
   // Compute or instantiate the exception specification now.
   if (SourceFPT->getExceptionSpecType() == EST_Unevaluated)
-    EvaluateImplicitExceptionSpec(Loc, cast<CXXMethodDecl>(SourceDecl));
+    EvaluateImplicitExceptionSpec(Loc, SourceDecl);
   else
     InstantiateExceptionSpec(Loc, SourceDecl);
 
@@ -1221,6 +1221,17 @@ CanThrowResult Sema::canThrow(const Stmt *S) {
     return mergeCanThrow(CT, canSubStmtsThrow(*this, BTE));
   }
 
+  case Expr::PseudoObjectExprClass: {
+    auto *POE = cast<PseudoObjectExpr>(S);
+    CanThrowResult CT = CT_Cannot;
+    for (const Expr *E : POE->semantics()) {
+      CT = mergeCanThrow(CT, canThrow(E));
+      if (CT == CT_Can)
+        break;
+    }
+    return CT;
+  }
+
     // ObjC message sends are like function calls, but never have exception
     // specs.
   case Expr::ObjCMessageExprClass:
@@ -1327,7 +1338,6 @@ CanThrowResult Sema::canThrow(const Stmt *S) {
   case Expr::ObjCAvailabilityCheckExprClass:
   case Expr::OffsetOfExprClass:
   case Expr::PackExpansionExprClass:
-  case Expr::PseudoObjectExprClass:
   case Expr::SubstNonTypeTemplateParmExprClass:
   case Expr::SubstNonTypeTemplateParmPackExprClass:
   case Expr::FunctionParmPackExprClass:
@@ -1335,7 +1345,7 @@ CanThrowResult Sema::canThrow(const Stmt *S) {
   case Expr::UnresolvedLookupExprClass:
   case Expr::UnresolvedMemberExprClass:
   case Expr::TypoExprClass:
-    // FIXME: Can any of the above throw?  If so, when?
+    // FIXME: Many of the above can throw.
     return CT_Cannot;
 
   case Expr::AddrLabelExprClass:
index f6daaf0..3d0ab2c 100644 (file)
@@ -24,10 +24,10 @@ struct A {
   friend bool operator>=(const A&, const A&) = default;
 };
 struct TestA {
-  friend constexpr bool operator==(const A&, const A&);
-  friend constexpr bool operator!=(const A&, const A&);
+  friend constexpr bool operator==(const A&, const A&) noexcept;
+  friend constexpr bool operator!=(const A&, const A&) noexcept;
 
-  friend constexpr std::strong_ordering operator<=>(const A&, const A&);
+  friend constexpr std::strong_ordering operator<=>(const A&, const A&) noexcept;
   friend constexpr bool operator<(const A&, const A&);
   friend constexpr bool operator<=(const A&, const A&);
   friend constexpr bool operator>(const A&, const A&);
@@ -51,10 +51,10 @@ struct TestReversedA {
   friend constexpr bool operator>(const ReversedA&, const ReversedA&);
   friend constexpr bool operator<=(const ReversedA&, const ReversedA&);
   friend constexpr bool operator<(const ReversedA&, const ReversedA&);
-  friend constexpr std::strong_ordering operator<=>(const ReversedA&, const ReversedA&);
+  friend constexpr std::strong_ordering operator<=>(const ReversedA&, const ReversedA&) noexcept;
 
-  friend constexpr bool operator!=(const ReversedA&, const ReversedA&);
-  friend constexpr bool operator==(const ReversedA&, const ReversedA&);
+  friend constexpr bool operator!=(const ReversedA&, const ReversedA&) noexcept;
+  friend constexpr bool operator==(const ReversedA&, const ReversedA&) noexcept;
 };
 
 struct B {
@@ -69,8 +69,8 @@ struct B {
   friend bool operator>=(const B&, const B&) = default;
 };
 struct TestB {
-  friend constexpr bool operator==(const B&, const B&);
-  friend constexpr bool operator!=(const B&, const B&);
+  friend constexpr bool operator==(const B&, const B&) noexcept;
+  friend constexpr bool operator!=(const B&, const B&) noexcept;
 
   friend constexpr std::strong_ordering operator<=>(const B&, const B&);
   friend constexpr bool operator<(const B&, const B&);
index 1ab7707..3820b5b 100644 (file)
@@ -21,8 +21,8 @@ namespace N {
 
   constexpr bool (*test_a_not_found)(const A&, const A&) = &operator==; // expected-error {{undeclared}}
 
-  constexpr bool operator==(const A&, const A&);
-  constexpr bool (*test_a)(const A&, const A&) = &operator==;
+  constexpr bool operator==(const A&, const A&) noexcept;
+  constexpr bool (*test_a)(const A&, const A&) noexcept = &operator==;
   static_assert((*test_a)(A(), A()));
 }
 
diff --git a/clang/test/CXX/except/except.spec/p11-2a.cpp b/clang/test/CXX/except/except.spec/p11-2a.cpp
new file mode 100644 (file)
index 0000000..5950bce
--- /dev/null
@@ -0,0 +1,226 @@
+// RUN: %clang_cc1 -std=c++2a -verify %s
+// RUN: %clang_cc1 -std=c++2a -verify %s -DDEFINE_FIRST
+
+// As modified by P2002R0:
+//   The exception specification for a comparison operator function (12.6.2)
+//   without a noexcept-specifier that is defaulted on its first declaration is
+//   potentially-throwing if and only if any expression in the implicit
+//   definition is potentially-throwing.
+
+#define CAT2(a, b) a ## b
+#define CAT(a, b) CAT2(a, b)
+
+#ifdef DEFINE_FIRST
+#define DEF(x) auto CAT(a, __LINE__) = x
+#else
+#define DEF(x)
+#endif
+
+namespace std {
+  struct strong_ordering {
+    int n;
+    static const strong_ordering equal, less, greater;
+  };
+  constexpr strong_ordering strong_ordering::equal{0},
+      strong_ordering::less{-1}, strong_ordering::greater{1};
+  bool operator!=(std::strong_ordering o, int n) noexcept;
+}
+
+namespace Eq {
+  struct A {
+    bool operator==(const A&) const = default;
+  };
+  DEF(A() == A());
+  static_assert(noexcept(A() == A()));
+
+  struct B {
+    bool operator==(const B&) const;
+  };
+  struct C {
+    B b;
+    bool operator==(const C&) const = default;
+  };
+  DEF(C() == C());
+  static_assert(!noexcept(C() == C()));
+
+  // Ensure we do not trigger odr-use from exception specification computation.
+  template<typename T> struct D {
+    bool operator==(const D &) const {
+      typename T::error error; // expected-error {{no type}}
+    }
+  };
+  struct E {
+    D<E> d;
+    bool operator==(const E&) const = default;
+  };
+  static_assert(!noexcept(E() == E()));
+
+  // (but we do when defining the function).
+  struct F {
+    D<F> d;
+    bool operator==(const F&) const = default; // expected-note {{in instantiation}}
+  };
+  bool equal = F() == F();
+  static_assert(!noexcept(F() == F()));
+}
+
+namespace Spaceship {
+  struct X {
+    friend std::strong_ordering operator<=>(X, X);
+  };
+  struct Y : X {
+    friend std::strong_ordering operator<=>(Y, Y) = default;
+  };
+  DEF(Y() <=> Y());
+  static_assert(!noexcept(Y() <=> Y()));
+
+  struct ThrowingCmpCat {
+    ThrowingCmpCat(std::strong_ordering);
+    operator std::strong_ordering();
+  };
+  bool operator!=(ThrowingCmpCat o, int n) noexcept;
+
+  struct A {
+    friend ThrowingCmpCat operator<=>(A, A) noexcept;
+  };
+
+  struct B {
+    A a;
+    std::strong_ordering operator<=>(const B&) const = default;
+  };
+  DEF(B() <=> B());
+  static_assert(!noexcept(B() <=> B()));
+
+  struct C {
+    int n;
+    ThrowingCmpCat operator<=>(const C&) const = default;
+  };
+  DEF(C() <=> C());
+  static_assert(!noexcept(C() <=> C()));
+
+  struct D {
+    int n;
+    std::strong_ordering operator<=>(const D&) const = default;
+  };
+  DEF(D() <=> D());
+  static_assert(noexcept(D() <=> D()));
+
+
+  struct ThrowingCmpCat2 {
+    ThrowingCmpCat2(std::strong_ordering) noexcept;
+    operator std::strong_ordering() noexcept;
+  };
+  bool operator!=(ThrowingCmpCat2 o, int n);
+
+  struct E {
+    friend ThrowingCmpCat2 operator<=>(E, E) noexcept;
+  };
+
+  struct F {
+    E e;
+    std::strong_ordering operator<=>(const F&) const = default;
+  };
+  DEF(F() <=> F());
+  static_assert(noexcept(F() <=> F()));
+
+  struct G {
+    int n;
+    ThrowingCmpCat2 operator<=>(const G&) const = default;
+  };
+  DEF(G() <=> G());
+  static_assert(!noexcept(G() <=> G()));
+}
+
+namespace Synth {
+  struct A {
+    friend bool operator==(A, A) noexcept;
+    friend bool operator<(A, A) noexcept;
+  };
+  struct B {
+    A a;
+    friend std::strong_ordering operator<=>(B, B) = default;
+  };
+  std::strong_ordering operator<=>(B, B) noexcept;
+
+  struct C {
+    friend bool operator==(C, C);
+    friend bool operator<(C, C) noexcept;
+  };
+  struct D {
+    C c;
+    friend std::strong_ordering operator<=>(D, D) = default; // expected-note {{previous}}
+  };
+  std::strong_ordering operator<=>(D, D) noexcept; // expected-error {{does not match}}
+
+  struct E {
+    friend bool operator==(E, E) noexcept;
+    friend bool operator<(E, E);
+  };
+  struct F {
+    E e;
+    friend std::strong_ordering operator<=>(F, F) = default; // expected-note {{previous}}
+  };
+  std::strong_ordering operator<=>(F, F) noexcept; // expected-error {{does not match}}
+}
+
+namespace Secondary {
+  struct A {
+    friend bool operator==(A, A);
+    friend bool operator!=(A, A) = default; // expected-note {{previous}}
+
+    friend int operator<=>(A, A);
+    friend bool operator<(A, A) = default; // expected-note {{previous}}
+    friend bool operator<=(A, A) = default; // expected-note {{previous}}
+    friend bool operator>(A, A) = default; // expected-note {{previous}}
+    friend bool operator>=(A, A) = default; // expected-note {{previous}}
+  };
+  bool operator!=(A, A) noexcept; // expected-error {{does not match}}
+  bool operator<(A, A) noexcept; // expected-error {{does not match}}
+  bool operator<=(A, A) noexcept; // expected-error {{does not match}}
+  bool operator>(A, A) noexcept; // expected-error {{does not match}}
+  bool operator>=(A, A) noexcept; // expected-error {{does not match}}
+
+  struct B {
+    friend bool operator==(B, B) noexcept;
+    friend bool operator!=(B, B) = default;
+
+    friend int operator<=>(B, B) noexcept;
+    friend bool operator<(B, B) = default;
+    friend bool operator<=(B, B) = default;
+    friend bool operator>(B, B) = default;
+    friend bool operator>=(B, B) = default;
+  };
+  bool operator!=(B, B) noexcept;
+  bool operator<(B, B) noexcept;
+  bool operator<=(B, B) noexcept;
+  bool operator>(B, B) noexcept;
+  bool operator>=(B, B) noexcept;
+}
+
+// Check that we attempt to define a defaulted comparison before trying to
+// compute its exception specification.
+namespace DefineBeforeComputingExceptionSpec {
+  template<int> struct A {
+    A();
+    A(const A&) = delete; // expected-note 3{{here}}
+    friend bool operator==(A, A); // expected-note 3{{passing}}
+    friend bool operator!=(const A&, const A&) = default; // expected-error 3{{call to deleted constructor}}
+  };
+
+  bool a0 = A<0>() != A<0>(); // expected-note {{in defaulted equality comparison operator}}
+  bool a1 = operator!=(A<1>(), A<1>()); // expected-note {{in defaulted equality comparison operator}}
+
+  template struct A<2>;
+  bool operator!=(const A<2>&, const A<2>&) noexcept; // expected-note {{in evaluation of exception specification}}
+
+  template<int> struct B {
+    B();
+    B(const B&) = delete; // expected-note 3{{here}}
+    friend bool operator==(B, B); // expected-note 3{{passing}}
+    bool operator!=(const B&) const = default; // expected-error 3{{call to deleted constructor}}
+  };
+
+  bool b0 = B<0>() != B<0>(); // expected-note {{in defaulted equality comparison operator}}
+  bool b1 = B<1>().operator!=(B<1>()); // expected-note {{in defaulted equality comparison operator}}
+  int b2 = sizeof(&B<2>::operator!=); // expected-note {{in evaluation of exception specification}}
+}