1. Revision history
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 CWG 1778, 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 LWG 2165 that this creates problems for
, which
declares its default constructor thusly:
template < typename T > struct atomic { atomic () noexcept = default ;
... which resulted in
being ill-formed if
has a
potentially-throwing default constructor.
2.2. Potential fixes
LWG 2165 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 bad for std :: atomic < T >
The result of the current wording is that this code is accepted:
struct Foo { Foo () : n ( 0 ) {} // happens to not be noexcept int n ; }; std :: atomic < Foo > f = Foo (); // ok
... but this is ill-formed:
std :: atomic < Foo > f ; // error
This appears extremely hard to justify. As LWG 2334 notes,
Initialization of an atomic object is not an atomic operation.
There appears to be absolutely no reason whatsoever
to require the default constructor of
(or the constructor from a
) to be
.
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.
Fix
by removing the spurious
.
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
Change in [atomics.types.generic]:
template < class T > struct atomic {
[...]
atomic ()
noexcept
= default ;
constexpr atomic ( T )
noexcept
;
Change before [atomics.types.operations]/2:
atomic ()
noexcept
= default ;
Change before [atomics.types.operations]/3:
constexpr atomic ( T )
noexcept
;