11

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?

cigien
  • 57,834
  • 11
  • 73
  • 112
Kevin
  • 6,993
  • 1
  • 15
  • 24
  • 3
    Some ranges have iterators that doesn't depend on the range itself, some don't. See [Borrowed Range](https://en.cppreference.com/w/cpp/ranges/borrowed_range) – BoP Jan 29 '22 at 20:07
  • @BoP Ah, that's what I was missing, thanks. If you make a proper answer I'll accept it. – Kevin Jan 29 '22 at 20:09
  • @BoP Even then, does it guarantee that iterators obtained from two different constructions of the view into the same range (as is the case here) can form a valid range? – user17732522 Jan 29 '22 at 20:12
  • I don't think there are any explicit guarantees. For example iterators from a string_view depends on the underlying string, and not the string_view. However, I haven't seen that two string_views into the same string can mix and match their iterators. :-) – BoP Jan 29 '22 at 20:33

1 Answers1

8

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.

dfrib
  • 70,367
  • 12
  • 127
  • 192
Barry
  • 286,269
  • 29
  • 621
  • 977
  • 1
    `std::ranges::begin(rvalue)` being ill formed if `rvalue` isn't a borrowed range is really helpful, thanks. But what about the point others have made about comparing iterators from different views, even though the views are to the same data? – Kevin Jan 29 '22 at 21:00
  • @Kevin If it's a borrowed range, then the iterators are into the same underlying thing, so it's fine. – Barry Jan 29 '22 at 21:03
  • 2
    It seems that P2017 was only applied after N4861. Is it still supposed to apply in C++20? – user17732522 Jan 30 '22 at 00:06
  • 1
    @user17732522 Yes. – Barry Jan 30 '22 at 00:11