Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[lib] Extend exception propagation to cover the initialization LWG3640 #4863

Open
JohelEGP opened this issue Sep 4, 2021 · 18 comments · May be fixed by #4869
Open

[lib] Extend exception propagation to cover the initialization LWG3640 #4863

JohelEGP opened this issue Sep 4, 2021 · 18 comments · May be fixed by #4869
Assignees
Labels
big An issue causing a large set of changes, scattered across most of the text. lwg Issue must be reviewed by LWG. not-editorial Issue is not deemed editorial; the editorial issue is kept open for tracking.

Comments

@JohelEGP
Copy link
Contributor

JohelEGP commented Sep 4, 2021

Problem and solution

The wording for the specification of facilities that propagate exceptions inadvertently exclude some cases of thrown exceptions. The ones I've identified have the form "any exception thrown by the {(selected) constructor,construction} of {T,some object}". This formulation excludes exceptions thrown by the initialization of the constructor's parameters ([expr.call] p7). These are not thrown by the constructor nor during the construction of the object (after its construction has begun ([class.cdtor] p1)).

Implying that the initialization of the constructor's parameter are part of the effects for which exceptions are propagated can be done with these changes:

  • "Any exception thrown by calling the (selected) constructor of T.
  • "Any exception thrown by the constructioninitialization of {T,some object}".

The former is already present in the standard, as exemplified below. As for the latter (emphasis mine),

The process of initialization described in [dcl.init] applies to all initializations regardless of syntactic context, including the initialization of a function parameter ([expr.call]), the initialization of a return value ([stmt.return]), or when an initializer follows a declarator.
-- https://eel.is/c++draft/dcl.init p1

Examples

Example, good

Constructors and member functions of pair do not throw exceptions unless one of the element-wise operations specified to be called for that operation throws an exception. -- https://eel.is/c++draft/pairs.pair#1

Talking in terms of operations includes initialization.

Example, bad

For each tuple constructor, an exception is thrown only if the construction of one of the types in Types throws an exception. -- https://eel.is/c++draft/tuple.cnstr#2

We can make a tuple constructor throw an exception other than by constructing one of the specified types: https://godbolt.org/z/enGThMq4P.

#include <tuple>
struct X { X(int) { } };
struct Y { operator int() { throw 0; } };
int main() { std::tuple<X>(Y{}); }

As described at the top, this throws before beginning the construction of an X subobject.

Do note that the constructor above is described to initialize:

Effects: Initializes the elements in the tuple with the corresponding value in std​::​forward(u). -- https://eel.is/c++draft/tuple.cnstr#13

One could argue that [tuple.cnstr] doesn't say "if and only if". That just leaves in the air what happens in this case.

Example, good

Throws: Any exception thrown by the initialization of the selected alternative Tj. -- https://eel.is/c++draft/variant.ctor#18
Throws: Any exception thrown by calling the selected constructor of T. -- https://eel.is/c++draft/variant.ctor#23

@JohelEGP JohelEGP changed the title [lib] Extend exception propagation to cover the whole call [lib] Extend exception propagation to cover the initialization Sep 4, 2021
@jensmaurer
Copy link
Member

jensmaurer commented Sep 4, 2021

I agree with the problem statement.

Regarding the solutions offered, phrasing this in terms of initialization seems preferable, because it also covers the case where there is no constructor (e.g. for built-in types).

Is there a full list of offending wording somewhere?

@jensmaurer jensmaurer added the big An issue causing a large set of changes, scattered across most of the text. label Sep 4, 2021
@JohelEGP
Copy link
Contributor Author

JohelEGP commented Sep 4, 2021

I, too, prefer initialization, if at least for uniformity. But I believe the change is big enough, so inserting "calling" could be a first step. Unless the initialization of fundamental types are allowed to throw exceptions, this would not be wrong. But it would require saying "if any" to account for when there's no actual constructor involved (which can also apply to class types).

There's no full list. I found this problem while searching for "exception" in the library. But for the case of exception propagation, the actual suspects (so far) are uses of "constructor" and "construction". More generally, it's the reference to particular steps of initialization, when the intention is to refer to the initialization itself.

Feel free to assign this to me.

@JohelEGP

This comment has been minimized.

@JohelEGP
Copy link
Contributor Author

JohelEGP commented Sep 4, 2021

Unless any one of you

Me included.

On further analysis of sibling vocabulary types, all of them use type traits instead of named requirements for these operations. So I will proceed on the assumption there's no such defect.

@JohelEGP
Copy link
Contributor Author

JohelEGP commented Sep 5, 2021

