17

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.

Pluto
  • 910
  • 3
  • 11
  • To clarify, calling the move constructor of `D` does result in a throw. – eerorika Aug 11 '21 at 13:19
  • "_Why `noexcept( D(std::declval()) )` returns `true`?_" - Because you erroneously claimed that moving `B` can't throw. I suspect _undefined behavior_. Edit: [example](https://godbolt.org/z/c3EPM44vh) (remove `noexcept` and it'll catch the exception just fine instead of crashing) – Ted Lyngmo Aug 11 '21 at 13:25
  • 2
    @TedLyngmo Throwing from `noexcept` is defined to call `std::terminate` (or `std::unexpected` depending on the version of C++), it is not UB. – François Andrieux Aug 11 '21 at 13:35
  • 1
    @FrançoisAndrieux Ah, thanks - yes, I do see the `std::terminate` call in the log I provided myself :-) – Ted Lyngmo Aug 11 '21 at 13:36
  • 3
    I always thought default arguments were evaluated in the context of the caller, not as part of the function. I would expect throwing while evaluating a default argument value would not be affected by the function being `noexcept`. Edit : On second thought, I guess either way the default argument in the context of the `noexcept` operator, so it makes sense that the result would also depend on default arguments. – François Andrieux Aug 11 '21 at 13:36
  • @FrançoisAndrieux I call the move constructor of `D` in a `try` block with a `catch - all` clause, but it still called `std::terminate`. If I explicitly declare the move-ctor of `D` as `noexcept(false)`, the exception will be caught by the `catch -all` clause. So I think you are correct. The exception thrown from the default argument will be caught by the caller. Because the move-ctor of `D` is `noexcept`(and this is what i don't understand), the exception will be "caught" by `noexcept` in `D` and `std::terminate` will be invoked. – Pluto Aug 11 '21 at 13:55
  • @Pluto `D::D(D&&)` will be `noexcept(false)` by default if you don't make an erroneous claim in `B::B(B&&, int = (throw 0, 0))` - which should be `noexcept(false)` - and it will be by default, if you don't explicitly set it to `noexcept(true)` – Ted Lyngmo Aug 11 '21 at 14:01
  • 2
    If I understand [except.spec#7](https://eel.is/c++draft/except.spec#7) correctly, `D(D&&)` should be `noexcept(false)`... – Jarod42 Aug 11 '21 at 14:09
  • @FrançoisAndrieux I also hope this is a compiler bug, because I can't find an error in my analysis of the standard. But `B(B&&, int = (throw 42, 0)) noexcept` is actually an example in the standard. I just simplified it. – Pluto Aug 11 '21 at 14:10
  • @Pluto I think Jaro42d's link simplifies the analysis. It seems to me like *"the invocation of a constructor selected by overload resolution in the implicit definition of the constructor for class X to initialize a potentially constructed subobject"* is potentially throwing so `D(D&&)` should not be `noexcept` even if `B(B&&)` is `noexcept`. I think the compiler is taking a shortcut and only looking at if the subobject constructor itself is qualified as `noexcept`, not if invoking that constructor is `noexcept`. – François Andrieux Aug 11 '21 at 14:16
  • @Jarod42 Yes, this also proves that `D(D&&)` should be `noexcept(false)`. Is this really a problem with the compiler? But why do GCC, Clang and MSVC all have the same result? Moreover, this example is simplified from the example in the standard. – Pluto Aug 11 '21 at 14:29
  • "_if and only if any of the following constructs is potentially-throwing: ... the invocation of a constructor selected by overload resolution in the implicit definition of the constructor for class X to initialize a potentially constructed subobject_" - but, the subobject move constructor is not potentially throwing if declared as `B(B&&, int = (throw 0, 0)) noexcept` - the fact that it may throw anyway is not up to `D::D(D&&)` to analyze – Ted Lyngmo Aug 11 '21 at 14:31
  • @TedLyngmo: Even example from [except.spec#12](https://eel.is/c++draft/except.spec#12) states: "`// D​::​D(D&&) potentially-throwing, as the default argument for B's constructor may throw`". – Jarod42 Aug 11 '21 at 14:34
  • @Jarod42 Indeed - I stand corrected. Utterly confused, but still :-) – Ted Lyngmo Aug 11 '21 at 14:35
  • @TedLyngmo I don't think `B(B&&)` is not potentially-throwing. *Potentially-throwing expression* and *potentially-throwing exception specification* are different. `B(B&&)` is marked with `noexcept`, it simply does not satisfy the latter. But it is still a *potentially-throwing expression*, because `noexcept(B(std::declval()))` returns `false`. – Pluto Aug 11 '21 at 14:36
  • @Pluto Yes, `B::B(B&&, int = ...)` is potentially throwing but when marked with `noexcept` it'll call `std::terminate` instead of throwing an exception. My belief (up until a few minutes ago) was that `D::D(D&&)` would make itself `noexcept` by default because of the `noexcept` in `B`. – Ted Lyngmo Aug 11 '21 at 14:42
  • @TedLyngmo I am so similar to you. Before I saw the strange example in the standard, I always thought that the `noexcept` of the move constructor of `B` would naturally lead to the `noexcept` of the move constructor of `D`. Although the result of the compiler conforms to this idea, the standard does Deny it. – Pluto Aug 11 '21 at 14:47
  • 1
    @TedLyngmo I don't think that throwing while evaluating the default argument value for a `noexcept` function means calling `std::terminate`. My understanding is that the default value is evaluated outside of the context of the function, it basically replaces the argument at the call site. If you have `void foo(int) noexcept` and `int bar()` then if you try `foo(bar())` and `bar` throws, the `foo` function did not throw so there is no need for `std::terminate`. It seems to me like this is the same situation. – François Andrieux Aug 11 '21 at 14:47
  • @FrançoisAndrieux If that's the case, I'm even more confused :-) The `try / catch` in my example at the top catches the exception if we declare the move constructor `noexcept(false)` but calls `std::terminate` if we make the move constructor `noexcept(true)`. – Ted Lyngmo Aug 11 '21 at 14:51
  • 1
    @TedLyngmo That's because your example uses `D` which seems to have a `noexcept` move constructor (which is the basis of this question) and in that case the default argument is evaluated in a `noexcept` context (the context is `D::D(D&&) noexcept`). If you use `B` instead you will get `"throw"` as an output even when `B(B&&)` is `noexcept` because the default argument is not evaluated in a `noexcept` context (the context is `int main()` which is not `noexcept`). – François Andrieux Aug 11 '21 at 15:15
  • @FrançoisAndrieux (facepalm) Doh... Thanks again. I think I need a break :-D – Ted Lyngmo Aug 11 '21 at 15:17
  • «implicitly invokes» means only subexpressions of _E_, not bodies of functions which may be invoked during _E_ evaluation – Language Lawyer Aug 11 '21 at 21:07
  • @LanguageLawyer So is the call of `D`'s move constructor to `B`'s move constructor an "implicitly invoke"? – Pluto Aug 12 '21 at 01:33
  • Call to a base move constructor happens inside the body of a derived class – Language Lawyer Aug 12 '21 at 13:47
  • 1
    @LanguageLawyer But how to understand the examples in the standard I quoted? The standard stipulates that `D::D(D&&)` should be potentially-throwing. And I also discussed that `D::D(D&&)` is not affected by the destructor of `B` and `D`. – Pluto Aug 13 '21 at 02:16

1 Answers1

2

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.

dfrib
  • 70,367
  • 12
  • 127
  • 192
  • I'd say "such an initialization" refers to an initialization described by 7.1; that's the part that mentions initialization; `D(D&&)` is referred to as "implicitly-declared constructor for a class X", not in terms of an initialization. Which means that 7.2 is also talking about the initialization of potentially constructed subobjects, so the question is whether `throw 0` is a subexpression of the invocation of `B`'s constructor using the default argument, and the answer to that is, as far as I can tell, yes. – bogdan Aug 11 '21 at 23:47
  • "`D​::​D(D&&)` is potentially throwing in the [except.spec]/12 example because its destructor is throwing": "its destructor", as in, `D::~D()`? As far as I understand, a potentially-throwing destructor doesn't make a constructor of the class potentially-throwing (an expression that creates a temporary of that type could be potentially-throwing, but that's different). If "its destructor" refers to `B::~B()`, then see Note 2 on paragraph 7 - that doesn't make `D​::​D(D&&)` potentially-throwing either. Also, note that `D::D(const D&)` is non-throwing, although the same destructors are involved. – bogdan Aug 12 '21 at 00:04
  • Yes, I am also confused about the concept of "implicitly invoke". But I also think Bodgan's comment is reasonable. For the example in the standard, if the move constructor of `D` is affected by the destructor, why is the copy constructor of `D` not affected? Does this mean that `D`'s move constructor is indeed affected by `B`'s move constructor? But the standard expects `D`'s move constructor to be potentially-throwing, which is different from the result of the compiler. – Pluto Aug 12 '21 at 01:49