Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Strange behavior when a function declared as noexcept throws an exception in its default argument

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 is true 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.

like image 327
Pluto Avatar asked Aug 11 '21 13:08

Pluto


People also ask

What happens if Noexcept function throws exception?

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.

What does Noexcept false do?

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.

What does Noexcept mean?

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.

What is the point of Noexcept?

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.


1 Answers

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.

like image 114
dfrib Avatar answered Oct 10 '22 03:10

dfrib