1

这是我(简化的)尝试实现一个ranges::min_element适用于左值和右值参数的版本:

#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';
}

输出是

42
42

当然它缺少一些实现细节,但思路必须清楚:如果输入范围是右值,则返回值可以存储它的移动副本。因此,算法必须完全有可能std::ranges使用右值参数。

我的问题是:为什么标准相反,只是通过引入那个奇怪的std::ranges::dangling占位符来禁止在其算法中使用右值范围?

4

1 回答 1

11

这种方法有两个问题。

首先,它破坏了算法的语义。(以及返回迭代器的任何其他算法)的要点是min_element将迭代器返回范围中。您没有这样做 - 您将迭代器返回到不同的范围。在这种情况下,这确实混淆了回报甚至意味着什么的概念。您甚至会将这个迭代器与什么进行比较?没有对应的.end()

其次,C++ 中的迭代器模型非常强烈地基于迭代器易于复制的概念。每个算法都按值获取迭代器并自由复制它们。迭代器被认为是轻量级的,重要的是,它是非拥有的。对于前向迭代器,假设迭代器的副本是可互换的。

如果您突然有一个迭代器,该迭代器具有它所引用的成员 ,那么这一切都会中断 。std::vector<T>复制迭代器变得非常昂贵。现在每个不同的迭代器副本实际上都是一个完全不同范围的迭代器?

通过让迭代器有一个成员std::shared_ptr<std::vector<T>>而不是一个std::vector<T>. 这种方式副本更便宜且不再独立,因此您拥有更接近合法迭代器的东西。但是现在您必须进行额外的分配(以创建共享指针),您仍然会遇到问题,即您返回的迭代器与给定的算法范围不同,并且您遇到的问题是算法非常根据您是否提供左值或右值范围而具有不同的语义。

基本上,min_element在右值范围内需要:

  • 只需将迭代器返回到范围内,即使它会悬空
  • 在这样一个可能悬空的迭代器周围返回某种包装器(这是最初的 Ranges 设计,dangling<I>仍然可以让你了解底层I
  • 返回某种类型,表明这不起作用(当前设计)
  • 如果使用会导致悬空(Rust 允许的),则无法完全编译

我认为这里没有其他选择,真的。

于 2021-01-28T21:33:06.713 回答