Reputation: 1931
I've written a class for using it as a convenient view e.g. in range-based for
s. Overall, it is just a pair of iterators with bound checking:
template<typename I> class Range {
private:
I begin;
I end;
public:
Range(I const begin, I const end)
: begin(begin), end(end)
{}
Range<I>& skip(int const amount) {
begin = std::min(begin + amount, end);
return *this;
}
};
template<typename C> auto whole(C const& container) {
using Iterator = decltype(std::begin(container));
return Range<Iterator>(std::begin(container), std::end(container));
}
Here's the intended usage of it (which led to UB):
std::vector<int> const vector{1, 2, 3};
for (int const value : whole(vector).skip(1)) {
std::cout << value << ' ';
}
Removing the skip(1)
part helps, same about the following refactoring of Range::skip
:
Range<I> skip(int const amount) const {
I const new_begin = std::min(begin + amount, end);
return Range<I>(new_begin, end);
}
It seems like a temporary mustn't return a reference to itself. Here's what cppreference says:
All temporary objects are destroyed as the last step in evaluating the full-expression that (lexically) contains the point where they were created, and if multiple temporary objects were created, they are destroyed in the order opposite to the order of creation.
Though I'm not sure it's the case and I don't now how to interpret it practically. What's the actual problem and how can I reliably avoid such UB? Are alike expressions e.g. auto string = std::string("abc").append("def")
also unsafe?
Upvotes: 2
Views: 359
Reputation: 172894
In ranged-based for loop the range-expression is bound to reference as (prototype code)
auto && __range = whole(vector).skip(1) ;
The problem is the temporary created by whole(vector)
is destroyed immediately after the full expression, the reference __range
(which binds to the returned reference from skip
, i.e. the temporary) becomes dangled; after that any dereference on it leads to UB.
auto string = std::string("abc").append("def")
is fine, string
gets copied, it's an independent object from the temporary std::string
.
Since C++20 you can add init-statement:
If range_expression returns a temporary, its lifetime is extended until the end of the loop, as indicated by binding to the forwarding reference __range, but beware that the lifetime of any temporary within range_expression is not extended.
E.g.
std::vector<int> const vector{1, 2, 3};
for (auto thing = whole(vector); int const value : thing.skip(1)) {
std::cout << value << ' ';
}
Upvotes: 1
Reputation: 409166
Regarding std::string
and the append
function, the append
function returns a reference to the string it appends to. Keeping this reference after the string object is destructed will lead to undefined behavior if you use it.
However if you copy the string object, then you're safe, because you will have a copy of the string and not a reference to a non-existing object:
std::string s = std::string("abc").append("def");
Here s
will be a copy, initialized through copy-initialization (it passes std::string("abc").append("def")
to the copy-constructor for s
, and the temporary object will live all throughout that constructor).
As for
for (int const value : whole(vector).skip(1)) { ... }
If the Range<T>
class was modified to be iterable (you need a begin
and end
functions to return the iterators), then it still would not be UB.
That's because such a range-for loop is corresponding to a loop like
for (auto iter = whole(vector).skip(1).begin();
iter != whole(vector).skip(1).end();
++iter)
{
...
}
The Range<T>
class doesn't contain a copy of the vector, it contains copies of the iterator of the vector (for your shown example). These iterators would be copied or used before the temporary Range<T>
object is destructed.
Upvotes: 1
Reputation: 62636
A range-for holds a reference to the range. Your example
for (int const value : whole(vector).skip(1)) {
std::cout << value << ' ';
}
is defined to be equivalent to
{
auto && __range = whole(vector).skip(1);
auto __begin = __range.begin();
auto __end = __range.end();
for ( ; __begin != __end; ++__begin) {
int const value = *__begin;
std::cout << value << ' ';
}
}
The reference returned by skip
becomes invalid at the first ;
.
Where you are initialising an object, like
std::string s = std::string("abc").append("def");
it is safe, because the temporary only needs to outlive the constructor.
As an aside, I prefer Range<I> skip(int) const
over the mutating one.
Upvotes: 1