Barry Barry - 2 months ago 12
C++ Question

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
?

Answer

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!