简体   繁体   中英

Why do std range algorithms return std::ranges::dangling for rvalue arguments instead of… well, just working?

Here is my (simplified) attempt to implement a ranges::min_element version that would work for both lvalue and rvalue arguments:

#include <iterator>
#include <algorithm>
#include <type_traits>
#include <utility>

namespace better_std_ranges
{
    template<typename Range>
    constexpr auto min_element(Range& range)
    {
        using std::begin;
        using std::end;
        return std::min_element(begin(range), end(range));
    }

    template<typename Range>
    constexpr auto min_element(Range&& range)
    {
        static_assert(!std::is_reference_v<Range>, "wrong overload chosen");

        class _result_iterator_type // todo: inherit from some crtp base that will provide lacking operators depending on _underlying_iterator_type::iterator_category
        {
            using _underlying_iterator_type = std::decay_t<decltype(std::begin(std::declval<Range&>()))>;

        public:
            explicit constexpr _result_iterator_type(Range&& range) noexcept(std::is_nothrow_move_constructible_v<Range>)
            : _underlying_range{std::move(range)}
            , _underlying_iterator(::better_std_ranges::min_element(_underlying_range))
            {
            }

            using difference_type   = typename _underlying_iterator_type::difference_type;
            using value_type        = typename _underlying_iterator_type::value_type;
            using pointer           = typename _underlying_iterator_type::pointer;
            using reference         = typename _underlying_iterator_type::reference;
            using iterator_category = typename _underlying_iterator_type::iterator_category;

            constexpr decltype(auto) operator*() const
            {
                return *_underlying_iterator;
            }

            // todo: define other member functions that were not provided by the inheritance above

        private:
            Range _underlying_range;
            _underlying_iterator_type _underlying_iterator;
        };

        return _result_iterator_type{std::move(range)};
    }
}

#include <vector>
#include <iostream>

auto make_vector()
{
    return std::vector{100, 200, 42, 500, 1000};
}

int main()
{
    auto lvalue_vector = make_vector();
    auto lvalue_vector_min_element_iterator = better_std_ranges::min_element(lvalue_vector);
    std::cout << *lvalue_vector_min_element_iterator << '\n';

    auto rvalue_vector_min_element_iterator = better_std_ranges::min_element(make_vector());
    std::cout << *rvalue_vector_min_element_iterator << '\n';
}

The output is

42
42

Surely it lacks some implementation details, but the idea must be clear: if an input range is an rvalue, the return value can store a moved copy of it. Thus it must be totally possible for std::ranges algorithms to work with rvalue arguments.

My question is: why does the standard go the opposite way and just prohibit using rvalue ranges with its algorithms by introducing that strange std::ranges::dangling placeholder?

There are two problems with this approach.

First, it breaks the semantics of the algorithm. The point of min_element (and any other algorithm that returns an iterator) is to return an iterator into the range. You're not doing that - you're returning an iterator into a different range. That really confuses the notion of what the return even means in this case. What would you even compare this iterator to? There's no corresponding .end() ?

Second, the iterator model in C++ is based very strongly around the notion that iterators are cheap to copy. Every algorithm takes iterators by value and copies them around freely. Iterators are assumed to be light-weight and, importantly, non-owning. For forward iterators, copies of an iterator are assumed to be interchangeable.

Everything about this breaks if you suddenly have an iterator that has member std::vector<T> that it refers into. Copying iterators becomes very expensive. And now each distinct iterator copy is actually an iterator into a completely different range?

You can do a little bit better by having the iterator have a member std::shared_ptr<std::vector<T>> instead of a std::vector<T> . This way copies are much cheaper and no longer independent, so you have something closer to a legitimate iterator. But now you have to do an extra allocation (to create the shared pointer), you still have the issue where the iterator you're returning is into a different range than the algorithm was given, and you have the issue where the algorithm has very different semantics based on whether you provide an lvalue or rvalue range.

Basically, min_element on an rvalue range needs to either:

  • just return an iterator into the range, even if it will dangle
  • return some kind of wrapper around such a potentially-dangling iterator (this was the original Ranges design, dangling<I> could still let you get at the underlying I )
  • return some kind of type indicating that this doesn't work (the current design)
  • fail to compile entirely if usage would lead to dangling (what Rust would allow for)

I don't think there's another option here, really.

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