7

这个程序:

#include <ranges>
#include <numeric>
#include <iostream>

int main() {
    auto rng = std::ranges::istream_view<int>(std::cin);
    std::cout << std::accumulate(std::ranges::begin(rng), std::ranges::end(rng), 0);
}

应该总结所有在标准输入流中显示为文本的整数。但是 - 它不能编译。我知道std::ranges::begin()并且std::ranges::end()存在,所以发生了什么事?编译器告诉我找不到合适的候选人;为什么?

4

2 回答 2

10

从开始到 C++17,所有内容<algorithm>都基于迭代器对:你有一个iterator引用范围的开头和一个iterator引用范围的结尾,始终具有相同的类型。

在 C++20 中,这是通用的。范围现在由该迭代器的 aniterator和 asentinel表示 - 其中它sentinel本身实际上不需要是任何类型的迭代器,它只需要是一个可以比较等于其对应迭代器的类型(这是sentinel_for概念)。

C++17 范围往往是†</sup> 有效的 C++20 范围,但不一定是相反的方向。一个原因是拥有不同sentinel类型的能力,但还有其他的,这也涉及到这个问题(见下文)。

为了配合新模型,C++20 在命名空间中添加了大量算法,这些算法std::ranges采用 aniterator和 a sentinel,而不是两个iterators。例如,虽然我们一直有:

template<class InputIterator, class T>
  constexpr InputIterator find(InputIterator first, InputIterator last,
                               const T& value);

我们现在还有:

namespace ranges {
  template<input_­iterator I, sentinel_­for<I> S, class T, class Proj = identity>
    requires indirect_­binary_­predicate<ranges::equal_to, projected<I, Proj>, const T*>
    constexpr I find(I first, S last, const T& value, Proj proj = {});

  template<input_­range R, class T, class Proj = identity>
    requires indirect_­binary_­predicate<ranges::equal_to,
                                       projected<iterator_t<R>, Proj>, const T*>
    constexpr borrowed_iterator_t<R>
      find(R&& r, const T& value, Proj proj = {});
}

这里的第一个重载采用iterator/sentinel对,而第二个重载采用一个范围。


虽然许多算法在 中添加了相应的重载std::ranges,但其中的重载<numeric>被忽略了。有std::accumulate但没有std::ranges::accumulate。因此,我们目前唯一可用的版本是采用迭代器对的版本。否则,您可以只写:

auto rng = std::ranges::istream_view<int>(std::cin);
std::cout << std::ranges::accumulate(rng, 0);

不幸的是,std::ranges::istream_view它是新的 C++20 范围之一,其标记类型与其迭代器类型不同,因此您不能传递rng.begin()rng.end()进入std::accumulate其中任何一个。

这通常为您留下两个选项(三个,如果您包括等待 C++23,希望有一个std::ranges::fold):

  1. 编写您自己的基于范围和基于迭代器哨兵的算法。这fold很容易做到。

或者

  1. 有一个实用程序可以将 C++20 范围包装成与 C++17 兼容的范围:views::common. 所以你可以这样:
auto rng = std::ranges::istream_view<int>(ints) | std::views::common;
std::cout << std::accumulate(rng.begin(), rng.end(), 0);

除非在这种特定情况下。

istream_view的迭代器是不可复制的,在 C++17 中所有的迭代器都必须是。所以实际上没有办法提供基于istream_view. 您需要适当的 C++20 范围支持。未来std::ranges::fold将支持只移动视图和只移动迭代器,但std::accumulate永远不会。

在这种情况下,只留下选项 1。


†</sup>C++20 迭代器必须是可默认构造的,这不是 C++17 迭代器的要求。因此,具有不可默认构造的迭代器的 C++17 范围将不是有效的 C++20 范围。

于 2020-12-15T20:25:04.783 回答
4

问题在于,在一般情况下,C++ 范围的结尾不是迭代器,而是哨兵。哨兵可以具有与迭代器不同的类型并允许更少的操作 - 因为,一般来说,您最需要与它进行比较才能知道您已经到达范围的末尾,并且可能不允许像使用它一样使用它任何迭代器。有关此区别的更多信息,请阅读:

哨兵和结束迭代器有什么区别?

现在,标准库算法(包括 中的算法<numeric>)采用相同类型的迭代器对。在您的示例中:

template< class InputIt, class T >
constexpr T accumulate( InputIt first, InputIt last, T init );

看?InputIt必须是范围开头和结尾的类型。这甚至可以(可能)不是“固定”的istream_view范围,因为标准输入的结尾本身实际上不是迭代器。(虽然也许你可以把它变成一个迭代器并在用它做不相关的事情时抛出异常。)

因此,我们需要一个新的变体std::accumulatestd::ranges::accumulate,而我们目前还没有. 或者,当然,您可以自己编写,这应该不会太难。

编辑: @RemyLebeau 建议的最后一个选项是std::istream_iterator改用:

std::cout << std::accumulate(
    std::istream_iterator<int>(std::cin), 
    std::istream_iterator<int>(), 
    0);
于 2020-12-15T19:50:22.500 回答