Reputation: 303287
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
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
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)
statementlet 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)
statementlet 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
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