Even "initialization" doesn't encompass enough. An exception thrown by the destruction of a temporary created as part of the initialization of a constructor's parameter is not part of the initialization (https://eel.is/c++draft/expr.call#7.sentence-9, https://eel.is/c++draft/full#class.temporary-4.sentence-5).
See https://godbolt.org/z/3MYod6za7:

#include <tuple>
struct Z { ~Z() noexcept(false) { throw 0; } };
struct X { X(const Z&) noexcept { } };
struct Y { operator Z() { return {}; } };
int main() try { std::tuple<X>(Y{}); } catch(...) { throw 0ull; }
// Terminate on thrown int means throw within noexcept

Exceptions thrown by destructors of returned prvalues seem to have the same problem: https://godbolt.org/z/xeaaEToMq.

For each \tcode{tuple} assignment operator, an exception is thrown only if the
assignment of one of the types in \tcode{Types} throws an exception. -- https://eel.is/c++draft/tuple#assign-1.sentence-1

#include <tuple>
struct Y { ~Y() noexcept(false) { throw 0; } };
struct X { Y operator=(const X&) noexcept { return {}; } };
int main()
try {
  std::tuple<X> x{};
  x = x;
  throw '0';
}
catch(int) {
  throw 0ull; // Terminate on thrown int means throw within noexcept
}

I propose the following amendment to the proposed solution:

Implying that the side effects of the initialization of a constructor's parameter are part of the effects for which exceptions are propagated can be done with these changes:

  • "Any exception thrown by propagated from a side effect of calling the (selected) constructor of T.
  • "Any exception thrown by the construction propagated from a side effect of the initialization of {T,some object}".

The use of "propagated" was taken from https://eel.is/c++draft/futures.async#3. This use of side effect is novel.

@jensmaurer
Copy link
Member

@jwakely , there seems to be material for at least two library issues here.

The first one is for optional.assign p7 and vicinity:

"This does not require T to meet the Cpp17CopyAssignable requirements. So the assignment as specified can return something other than T&. Particularly a prvalue whose destruction throws. Only T's destructor is required to not throw."

A type traits check (e.g. is_copy_assignable) does not replace a semantic precondition, e.g. Cpp17CopyAssignable.

The second one is bad phrasing around "thrown by the construction", because it ignores exceptions thrown by the destruction of temporaries and by the destruction of prvalue results. LWG should form an opinion what the appropriate phrasing should be (possibly in conjunction with a front-matter explanation), and apply that uniformly (or have it editorially applied uniformly).

And no, "side effects" is a defined term in [intro.execution]; we can't use that here. "full-expression" might help, though.

@JohelEGP
Copy link
Contributor Author

JohelEGP commented Sep 5, 2021

I did notice that std::tuple<T> didn't even require T to meet the Cpp17Destructible requirements, as opposed to std::optional. Now, a quick test shows that implementations do not swallow exceptions in the destructor, suggesting a defect as it fails to meet its own requirements. It doesn't define a destructor, so this is fine. std::any, on the other hand, does define a destructor yet doesn't impose the named requirement. Here's a proof of implementation divergence: https://godbolt.org/z/Yrszd3d4f.

@JohelEGP
Copy link
Contributor Author

JohelEGP commented Sep 5, 2021

And no, "side effects" is a defined term in [intro.execution]; we can't use that here. "full-expression" might help, though.

I propose amending the proposed solution to the following:

Let E be the construction of {T,some object}. Implying that the full expression E is a subexpression of propagates exceptions as a result of evaluating E can be done with this change:

  • "Any exception propagated from the full expression that evaluates E".

@JohelEGP
Copy link
Contributor Author

JohelEGP commented Sep 5, 2021

That could even be defined as exception-propagating.

+3.21 [defns.exception-propagating]
+exception-propagating
+an effect whose exceptions thrown due to the evaluation of its implementing expression are propagated

-Constructors and member functions of pair do not throw exceptions unless one of the element-wise
-operations specified to be called for that operation throws an exception.
+For constructors and member functions of pair, each element-wise
+operation specified to be called for that operation is exception-propagating.

-For each \tcode{tuple} constructor, an exception is thrown only if the construction of
-one of the types in \tcode{Types} throws an exception.
+For each \tcode{tuple} constructor, the construction of each type in \tcode{Types} is exception-propagating.

 \item
 \throws
-any exceptions thrown by the function, and the conditions that would cause the exception.
+any exceptions thrown by the function and the conditions that would cause the exception, and
+effects specified as exception-propagating.

-Throws: Any exception thrown by the initialization of the selected alternative Tj.
+Throws: The initialization of the selected alternative Tj is exception-propagating.

-Throws: Any exception thrown by the selected constructor of T.
+Throws: Calling the selected constructor of T is exception-propagating.

@JohelEGP
Copy link
Contributor Author

JohelEGP commented Sep 5, 2021

@jwakely , there seems to be material for at least two library issues here.

The first one is for optional.assign p7 and vicinity:

"This does not require T to meet the Cpp17CopyAssignable requirements. So the assignment as specified can return something other than T&. Particularly a prvalue whose destruction throws. Only T's destructor is required to not throw."

A type traits check (e.g. is_copy_assignable) does not replace a semantic precondition, e.g. Cpp17CopyAssignable.

All uses of type traits are suspects. For these siblings vocabulary types,
1630859522
besides std::any, the matches when searching for Cpp17 with corresponding type trait constraints are ~3, yet matches for _v are ever present.

@JohelEGP
Copy link
Contributor Author

JohelEGP commented Sep 5, 2021

I did notice that std::tuple<T> didn't even require T to meet the Cpp17Destructible requirements, as opposed to std::optional. Now, a quick test shows that implementations do not swallow exceptions in the destructor, suggesting a defect as it fails to meet its own requirements. It doesn't define a destructor, so this is fine. std::any, on the other hand, does define a destructor yet doesn't impose the named requirement. Here's a proof of implementation divergence: https://godbolt.org/z/Yrszd3d4f.

Amended that. It seems that libstdc++ is more conforming here as it behaves as if it had a non-throwing exception specification. Whereas libc++ lets the exception propagate (opened https://bugs.llvm.org/show_bug.cgi?id=51758).

@JohelEGP
Copy link
Contributor Author

JohelEGP commented Sep 6, 2021

The second one is bad phrasing around "thrown by the construction", because it ignores exceptions thrown by the destruction of temporaries and by the destruction of prvalue results. LWG should form an opinion what the appropriate phrasing should be (possibly in conjunction with a front-matter explanation), and apply that uniformly (or have it editorially applied uniformly).

And not just in the context of Throws:. More generally, when exceptions are involved, it's referring to anything else other than any exception that could be thrown due to an evaluation of a subexpression up to the complete evaluation of its enclosing full-expression.

For example, this is an exception safety guarantee for:

constexpr optional<T>& operator=(const optional& rhs);

If an exception is thrown during the call to T's copy constructor, no effect. -- https://eel.is/c++draft/full#optional.assign-7.sentence-2

An implementation that destroys the parameters at the end of the enclosing full-expression could detect this. And it can be done portably for a returned prvalue whose destruction throws. int[]{(void)x=y, [] { /* Did not throw, yet. */ }()};. One would have to intentionally design for that, though.

On another point: Can one really say "no effect" when one has to destroy T after it has been constructed to comply with the previous sentence?

Remarks: If any exception is thrown, the result of the expression bool(*this) remains unchanged. -- https://eel.is/c++draft/full#optional.assign-7.sentence-1

@JohelEGP
Copy link
Contributor Author

JohelEGP commented Sep 6, 2021

Another example for the need of more generalized wording regarding thrown exceptions:

Preconditions: D meets the Cpp17DefaultConstructible requirements (Table 27), and that construction does not throw an exception. -- https://eel.is/c++draft/unique.ptr.single.ctor#2

This should be, with minimal changes, "and that construction does not cause an exception to be thrown".

@JohelEGP
Copy link
Contributor Author

JohelEGP commented Sep 6, 2021

There is some overlap with #3207.

@jensmaurer
Copy link
Member

This should be, with minimal changes,

"does not exit via an exception"?

(An exception could be thrown inside and caught again, without anyone noticing.)

@jensmaurer jensmaurer added lwg Issue must be reviewed by LWG. not-editorial Issue is not deemed editorial; the editorial issue is kept open for tracking. labels Sep 25, 2021
@jensmaurer
Copy link
Member

This is probably too large to be handled editorially, and needs at least LWG guidance on the approach.

Please submit an LWG issue and let us know the issue number.

@JohelEGP
Copy link
Contributor Author

Definitely.

@JohelEGP JohelEGP changed the title [lib] Extend exception propagation to cover the initialization [lib] Extend exception propagation to cover the initialization LWG 3640 Nov 14, 2021
@JohelEGP
Copy link
Contributor Author

This is now https://wg21.link/LWG3640.

@jensmaurer jensmaurer changed the title [lib] Extend exception propagation to cover the initialization LWG 3640 [lib] Extend exception propagation to cover the initialization LWG3640 Nov 14, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
big An issue causing a large set of changes, scattered across most of the text. lwg Issue must be reviewed by LWG. not-editorial Issue is not deemed editorial; the editorial issue is kept open for tracking.
Projects
None yet
2 participants