24

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?

Barry
  • 286,269
  • 29
  • 621
  • 977

3 Answers3

14

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.

Daniel Frey
  • 55,810
  • 13
  • 122
  • 180
  • You only need to store such a copy when the thing being bound to is a temporary, which helps some. – Yakk - Adam Nevraumont May 01 '15 at 17:36
  • @Yakk Right, but you can not really detect if it is a *real* temporary of the expression or just an rvalue-reference to something which has a longer lifetime. To play it safe, you hence need to store a copy (real copy or move depending on what kind of reference you get). – Daniel Frey May 01 '15 at 17:39
  • if it is an rvalue reference to something "with a longer lifetime", you are making the implicit promise (by taking an rvalue reference) that you won't be holding onto the reference. The caller should be free to reuse that storage for something else, and not worry about you having a pointer-into-its-guts. Keeping an rvalue reference around internally when you don't have full control over the lifetime of your struct and the passed in rvalue reference is a bad idea. – Yakk - Adam Nevraumont May 01 '15 at 17:48
11

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!

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • 1
    In other words, lifetime extension cannot be *chained*. It can be done just *once*. – Nawaz May 01 '15 at 16:04
  • So there's no way to write `backwards` to take a temporary and *not* make a copy of it? If `wrap` didn't have an explicit constructor provided, `wrap{A{}}` would work fine since I would be binding directly to the rvalue ref within `wrap` - so it seems like something like this *should* be possible. – Barry May 01 '15 at 16:08
  • @Barry sure, but not in C++. Even `wrap{A{}}` is not that well supported by compilers last I checked (and sort of fell out of a corner case in the language I suspect). There are, in my experience, only a few cases where this is a problem: an `int arr[3]` or `std::array`. Most other containers are cheap-to-move. For arrays I tend to pass around array-views, which are again cheap-to-move. – Yakk - Adam Nevraumont May 01 '15 at 16:14
2

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.