简体   繁体   中英

Why return std::ranges::safe_iterator_t instead of std::ranges::safe_subrange_t from algorithms taking std::ranges::output_range

I'm writing an algorithm that writes some data to provided output range (initial text of the question included the specifics and that turned discussion in comments into a wrong direction). I want it to be as close as possible in API to other ranges algorithms in the standard library.

I've looked at the latest draft for instances of std::ranges::output_range and found only 2 algorithms:

And they both return std::ranges::safe_iterator_t . I thought it is logical to return std::ranges::safe_subrange_t instead. Even if you write to output stream, you can still return an iterator-sentinel pair in that case and pass that range down the line.

I've found P0970 and it looks like std::ranges::safe_subrange_t was added later. Maybe the algorithms were simply not updated? Or is there a different reason?

The presence of safe_iterator_t in the ranges design can be attributed to two things:

  1. Some algorithms return iterators into the range(s) passed into the algorithm, and
  2. Some ranges have iterators than can outlive their ranges, and some don't.

For (2), an example might be std::string_view . Iterators into the string view are still ok to use even after the string_view object itself has been destroyed. That is because a string_view is just referring to elements elsewhere in memory, and the string_view object itself contains no extra additional state. Counter-examples would be any of the containers; eg, std::vector , which owns its elements, and many of the views in C++20's std::ranges namespace, most of which contain additional state ( views::filter 's predicate, for instance).

Taking the two bullets above together, now consider a function like find (simiplifying):

template <input_range R, class T>
  requires ...
safe_iterator_t<R> find(R && rg, const T & val);

This function returns an iterator into the range rg , but if rg is an rvalue, then it will probably get deleted when the function returns. That means the returned iterator is almost certainly dangling.

safe_iterator_t checks to see if R is one of those special range type for which the iterators can safely outlive the range. If so, you just get the iterator back, no muss no fuss. If not, then this function returns an empty object of a special type named std::ranges::dangling . That is intended to clue you in to the fact that you need to be thinking more deeply about lifetime here.

The same logic holds for algorithms that take output ranges, like ranges::fill and ranges::generate .

So why not return safe_subrange_t instead of safe_iterator_t , you might ask? Wouldn't that make the algorithm compose nicely with other algorithms?

It would! But it would be returning to the caller information they already have; namely, the position of the end of the range. In the algorithms, we avoid doing needless work to make them as efficient as possible. Given ABIs and calling conventions, returning a pointer (eg, the found position) is more efficient than returning a struct containing two pointers (eg, the found position and the end of the range).

Rather, we use higher-level views (and actions, in range-v3) for composing multiple operations concisely.

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