Kevin
Kevin

Reputation: 7324

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?

Upvotes: 12

Views: 688

Answers (1)

Barry
Barry

Reputation: 302758

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 whenever 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.

Upvotes: 10

Related Questions