Here is an example about my question:
struct B {
B(B&&, int = (throw 0, 0)) noexcept {}
};
I know this is a very strange piece of code. It is just used to illustrate the problem. The move constructor of B
has a noexcept
specifier, while it has a default argument which throws an exception.
If I use the noexcept
operator to test the move constructor, it will return false
. But if I provide the second argument, it will then return 'true' (both on GCC and Clang):
noexcept( B(std::declval<B>()) ); // false
noexcept( B(std::declval<B>(), 1) ); // true
Then I added class D
, which inherits from B
and does not provide a move constructor.
struct D : public B { };
And I tested class D
:
noexcept( D(std::declval<D>()) ); // true
I have read the standard and I think that according to the standard, noexcept( D(std::declval<D>()) )
should return false
.
Now I try to analyze the results according to the standard.
According to [expr.unary.noexcept]:
The result of the
noexcept
operator istrue
unless the expression is potentially-throwing ([except.spec]).
So now we need to judge whether the expression B(std::declval<B>())
is potentially-throwing.
According to [except.spec]:
An expression E is potentially-throwing if
- E is a function call whose ..., with a potentially-throwing exception specification, or
- E implicitly invokes a function (such as ...) that has a potentially-throwing exception specification, or
- E is a throw-expression, or
- E is a
dynamic_cast
expression ...- E is a
typeid
expression ...- any of the immediate subexpressions of E is potentially-throwing.
In my example, the expression calls the move constructor of B
which is noexcept
, so it does not belong to the first two cases. Obviously, it does not belong to the next three situations.
The definition of immediate subexpressions is in [intro.execution]:
The immediate subexpressions of an expression E are
- the constituent expressions of E's operands ([expr.prop]),
- any function call that E implicitly invokes,
- if E is lambda-expression, ...
- if E is a function call or implicitly invokes a function, the constituent expressions of each default argument([dcl.fct.default]) used in the call, or
- if E creates an aggregate object ...
According to the standard, the default argument (throw 0, 0)
is the immediate subexpression of B(std::declval<B>())
, but not the immediate subexpression of B(std::declval<B>(), 1)
, and throw 0
is the immediate subexpression of (throw 0, 0)
, which is a potentially-throwing expression. So (throw 0, 0)
and B(std::declval<B>())
are also potentially-throwing expressions. It is true that noexcept( B(std::declval<B>()) )
returns false
and noexcept( B(std::declval<B>(), 1) )
returns true
.
But I am confused about the last example. Why noexcept( D(std::declval<D>()) )
returns true
? D(std::declval<D>())
will implicitly invokes the move constructor of B
, which satisfies the second requirement of immediate subexpression. So it should also satisfy the requirement of potentially-throwing transitively. But the result is just the opposite.
So is my explanation of the reasons for the first two results correct? And what is the reason for the third result?
Edit:
There is a similar example in the standard. In [except.spec]:
struct A {
A(int = (A(5), 0)) noexcept;
A(const A&) noexcept;
A(A&&) noexcept;
~A();
};
struct B {
B() noexcept;
B(const B&) = default; // implicit exception specification is noexcept(true)
B(B&&, int = (throw 42, 0)) noexcept;
~B() noexcept(false);
};
int n = 7;
struct D : public A, public B {
int * p = new int[n];
// D::D() potentially-throwing, as the new operator may throw bad_alloc or bad_array_new_length
// D::D(const D&) non-throwing
// D::D(D&&) potentially-throwing, as the default argument for B's constructor may throw
// D::~D() potentially-throwing
};
All special member functions in A
are noexcept
, while the move constructor of B
is potentially-throwing, and the destructor of B
is noexcept(false)
.
Will D
's move constructor be affected by B
's destructor? Probably not. Because D
's copy constructor is also affected by B
's destructor, but D
's copy constructor is not-throwing.
Besides, according to [except.spec]:
Even though destructors for fully-constructed subobjects are invoked when an exception is thrown during the execution of a constructor ([except.ctor]), their exception specifications do not contribute to the exception specification of the constructor, because an exception thrown from such a destructor would call the function
std::terminate
rather than escape the constructor ([except.throw], [except.terminate]).
So the move constructor of D
is truly affected by the move constructor of B
.
If any functions called between the one that throws an exception and the one that handles the exception are specified as noexcept , noexcept(true) (or throw() in /std:c++17 mode), the program is terminated when the noexcept function propagates the exception.
In contrast, noexcept(false) means that the function may throw an exception. The noexcept specification is part of the function type but can not be used for function overloading. There are two good reasons for the use of noexcept: First, an exception specifier documents the behaviour of the function.
The noexcept operator performs a compile-time check that returns true if an expression is declared to not throw any exceptions. It can be used within a function template's noexcept specifier to declare that the function will throw exceptions for some types but not others.
noexcept is primarily used to allow "you" to detect at compile-time if a function can throw an exception. Remember: most compilers don't emit special code for exceptions unless it actually throws something.
I would argue that the following code comment in the non-normative example of [except.spec]/12 is inaccurate, at best.
D::D(D&&) potentially-throwing, as the default argument for B's constructor may throw
D::D(D&&)
is potentially throwing in the [except.spec]/12 example because its destructor is throwing, not because of the default argument for B
.
If we return to OP's example (no throwing dtor), for D::D(D&&)
to be potentially-throwing, it should fulfill [except.spec]/7:
An implicitly-declared constructor for a class
X
, or a constructor without a noexcept-specifier that is defaulted on its first declaration, has a potentially-throwing exception specification if and only if any of the following constructs is potentially-throwing:
- (7.1) a constructor selected by overload resolution in the implicit definition of the constructor for class
X
to initialize a potentially constructed subobject, or- (7.2) a subexpression of such an initialization, such as a default argument expression, or,
- (7.3) for a default constructor, a default member initializer.
(7.1) does not apply: the subobject is of type B
and the viable constructor is B(B&&, int = (throw 0, 0)) noexcept
which, as a construct (not an expression) is declared to be noexcept, and does thus not have a potentially-throwing exception specification.
(7.3) does not apply.
Thus, (7.2) remains, and applies only if throw 0
is a subexpression of an initialization using the D::D(D&&)
constructor.
A subexpression is as per [intro.execution]/4:
A subexpression of an expression E is an immediate subexpression of E or a subexpression of an immediate subexpression of E.
and, as already listed by OP, an immediate subexpressions is specified by [intro.execution]/3:
The immediate subexpressions of an expression E are
- (3.1) the constituent expressions of E's operands ([expr.prop]),
- (3.2) any function call that E implicitly invokes,
- (3.3) if E is a lambda-expression, the initialization of the entities captured by copy and the constituent expressions of the initializer of the init-captures,
- (3.4) if E is a function call or implicitly invokes a function, the constituent expressions of each default argument ([dcl.fct.default]) used in the call, or
- (3.5) if E creates an aggregate object ([dcl.init.aggr]), the constituent expressions of each default member initializer ([class.mem]) used in the initialization.
I have not been able to find a formal specification of what "implicitly invokes" means, but based on [class.copy.ctor]/14:
The implicitly-defined copy/move constructor for a non-union class X performs a memberwise copy/move of its bases and members. [...]
the implicitly-defined move ctor performs e.g. the move of its bases explicitly (just as a user-provided ctor definition would). Thus, I would argue that invocation of D::D(&&)
does not implicitly invoke B(B&&, int = (throw 0, 0)) noexcept
, thus short-circuiting the subexpression recursion before reaching the throwing default argument of B
's move constructor. Meaning D::D(&&)
does not have a potentially-throwing exception specification.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With