1. Revision history
Since [P1286R1]:
-
Addressed CWG wording review comments.
-
Addressed LWG wording review comments by removing library wording; the fixes will be left for [LWG2334].
Since [P1286R0]:
-
Remove discussion of options; EWG has selected their desired alternative.
-
Approved unanimously by EWG.
2. Background
See lists.isocpp.org/core/2018/01/3741.php for more information.
2.1. CWG 1778: exception-specification in explicitly-defaulted functions
Prior to [CWG1778], we required that:
An explicitly-defaulted function [...] may have an explicit exception-specification only if it is compatible with the exception-specification on the implicit declaration.
It was observed in [LWG2165] that this creates problems for
, which
declares its default constructor thusly:
template < typename T > struct atomic { atomic () noexcept = default ;
... which (it was believed) resulted in
being ill-formed if
has
a potentially-throwing default constructor.
2.2. Potential fixes
[LWG2165] lists the following as potential fixes:
-
Add nothrow default constructible to requirements for template argument of the generic
atomic < T > -
Remove
from the overload set ifatomic < T >:: atomic ()
is not nothrow default constructible.T -
Remove
fromnoexcept
, allowing it to be deduced (but the default constructor is intended to be alwaysatomic < T >:: atomic ()
)noexcept -
Do not default
on its first declaration (but makes the default constructor user-provided and so preventsatomic < T >:: atomic ()
being trivial)atomic < T > -
A core change to allow the mismatched exception specification if the default constructor isn’t used (see c++std-core-21990)
2.3. Language change
CWG chose to resolve the issue by changing the rule to:
If a function that is explicitly defaulted has an explicit exception-specification that is not compatible with the exception-specification on the implicit declaration, then
if the function is explicitly defaulted on its first declaration, it is defined as deleted;
otherwise, the program is ill-formed.
That is: implicitly delete the default constructor if the specified exception specification doesn’t match the implicit one.
3. Problem
3.1. Existing approach is bad for compilers
Exception specifications are a complete-class context: they are a place where all members of the class and its enclosing classes can be used, just like member function bodies, default arguments, and default member initializers. This means we cannot in general determine the implicit exception specification of a member function until we reach the end of the outermost lexically-enclosing class. However, we need to know which special member functions a class has, and whether or not they are deleted, immediately after the class becomes complete, which (for a nested class) may be earlier.
Example:
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 ;
Here, we do not parse the exception specification for
until after we have finished parsing class
.
But the class
becomes complete at its close brace,
and at that point we must know the critical facts
regarding its definition,
including which of its special members are deleted.
Note that we cannot possibly tell whether the call to
within the
is valid, because we don’t
know whether
is deleted yet.
3.2. Existing approach is unnecessary for std :: atomic < T >
The existing core rule arose because of concern over a case such as
struct Foo { Foo () : n ( 0 ) {} // happens to not be noexcept int n ; }; std :: atomic < Foo > f ;
... being ill-formed. But it is not: the intent of
the
default constructor is to leave the
atomic storage uninitialized, so the
constructor is not invoked, so the implicit exception
specification of
is always inferred as
. The explicit exception specification
therefore has no effect.
If the default constructor of
did default-initialize a
, we
would need changes here. This problem is to be resolved as part of [LWG2334].
3.3. Existing approach prevents a useful feature
Consider the following pattern, which we found several instances of in our codebase when we tightened up the compiler to reject a mismatched exception specification on a defaulted function:
struct X { std :: map < ... > m ; // ... other members public : // I want a defaulted move constructor, and vector<X> needs to be // efficient, so please call std::terminate if moving the map throws // rather than slowing my code down with unnecessary copies X ( X && ) noexcept = default ; };
Users wanting this feature are forced to write out their own
special members, which is an error-prone operation that
was supposed to alleviate.
4. Approach
If the user explicitly specifies an exception specification on a defaulted function, that’s the exception specification. Don’t delete the function, don’t reject the program, just accept it.
5. Wording
Change in [dcl.fct.def.default]/2:
The type
of an explicitly defaulted function
T1 is allowed to differ from the type
F it would have had if it were implicitly declared, as follows:
T2
and
T1 may have differing ref-qualifiers; and
T2 and
T1 may have differing exception specifications; and
T2 if
has a parameter of type
T2 , the corresponding parameter of
const C & may be of type
T1 .
C & [...]
Change in [dcl.fct.def.default]/4:
//
~ S () noexcept ( false) = default ; deleted: exception specification does not matchOK, despite mismatched exception specification
Add the following to the example in [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
Do not make any changes to [atomics.types].