Barry
Barry

Reputation: 303287

temporary lifetime in range-for expression

Consider a simple class A that can be used as a range:

struct A { 
    ~A() { std::cout << "~A "; }

    const char* begin() const {
        std::cout << "A::begin ";
        return s.data();
    }   

    const char* end() const {
        std::cout << "A::end ";
        return s.data() + s.size();
    }   

    std::string s;
};

If I make a temporary A in a range-for, it works exactly as I would hope:

for (auto c : A{"works"}) {
    std::cout << c << ' ';
} 

// output
A::begin A::end w o r k s ~A 

However, if I try to wrap the temporary:

struct wrap {
    wrap(A&& a) : a(std::move(a))
    { } 

    const char* begin() const { return a.begin(); }
    const char* end() const { return a.end(); }

    A&& a;
};

for (auto c : wrap(A{"fails"})) {
    std::cout << c << ' ';
}

// The temporary A gets destroyed before the loop even begins: 
~A A::begin A::end 
^^

Why is A's lifetime not extended for the full range-for expression, and how can I make that happen without resorting to making a copy of the A?

Upvotes: 25

Views: 3291

Answers (3)

Dalton Messmer
Dalton Messmer

Reputation: 111

Good news: As of C++23, the lifetime of temporaries in range-based for loop initializers has been extended until the end of the loop. See p2718r0.

Upvotes: 5

Daniel Frey
Daniel Frey

Reputation: 56873

The reason the lifetime of the temporary is not extended is how the standard defines range-based for loops in

6.5.4 The range-based for statement [stmt.ranged]

1 For a range-based for statement of the form

for (for-range-declaration:expression)statement

let range-init be equivalent to the expression surrounded by parentheses

( expression )

and for a range-based for statement of the form

for (for-range-declaration:braced-init-list)statement

let range-init be equivalent to the braced-init-list. In each case, a range-based for statement is equivalent to

{
   auto && __range = range-init;
   for ( auto __begin = begin-expr,
              __end = end-expr;
         __begin != __end;
         ++__begin ) {
      for-range-declaration = *__begin;
      statement
   }
}

Note that auto && __range = range-init; would extend the lifetime of a temporary returned from range-init, but it does not extend the lifetime of nested temporaries inside of range-init.

This is IMHO a very unfortunate definition and was even discussed as Defect Report 900. It seems to be the only part of the standard where a reference is implicitly bound to extend the lifetime of an expressions result without extending the lifetime of nested temporaries.

The solution is to store a copy in the wrapper - which often defeats the purpose of the wrapper.

Upvotes: 14

Yakk - Adam Nevraumont
Yakk - Adam Nevraumont

Reputation: 275650

Lifetime extension only occurs when binding directly to references outside of a constructor.

Reference lifetime extension within a constructor would be technically challenging for compilers to implement.

If you want reference lifetime extension, you will be forced to make a copy of it. The usual way is:

struct wrap {
  wrap(A&& a) : a(std::move(a))
  {} 

  const char* begin() const { return a.begin(); }
  const char* end() const { return a.end(); }

  A a;
};

In many contexts, wrap is itself a template:

template<class A>
struct wrap {
  wrap(A&& a) : a(std::forward<A>(a))
  {} 

  const char* begin() const { return a.begin(); }
  const char* end() const { return a.end(); }

  A a;
};

and if A is a Foo& or a Foo const&, references are stored. If it is a Foo, then a copy is made.

An example of such a pattern in use would be if wrap where called backwards, and it returned iterators that where reverse iterators constructed from A. Then temporary ranges would be copied into backwards, while non-temporary objects would be just viewed.

In theory, a language that allowed you to markup parameters to functions and constructors are "dependent sources" whose lifetime should be extended as long as the object/return value would be interesting. This probably is tricky. As an example, imagine new wrap( A{"works"} ) -- the automatic storage temporary now has to last as long as the free store wrap!

Upvotes: 11

Related Questions