From 4a4e90a823882399fe22374629f79f1edd123d01 Mon Sep 17 00:00:00 2001 From: Richard Smith Date: Fri, 13 Dec 2019 14:11:04 -0800 Subject: [PATCH] [c++20] Compute exception specifications for defaulted comparisons. 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 | 7 +- clang/lib/Sema/SemaDeclCXX.cpp | 159 ++++++++++----- clang/lib/Sema/SemaExceptionSpec.cpp | 16 +- .../class.compare/class.compare.default/p3.cpp | 16 +- .../class.compare/class.compare.default/p4.cpp | 4 +- clang/test/CXX/except/except.spec/p11-2a.cpp | 226 +++++++++++++++++++++ 6 files changed, 360 insertions(+), 68 deletions(-) create mode 100644 clang/test/CXX/except/except.spec/p11-2a.cpp diff --git a/clang/include/clang/Sema/Sema.h b/clang/include/clang/Sema/Sema.h index 6b561dc..a4a4c2a 100644 --- a/clang/include/clang/Sema/Sema.h +++ b/clang/include/clang/Sema/Sema.h @@ -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. diff --git a/clang/lib/Sema/SemaDeclCXX.cpp b/clang/lib/Sema/SemaDeclCXX.cpp index 239aaf6..5dba1c1 100644 --- a/clang/lib/Sema/SemaDeclCXX.cpp +++ b/clang/lib/Sema/SemaDeclCXX.cpp @@ -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(MD); +static Sema::ImplicitExceptionSpecification +computeImplicitExceptionSpec(Sema &S, SourceLocation Loc, FunctionDecl *FD) { + auto DFK = S.getDefaultedFunctionKind(FD); + if (DFK.isSpecialMember()) + return ComputeDefaultedSpecialMemberExceptionSpec( + S, Loc, cast(FD), DFK.asSpecialMember(), nullptr); + if (DFK.isComparison()) + return ComputeDefaultedComparisonExceptionSpec(S, Loc, FD, + DFK.asComparison()); + + auto *CD = cast(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(); +void Sema::EvaluateImplicitExceptionSpec(SourceLocation Loc, FunctionDecl *FD) { + const FunctionProtoType *FPT = FD->getType()->castAs(); 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(); - 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::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()); - // 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(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()); + 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(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( diff --git a/clang/lib/Sema/SemaExceptionSpec.cpp b/clang/lib/Sema/SemaExceptionSpec.cpp index 38b9ceb..959cdad 100644 --- a/clang/lib/Sema/SemaExceptionSpec.cpp +++ b/clang/lib/Sema/SemaExceptionSpec.cpp @@ -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(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(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: diff --git a/clang/test/CXX/class/class.compare/class.compare.default/p3.cpp b/clang/test/CXX/class/class.compare/class.compare.default/p3.cpp index f6daaf0..3d0ab2c 100644 --- a/clang/test/CXX/class/class.compare/class.compare.default/p3.cpp +++ b/clang/test/CXX/class/class.compare/class.compare.default/p3.cpp @@ -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&); diff --git a/clang/test/CXX/class/class.compare/class.compare.default/p4.cpp b/clang/test/CXX/class/class.compare/class.compare.default/p4.cpp index 1ab7707..3820b5b 100644 --- a/clang/test/CXX/class/class.compare/class.compare.default/p4.cpp +++ b/clang/test/CXX/class/class.compare/class.compare.default/p4.cpp @@ -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 index 0000000..5950bce --- /dev/null +++ b/clang/test/CXX/except/except.spec/p11-2a.cpp @@ -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 struct D { + bool operator==(const D &) const { + typename T::error error; // expected-error {{no type}} + } + }; + struct E { + D d; + bool operator==(const E&) const = default; + }; + static_assert(!noexcept(E() == E())); + + // (but we do when defining the function). + struct F { + D 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 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 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}} +} -- 2.7.4