Skip to content

CWG3043 [class.temporary] Range extension for enumerating expansion statements #724

Description

@jakubjelinek

Full name of submitter (unless configured in github; will be published with the issue): Jakub Jelinek

Reference (section label): [class.temporary]

Link to reflector thread (if any):

Issue description: P1306R5 adjusted the fourth context bullet so that it applies also to enumerating expansion statements.
The rest of the bullet talks about for-range-initializer which doesn't apply to enumerating expansion statement and references, but in enumerating expansion statements there might be no references.
"The fourth context is when a temporary object is created in the for-range-initializer of either a range-based for statement or an enumerating expansion statement ([stmt.expand]). If such a temporary object would otherwise be destroyed at the end of the for-range-initializer full-expression, the object persists for the lifetime of the reference initialized by the for-range-initializer."
So, I think enumerating expansion statement temporary extension should probably be handled in a separate bullet and make it clear what is being extended and in which cases.
Consider

struct T { int t; };
struct S { const T &s; }; S foo (const T &x) { S s {x}; return s; }
void bar () {
  template for (auto a = { 42, foo ({ 42 }), 1LL })
    ;
}

For S1 this adds auto a = foo ({ 42 }); declaration before instantiation of the compound-statement. a is not a reference, in this case it contains a reference non-static data member, but it could very well be e.g. a pointer as well. Is the intent that the temporary is only destroyed at the end of the compound-statement, regardless if for-range-declaration has reference type or not?

Suggested resolution:

Activity

  1. jensmaurer commented on Jul 7, 2025

    @jensmaurer
    Member

    The general idea of this for-range "temporary extension" rule is that temporaries created in the initializer are lifetime-extended for the invented "auto&& range = init" statement.

    I appreciate this doesn't really fit the enumerating case here, in particular when initializing a non-reference variable (we expect a copy before anything can dangle in the usual cases).

  2. jakubjelinek commented on Jul 7, 2025

    @jakubjelinek
    Author

    It could be even

    struct T { int t; };
    struct S { const T *s; }; S foo (const T &x) { S s {&x}; return s; }
    struct U { const int *s; }; U bar (const int &x) { U u {&x}; return u; }
    void baz () {
      template for (auto a = { foo ({ 42 }), bar (42) })
       {
         auto b = *a.s;
       }
    }

    with no references at all, and the question is if it should be lifetime extended even in that case (until end of the compound-statement).
    So one possible wording for the enumerating case could be:
    "If such a temporary object would otherwise be destroyed at the end of the ui full-expression, the object persists for the lifetime of the for-range-declaration."
    but it would need to link to the ui from [stmt.expand]/(5.3) (at least if it should be destructed for each "iteration" separately at the end of that "iteration".

  3. jensmaurer commented on Jul 7, 2025

    @jensmaurer
    Member

    Your example would also be a trap if someone wrote auto x = foo({42}); in regular code, so I'm not sure we want to prevent anything from dangling here.

    If (big if) we lifetime-extend anything in this case, we certainly should destroy at the end of the corresponding "iteration".

  4. jakubjelinek commented on Jul 7, 2025

    @jakubjelinek
    Author

    Sure, and so does the case with & members. One possibility is of course to extend only if for-range-declaration has a reference type. In the stmt.ranged case auto &&range = for-range-initializer ; always has.

  5. jensmaurer commented on Jul 8, 2025

    @jensmaurer
    Member

    Yes, but in the [stmt.range] case (and the other expansion statement cases), we have an expression that describes the entire range (and it's surprising those temporaries don't survive). For the enumerating expansions, we evaluate the values for the individual "iterations" separately, so there is no "covers the entire range" situation that would be surprising. I think. CWG issue time.

  6. jakubjelinek commented on Jul 14, 2025

    @jakubjelinek
    Author

    Also, wonder what is the point of having the fifth case talk about iterating expansion statements. In that case expansion-initializer is always used in

    static constexpr auto&& range = expansion-initializer ;

    and so has to be a constant expression, so I don't see how something would need to be lifetime extended in that case.
    For destructuring expansion statements sure, those don't have to be constexpr, so lifetime extension makes sense there.

  7. jensmaurer commented on Jul 27, 2025

    @jensmaurer
    Member
  8. changed the title [-][class.temporary] Range extension for enumerating expansion statements[/-] [+]CWG3043 [class.temporary] Range extension for enumerating expansion statements[/+] on Jul 27, 2025
  9. t3nsor commented on Aug 25, 2025

    @t3nsor

    in the [stmt.range] case (and the other expansion statement cases), we have an expression that describes the entire range (and it's surprising those temporaries don't survive). For the enumerating expansions, we evaluate the values for the individual "iterations" separately, so there is no "covers the entire range" situation that would be surprising. I think.

    This argument suggests that destroying temporaries created in one element of the expression-list before the next element is evaluated would not be surprising, but it doesn't argue for either side of "should we lifetime-extend until the end of the iteration".

    As for that, one of the points that was brought up in EWG when discussing lifetime extension in the range-based for loop was the fact that, even though auto&& r = foo().bar() doesn't lifetime-extend the foo() temporary (and we expect the user to be aware of this and act accordingly), the range-based for loop case is special because the syntax doesn't explicitly show you the reference initialization. To restate in Core lingo, the boundaries of the full-expression in which foo().bar() is evaluated are not made obvious to the user. This makes the syntax a footgun.

    The same logic applies to the enumerating expansion statement: the user is not actually shown the full-expression in which each element of the list is evaluated, so if we don't lifetime-extend the temporaries until the end of the corresponding iteration then it's going to be a footgun.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions