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
.