This patch implements C++20 P1286R2, Contra CWG DR1778 as described here <http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p1286r2.html>. Essentially, it removes the restriction that the exception specification of a defaulted special member matches the implicit exception specification. "Implementing" it means just removing some code (woo!) that was marking a function DECL_DELETED_FN if the exception-specifications didn't match...
...except there's a snag. We no longer reject struct A { A() noexcept(false) = default; }; because A::A() is no longer deleted. But this assert fails: static_assert(!noexcept(A()), ""); and it doesn't seem to me it should, but it's sort of Catch 22. The default constructor *is* trivial as per [class.default.ctor]/3, so trivial_fn_p is true. When build_over_call sees a default_ctor_p that is trivial_fn_p, it elides the call and creates a TARGET_EXPR <D, void_cst>. When evaluating noexcept(A()) expr_noexcept_p gets TARGET_EXPR <D, {}> and since there is no CALL_EXPR/AGGR_INIT_EXPR that throws, it says "ok, this can't throw". [except.spec]/6: An expression e is potentially-throwing if (6.1) e is a function call [...] with a potentially-throwing exception specification, or (6.2) e implicitly invokes a function [...] that is potentially-throwing, or ... (6.6) any of the immediate subexpressions of e is potentially-throwing. which of these apply here, 6.1? But such a TARGET_EXPR can't throw, can it? I commented out the failing asserts in noexcept55.C. I'd love to hear your thoughts on this. Bootstrapped/regtested on x86_64-linux. 2019-09-05 Marek Polacek <pola...@redhat.com> PR c++/90537 - Implement P1286R2, Contra CWG DR1778. * cp-tree.h (after_nsdmi_defaulted_late_checks): Remove. * method.c (maybe_explain_implicit_delete): Don't compare noexcept-specifiers. (defaulted_late_check): Likewise. (after_nsdmi_defaulted_late_checks): Remove function. * parser.c (unparsed_classes): Remove macro. (push_unparsed_function_queues): Adjust initializer. (cp_parser_class_specifier_1): Remove unparsed_classes. Don't call after_nsdmi_defaulted_late_checks. * parser.h (cp_unparsed_functions_entry): Remove the member holding nested classes. * g++.dg/cpp0x/defaulted23.C: Remove dg-error. * g++.dg/cpp0x/defaulted43.C: Likewise. * g++.dg/cpp0x/noexcept55.C: New test. * g++.dg/cpp0x/noexcept56.C: New test. * g++.dg/cpp0x/noexcept57.C: New test. * g++.dg/cpp0x/noexcept58.C: New test. * g++.dg/cpp0x/noexcept59.C: New test. * g++.dg/cpp0x/noexcept60.C: New test. diff --git gcc/cp/cp-tree.h gcc/cp/cp-tree.h index 038f58d10f8..165daac4cdc 100644 --- gcc/cp/cp-tree.h +++ gcc/cp/cp-tree.h @@ -6698,7 +6698,6 @@ extern tree forward_parm (tree); extern bool is_trivially_xible (enum tree_code, tree, tree); extern bool is_xible (enum tree_code, tree, tree); extern tree get_defaulted_eh_spec (tree, tsubst_flags_t = tf_warning_or_error); -extern void after_nsdmi_defaulted_late_checks (tree); extern bool maybe_explain_implicit_delete (tree); extern void explain_implicit_non_constexpr (tree); extern void deduce_inheriting_ctor (tree); diff --git gcc/cp/method.c gcc/cp/method.c index 53fa85b9790..1cfbb1cec13 100644 --- gcc/cp/method.c +++ gcc/cp/method.c @@ -1857,13 +1857,6 @@ maybe_explain_implicit_delete (tree decl) NULL, NULL, &deleted_p, NULL, true, &inh, parms); } - else if (!comp_except_specs - (TYPE_RAISES_EXCEPTIONS (TREE_TYPE (decl)), - raises, ce_normal)) - inform (DECL_SOURCE_LOCATION (decl), "%q#F is implicitly " - "deleted because its exception-specification does not " - "match the implicit exception-specification %qX", - decl, raises); else if (flag_checking) gcc_unreachable (); @@ -2198,12 +2191,14 @@ defaulted_late_check (tree fn) return; } - /* 8.4.2/2: An explicitly-defaulted function (...) may have an explicit - exception-specification only if it is compatible (15.4) with the - exception-specification on the implicit declaration. If a function - is explicitly defaulted on its first declaration, (...) it is - implicitly considered to have the same exception-specification as if - it had been implicitly declared. */ + /* [dcl.fct.def.default] An explicitly-defaulted function may have differing + exception-specifications from the exception-specification on the implicit + declaration. + + [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 and no other declaration + or that function shall have a noexcept-specifier. */ maybe_instantiate_noexcept (fn); tree fn_spec = TYPE_RAISES_EXCEPTIONS (TREE_TYPE (fn)); if (!fn_spec) @@ -2211,27 +2206,6 @@ defaulted_late_check (tree fn) if (DECL_DEFAULTED_IN_CLASS_P (fn)) TREE_TYPE (fn) = build_exception_variant (TREE_TYPE (fn), eh_spec); } - else if (UNEVALUATED_NOEXCEPT_SPEC_P (fn_spec)) - /* Equivalent to the implicit spec. */; - else if (DECL_DEFAULTED_IN_CLASS_P (fn) - && !CLASSTYPE_TEMPLATE_INSTANTIATION (ctx)) - /* We can't compare an explicit exception-specification on a - constructor defaulted in the class body to the implicit - exception-specification until after we've parsed any NSDMI; see - after_nsdmi_defaulted_late_checks. */; - else - { - tree eh_spec = get_defaulted_eh_spec (fn); - if (!comp_except_specs (fn_spec, eh_spec, ce_normal)) - { - if (DECL_DEFAULTED_IN_CLASS_P (fn)) - DECL_DELETED_FN (fn) = true; - else - error ("function %q+D defaulted on its redeclaration " - "with an exception-specification that differs from " - "the implicit exception-specification %qX", fn, eh_spec); - } - } if (DECL_DEFAULTED_IN_CLASS_P (fn) && DECL_DECLARED_CONSTEXPR_P (implicit_fn)) @@ -2258,35 +2232,6 @@ defaulted_late_check (tree fn) } } -/* OK, we've parsed the NSDMI for class T, now we can check any explicit - exception-specifications on functions defaulted in the class body. */ - -void -after_nsdmi_defaulted_late_checks (tree t) -{ - if (uses_template_parms (t)) - return; - if (t == error_mark_node) - return; - for (tree fn = TYPE_FIELDS (t); fn; fn = DECL_CHAIN (fn)) - if (!DECL_ARTIFICIAL (fn) - && DECL_DECLARES_FUNCTION_P (fn) - && DECL_DEFAULTED_IN_CLASS_P (fn)) - { - tree fn_spec = TYPE_RAISES_EXCEPTIONS (TREE_TYPE (fn)); - if (UNEVALUATED_NOEXCEPT_SPEC_P (fn_spec)) - continue; - - tree eh_spec = get_defaulted_eh_spec (fn); - if (eh_spec == error_mark_node) - continue; - - if (!comp_except_specs (TYPE_RAISES_EXCEPTIONS (TREE_TYPE (fn)), - eh_spec, ce_normal)) - DECL_DELETED_FN (fn) = true; - } -} - /* Returns true iff FN can be explicitly defaulted, and gives any errors if defaulting FN is ill-formed. */ diff --git gcc/cp/parser.c gcc/cp/parser.c index baa60b8834e..66b38e30b6a 100644 --- gcc/cp/parser.c +++ gcc/cp/parser.c @@ -2003,16 +2003,13 @@ cp_parser_context_new (cp_parser_context* next) parser->unparsed_queues->last ().funs_with_definitions #define unparsed_nsdmis \ parser->unparsed_queues->last ().nsdmis -#define unparsed_classes \ - parser->unparsed_queues->last ().classes #define unparsed_noexcepts \ parser->unparsed_queues->last ().noexcepts static void push_unparsed_function_queues (cp_parser *parser) { - cp_unparsed_functions_entry e = { NULL, make_tree_vector (), NULL, NULL, - NULL }; + cp_unparsed_functions_entry e = { NULL, make_tree_vector (), NULL, NULL }; vec_safe_push (parser->unparsed_queues, e); } @@ -23648,7 +23645,6 @@ cp_parser_class_specifier_1 (cp_parser* parser) error recovery (c++/71169, c++/71832). */ vec_safe_truncate (unparsed_funs_with_default_args, 0); vec_safe_truncate (unparsed_nsdmis, 0); - vec_safe_truncate (unparsed_classes, 0); vec_safe_truncate (unparsed_funs_with_definitions, 0); } @@ -23703,12 +23699,6 @@ cp_parser_class_specifier_1 (cp_parser* parser) if (pushed_scope) pop_scope (pushed_scope); - /* Now do some post-NSDMI bookkeeping. */ - FOR_EACH_VEC_SAFE_ELT (unparsed_classes, ix, class_type) - after_nsdmi_defaulted_late_checks (class_type); - vec_safe_truncate (unparsed_classes, 0); - after_nsdmi_defaulted_late_checks (type); - /* If there are noexcept-specifiers that have not yet been processed, take care of them now. */ class_type = NULL_TREE; @@ -23779,8 +23769,6 @@ cp_parser_class_specifier_1 (cp_parser* parser) cp_parser_late_parsing_for_member (parser, decl); vec_safe_truncate (unparsed_funs_with_definitions, 0); } - else - vec_safe_push (unparsed_classes, type); /* Put back any saved access checks. */ pop_deferring_access_checks (); diff --git gcc/cp/parser.h gcc/cp/parser.h index 2890788f489..089056d7af3 100644 --- gcc/cp/parser.h +++ gcc/cp/parser.h @@ -163,10 +163,6 @@ struct GTY(()) cp_unparsed_functions_entry { FIELD_DECLs appear in this list in declaration order. */ vec<tree, va_gc> *nsdmis; - /* Nested classes go in this vector, so that we can do some final - processing after parsing any NSDMIs. */ - vec<tree, va_gc> *classes; - /* Functions with noexcept-specifiers that require post-processing. */ vec<tree, va_gc> *noexcepts; }; diff --git gcc/testsuite/g++.dg/cpp0x/defaulted23.C gcc/testsuite/g++.dg/cpp0x/defaulted23.C index dfbdd2f2ed1..8609b21a8fe 100644 --- gcc/testsuite/g++.dg/cpp0x/defaulted23.C +++ gcc/testsuite/g++.dg/cpp0x/defaulted23.C @@ -10,10 +10,10 @@ A a; struct B { - B() throw (int) = default; // { dg-message "exception-specification" "" { target { ! c++17 } } } -}; // { dg-error "dynamic exception specification" "" { target c++17 } .-1 } - // { dg-warning "deprecated" "" { target { ! c++17 } } .-2 } -B b; // { dg-error "deleted" "" { target { ! c++17 } } } + B() throw (int) = default; // { dg-error "dynamic exception specification" "" { target c++17 } } +}; // { dg-warning "deprecated" "" { target { ! c++17 } } .-1 } + +B b; struct C { diff --git gcc/testsuite/g++.dg/cpp0x/defaulted43.C gcc/testsuite/g++.dg/cpp0x/defaulted43.C index f2846fe390c..0be3e2d0c9d 100644 --- gcc/testsuite/g++.dg/cpp0x/defaulted43.C +++ gcc/testsuite/g++.dg/cpp0x/defaulted43.C @@ -17,8 +17,8 @@ struct A T t; }; -A::A() noexcept = default; // { dg-error "defaulted" } -A::~A() noexcept = default; // { dg-error "defaulted" } +A::A() noexcept = default; +A::~A() noexcept = default; struct U { @@ -51,10 +51,10 @@ V v; struct C { - C() noexcept = default; // { dg-message "exception-specification" } - ~C() noexcept = default; // { dg-message "exception-specification" } + C() noexcept = default; + ~C() noexcept = default; V v; }; -C c; // { dg-error "deleted" } +C c; diff --git gcc/testsuite/g++.dg/cpp0x/noexcept55.C gcc/testsuite/g++.dg/cpp0x/noexcept55.C new file mode 100644 index 00000000000..1c36d39bffb --- /dev/null +++ gcc/testsuite/g++.dg/cpp0x/noexcept55.C @@ -0,0 +1,64 @@ +// PR c++/90537 - Implement P1286R2, Contra CWG DR1778. +// { dg-do compile { target c++11 } } + +// An explicitly-defaulted function may have a different exception-specification +// from the exception-specification on an implicit declaration. This didn't +// compile pre-P1286R2. +struct A { + A() noexcept(false) = default; + A(const A&) noexcept(false) = default; + A(A&&) noexcept(false) = default; + A &operator=(const A&) noexcept(false) = default; + A &operator=(A&&) noexcept(false) = default; + ~A() noexcept(false) = default; +}; + +A a; +// FIXME The default ctor is trivial_fn_p, so build_over_call elides the call +// and creates TARGET_EXPR <D, {}>, and expr_noexcept_p thinks it's noexcept. +#if 0 +static_assert(!noexcept(A()), ""); +static_assert(!noexcept(A(A())), ""); +static_assert(!noexcept(A(a)), ""); +static_assert(!noexcept(A(static_cast<A&&>(a))), ""); +static_assert(!noexcept(a = a), ""); +static_assert(!noexcept(a = A()), ""); +static_assert(!noexcept(a = static_cast<A&&>(a)), ""); +#endif + +// This compiled even pre-P1286R2. Make sure it continues to do so. +struct B { + B() noexcept(true) = default; + B(const B&) noexcept(true) = default; + B(B&&) noexcept(true) = default; + B &operator=(const B&) noexcept(true) = default; + B &operator=(B&&) noexcept(true) = default; + ~B() noexcept(true) = default; +}; + +B b; +static_assert(noexcept(B()), ""); +static_assert(noexcept(B(B())), ""); +static_assert(noexcept(B(b)), ""); +static_assert(noexcept(B(static_cast<B&&>(b))), ""); +static_assert(noexcept(b = b), ""); +static_assert(noexcept(b = B()), ""); +static_assert(noexcept(b = static_cast<B&&>(b)), ""); + +struct C { + C() = default; + C(const C&) = default; + C(C&&) = default; + C &operator=(const C&) = default; + C &operator=(C&&) = default; + ~C() = default; +}; + +C c; +static_assert(noexcept(C()), ""); +static_assert(noexcept(C(C())), ""); +static_assert(noexcept(C(c)), ""); +static_assert(noexcept(C(static_cast<C&&>(c))), ""); +static_assert(noexcept(c = c), ""); +static_assert(noexcept(c = C()), ""); +static_assert(noexcept(c = static_cast<C&&>(c)), ""); diff --git gcc/testsuite/g++.dg/cpp0x/noexcept56.C gcc/testsuite/g++.dg/cpp0x/noexcept56.C new file mode 100644 index 00000000000..14fbfbdee51 --- /dev/null +++ gcc/testsuite/g++.dg/cpp0x/noexcept56.C @@ -0,0 +1,16 @@ +// PR c++/90537 - Implement P1286R2, Contra CWG DR1778. +// { dg-do compile { target c++11 } } + +struct X { X(); }; + +struct A { + struct B { + B() noexcept(A::value) = default; + X x; + }; + decltype(B()) b; + static constexpr bool value = true; +}; +A::B b; + +static_assert(noexcept(A::B()), ""); diff --git gcc/testsuite/g++.dg/cpp0x/noexcept57.C gcc/testsuite/g++.dg/cpp0x/noexcept57.C new file mode 100644 index 00000000000..b03f05fb4dc --- /dev/null +++ gcc/testsuite/g++.dg/cpp0x/noexcept57.C @@ -0,0 +1,62 @@ +// PR c++/90537 - Implement P1286R2, Contra CWG DR1778. +// { dg-do compile { target c++11 } } + +// A base that throws. +struct B { + B() noexcept(false); + B(const B&) noexcept(false); + B(B&&) noexcept(false); + B &operator=(const B&) noexcept(false); + B &operator=(B&&) noexcept(false); + ~B() noexcept(false); +}; + +struct D1 : B { + D1() noexcept(false) = default; + D1(const D1&) noexcept(false) = default; + D1(D1&&) noexcept(false) = default; + D1 &operator=(const D1&) noexcept(false) = default; + D1 &operator=(D1&&) noexcept(false) = default; + ~D1() noexcept(false) = default; +}; + +D1 d1; +static_assert(!noexcept(D1()), ""); +static_assert(!noexcept(D1(static_cast<D1&&>(d1))), ""); +static_assert(!noexcept(D1(d1)), ""); +static_assert(!noexcept(d1 = static_cast<D1&&>(d1)), ""); +static_assert(!noexcept(d1 = d1), ""); + +struct D2 : B { + D2() = default; + D2(const D2&) = default; + D2(D2&&) = default; + D2 &operator=(const D2&) = default; + D2 &operator=(D2&&) = default; + ~D2() = default; +}; + +D2 d2; +static_assert(!noexcept(D2()), ""); +static_assert(!noexcept(D2(static_cast<D2&&>(d2))), ""); +static_assert(!noexcept(D2(d2)), ""); +static_assert(!noexcept(d2 = static_cast<D2&&>(d2)), ""); +static_assert(!noexcept(d2 = d2), ""); + +// This didn't compile pre-P1286R2 -- the implicit exception-specification +// is noexcept(false). +struct D3 : B { + D3() noexcept(true) = default; + D3(const D3&) noexcept(true) = default; + D3(D3&&) noexcept(true) = default; + D3 &operator=(const D3&) noexcept(true) = default; + D3 &operator=(D3&&) noexcept(true) = default; + ~D3() noexcept(true) = default; +}; + +D3 d3; +static_assert(noexcept(D3()), ""); +static_assert(noexcept(D3(static_cast<D3&&>(d3))), ""); +static_assert(noexcept(D3(d3)), ""); +static_assert(noexcept(d3 = static_cast<D3&&>(d3)), ""); +static_assert(noexcept(d3 = d3), ""); diff --git gcc/testsuite/g++.dg/cpp0x/noexcept58.C gcc/testsuite/g++.dg/cpp0x/noexcept58.C new file mode 100644 index 00000000000..1e6c4ea5f02 --- /dev/null +++ gcc/testsuite/g++.dg/cpp0x/noexcept58.C @@ -0,0 +1,8 @@ +// PR c++/90537 - Implement P1286R2, Contra CWG DR1778. +// { dg-do compile { target c++11 } } +// [dcl.fct.def.default]/4 + +struct T { T(); T(T &&) noexcept(false); }; +struct U { T t; U(); U(U &&) noexcept = default; }; +U u1; +U u2 = static_cast<U&&>(u1); // OK, calls std::terminate if T::T(T&&) throws diff --git gcc/testsuite/g++.dg/cpp0x/noexcept59.C gcc/testsuite/g++.dg/cpp0x/noexcept59.C new file mode 100644 index 00000000000..df4f30ff2d6 --- /dev/null +++ gcc/testsuite/g++.dg/cpp0x/noexcept59.C @@ -0,0 +1,87 @@ +// PR c++/90537 - Implement P1286R2, Contra CWG DR1778. +// { dg-do compile { target c++11 } } + +// An explicitly-defaulted function may have a different exception-specification +// from the exception-specification on an implicit declaration. This didn't +// compile pre-P1286R2. +struct A { + A() noexcept(false); + A(const A&) noexcept(false); + A(A&&) noexcept(false); + A &operator=(const A&) noexcept(false); + A &operator=(A&&) noexcept(false); + ~A() noexcept(false); +}; + +A::A() noexcept(false) = default; +A::A(const A&) noexcept(false) = default; +A::A(A&&) noexcept(false) = default; +A& A::operator=(const A&) noexcept(false) = default; +A& A::operator=(A&&) noexcept(false) = default; +A::~A() noexcept(false) = default; + +A a; +static_assert(!noexcept(A()), ""); +static_assert(!noexcept(A(A())), ""); +static_assert(!noexcept(A(a)), ""); +static_assert(!noexcept(A(static_cast<A&&>(a))), ""); +static_assert(!noexcept(a = a), ""); +static_assert(!noexcept(a = A()), ""); +static_assert(!noexcept(a = static_cast<A&&>(a)), ""); + +// This compiled even pre-P1286R2. Make sure it continues to do so. +struct B { + B() noexcept(true); + B(const B&) noexcept(true); + B(B&&) noexcept(true); + B &operator=(const B&) noexcept(true); + B &operator=(B&&) noexcept(true); + ~B() noexcept(true); +}; + +B::B() noexcept(true) = default; +B::B(const B&) noexcept(true) = default; +B::B(B&&) noexcept(true) = default; +B& B::operator=(const B&) noexcept(true) = default; +B& B::operator=(B&&) noexcept(true) = default; +B::~B() noexcept(true) = default; + +B b; +static_assert(noexcept(B()), ""); +static_assert(noexcept(B(B())), ""); +static_assert(noexcept(B(b)), ""); +static_assert(noexcept(B(static_cast<B&&>(b))), ""); +static_assert(noexcept(b = b), ""); +static_assert(noexcept(b = B()), ""); +static_assert(noexcept(b = static_cast<B&&>(b)), ""); + +struct C { + C(); + C(const C&); + C(C&&); + C &operator=(const C&); + C &operator=(C&&); + ~C(); +}; + +// [except.spec] If a declaration of a function does not have +// a noexcept-specifier, the declaration has a potentially throwing +// exception specification unless it is a destructor or a deallocation +// function or is defaulted on its first declaration, in which cases the +// exception specification is as specified below +// Here it's not defaulted on its first declaration. +C::C() = default; +C::C(const C&) = default; +C::C(C&&) = default; +C& C::operator=(const C&) = default; +C& C::operator=(C&&) = default; +C::~C() = default; + +C c; +static_assert(!noexcept(C()), ""); +static_assert(!noexcept(C(C())), ""); +static_assert(!noexcept(C(c)), ""); +static_assert(!noexcept(C(static_cast<C&&>(c))), ""); +static_assert(!noexcept(c = c), ""); +static_assert(!noexcept(c = C()), ""); +static_assert(!noexcept(c = static_cast<C&&>(c)), ""); diff --git gcc/testsuite/g++.dg/cpp0x/noexcept60.C gcc/testsuite/g++.dg/cpp0x/noexcept60.C new file mode 100644 index 00000000000..eeab1ed6c1c --- /dev/null +++ gcc/testsuite/g++.dg/cpp0x/noexcept60.C @@ -0,0 +1,81 @@ +// PR c++/90537 - Implement P1286R2, Contra CWG DR1778. +// { dg-do compile { target c++11 } } + +// A base that throws. +struct B { + B() noexcept(false); + B(const B&) noexcept(false); + B(B&&) noexcept(false); + B &operator=(const B&) noexcept(false); + B &operator=(B&&) noexcept(false); + ~B() noexcept(false); +}; + +struct D1 : B { + D1() noexcept(false); + D1(const D1&) noexcept(false); + D1(D1&&) noexcept(false); + D1 &operator=(const D1&) noexcept(false); + D1 &operator=(D1&&) noexcept(false); + ~D1() noexcept(false); +}; + +D1::D1() noexcept(false) = default; +D1::D1(const D1&) noexcept(false) = default; +D1::D1(D1&&) noexcept(false) = default; +D1& D1::operator=(const D1&) noexcept(false) = default; +D1& D1::operator=(D1&&) noexcept(false) = default; +D1::~D1() noexcept(false) = default; + +D1 d1; +static_assert(!noexcept(D1()), ""); +static_assert(!noexcept(D1(static_cast<D1&&>(d1))), ""); +static_assert(!noexcept(D1(d1)), ""); +static_assert(!noexcept(d1 = static_cast<D1&&>(d1)), ""); +static_assert(!noexcept(d1 = d1), ""); + +struct D2 : B { + D2(); + D2(const D2&); + D2(D2&&); + D2 &operator=(const D2&); + D2 &operator=(D2&&); + ~D2(); +}; + +D2::D2() = default; +D2::D2(const D2&) = default; +D2::D2(D2&&) = default; +D2& D2::operator=(const D2&) = default; +D2& D2::operator=(D2&&) = default; +D2::~D2() = default; + +D2 d2; +static_assert(!noexcept(D2()), ""); +static_assert(!noexcept(D2(static_cast<D2&&>(d2))), ""); +static_assert(!noexcept(D2(d2)), ""); +static_assert(!noexcept(d2 = static_cast<D2&&>(d2)), ""); +static_assert(!noexcept(d2 = d2), ""); + +struct D3 : B { + D3() noexcept(true); + D3(const D3&) noexcept(true); + D3(D3&&) noexcept(true); + D3 &operator=(const D3&) noexcept(true); + D3 &operator=(D3&&) noexcept(true); + ~D3() noexcept(true); +}; + +D3::D3() noexcept(true) = default; +D3::D3(const D3&) noexcept(true) = default; +D3::D3(D3&&) noexcept(true) = default; +D3& D3::operator=(const D3&) noexcept(true) = default; +D3& D3::operator=(D3&&) noexcept(true) = default; +D3::~D3() noexcept(true) = default; + +D3 d3; +static_assert(noexcept(D3()), ""); +static_assert(noexcept(D3(static_cast<D3&&>(d3))), ""); +static_assert(noexcept(D3(d3)), ""); +static_assert(noexcept(d3 = static_cast<D3&&>(d3)), ""); +static_assert(noexcept(d3 = d3), "");