简体   繁体   中英

Are view iterators valid beyond the lifetime of the view?

Say I have a custom container class that stores data in a map:

class Container
{
  public:
    void add(int key, std::string value) { _data.emplace(key, std::move(value)); }

  private:
    std::map<int, std::string> _data;
};

I want to provide an interface to access the values (not the keys) of the map. The ranges library provides std::views::values to give me a range of the map's values:

auto values() { return std::views::values(_data); }

Usage:

Container c;
c.add(1, "a");
c.add(3, "b");
c.add(2, "c");

for (auto &value : c.values())
    std::cout << value << " ";  // Prints "a c b"

But since I want to treat my class as a container, I want to have begin() and end() iterators. Can I do this?

auto begin() { return std::ranges::begin(values()); }
auto end() { return std::ranges::end(values()); }

Here I'm calling values() to get the range to the map's values, and getting an iterator to the beginning of the range (or the sentinel end iterator). But the range itself goes out of scope and is destroyed. Is the iterator itself still valid?

From this example , it seems like the iterator is valid. But is that guaranteed by the standard, either for std::views:values specifically or for views in general?

Are view iterators valid beyond the lifetime of the view?

The property here is called a borrowed range. If a range is a borrowed range, then its iterators are still valid even if a range is destroyed. R& , if R is a range, is the most trivial kind of borrowed range - since it's not the lifetime of the reference that the iterators would be tied into. There are several other familiar borrowed ranges - like span and string_view .

Some range adaptors are conditionally borrowed ( P2017 ). That is, they don't add any additional state on top of the range they are adapting -- so the adapted range can be borrowed if the underlying range is (or underlying ranges are). For instance, views::reverse(r) is borrowed whenever r is borrowed. But views::split(r, pat) isn't conditionally borrowed - because the pattern is stored in the adaptor itself rather than in the iterators (hypothetically, it could also be stored in the iterators, at a cost).

views::values(r) is an example of such: it is a borrowed range whenver r is borrowed. And, in your example, the underlying range is a ref_view , which is itself always borrowed (by the same principle that R& is always borrowed).

Note that here:

auto begin() { return std::ranges::begin(values()); }
auto end() { return std::ranges::end(values()); }

Passing an rvalue range into ranges::begin is only valid if the range is borrowed. That's [range.access.begin]/2.1 :

If E is an rvalue and enable_borrowed_range<remove_cv_t<T>> is false , ranges::begin(E) is ill-formed.

So because your original code compiled, you can be sure that it is valid.

The C++20 standard recognizes the concept of a range type whose iterators can outlive a particular object of that range type. This is called a "borrowed range". The standard defines several types which are borrowed ranges (by providing specializations of ranges::enable_borrowed_range ): subrange , span , string_view , and a couple of others.

However, no other standard library types have specializations of that variable. So those types are not borrowed ranges.

std::views::elements_view (which values_view is an alias of) is not a borrowed range. Therefore, you cannot assume that iterators which outlive the range are still valid.

And even if it were a borrowed range, it is never valid to compare iterators/sentinels from different ranges (even different instances of the same view of the same range).

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM