Skip to content

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

Description

@JohelEGP

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

Activity

  1. changed the title [-][lib] Extend exception propagation to cover the whole call[/-] [+][lib] Extend exception propagation to cover the initialization[/+] on Sep 4, 2021
  2. jensmaurer commented on Sep 4, 2021

    @jensmaurer
    Member

    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?

  3. added
    bigAn issue causing a large set of changes, scattered across most of the text.
    on Sep 4, 2021
  4. JohelEGP commented on Sep 4, 2021

    @JohelEGP
    ContributorAuthor

    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.

  5. JohelEGP commented on Sep 4, 2021

    @JohelEGP
    Author
  6. JohelEGP commented on Sep 4, 2021

    @JohelEGP
    ContributorAuthor

    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.

  7. JohelEGP commented on Sep 5, 2021

    @JohelEGP
    ContributorAuthor

    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.

  8. jensmaurer commented on Sep 5, 2021

    @jensmaurer
    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.

  9. JohelEGP commented on Sep 5, 2021

    @JohelEGP
    ContributorAuthor

    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.

  10. JohelEGP commented on Sep 5, 2021

    @JohelEGP
    ContributorAuthor

    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".
  11. JohelEGP commented on Sep 5, 2021

    @JohelEGP
    ContributorAuthor

    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.
  12. JohelEGP commented on Sep 5, 2021

    @JohelEGP
    ContributorAuthor

    @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.

  13. JohelEGP commented on Sep 5, 2021

    @JohelEGP
    ContributorAuthor

    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).

  14. JohelEGP commented on Sep 6, 2021

    @JohelEGP
    ContributorAuthor

    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

  15. JohelEGP commented on Sep 6, 2021

    @JohelEGP
    ContributorAuthor

    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".

  16. JohelEGP commented on Sep 6, 2021

    @JohelEGP
    ContributorAuthor

    There is some overlap with #3207.

  17. jensmaurer commented on Sep 25, 2021

    @jensmaurer
    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.)

  18. added
    lwgIssue must be reviewed by LWG.
    not-editorialIssue is not deemed editorial; the editorial issue is kept open for tracking.
    on Sep 25, 2021
  19. jensmaurer commented on Sep 25, 2021

    @jensmaurer
    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.

  20. JohelEGP commented on Sep 25, 2021

    @JohelEGP
    ContributorAuthor

    Definitely.

  21. changed the title [-][lib] Extend exception propagation to cover the initialization[/-] [+][lib] Extend exception propagation to cover the initialization LWG 3640[/+] on Nov 14, 2021
  22. JohelEGP commented on Nov 14, 2021

    @JohelEGP
    ContributorAuthor
  23. changed the title [-][lib] Extend exception propagation to cover the initialization LWG 3640[/-] [+][lib] Extend exception propagation to cover the initialization LWG3640[/+] on Nov 14, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bigAn issue causing a large set of changes, scattered across most of the text.lwgIssue must be reviewed by LWG.not-editorialIssue is not deemed editorial; the editorial issue is kept open for tracking.

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions