问题描述
sized_range
概念细化了范围,要求
范围内的元素数量可以在摊销中确定
恒定时间使用ranges::size
。
template<class T>
concept sized_range =
range<T> &&
requires(T& t) { ranges::size(t); };
标准规定获得ranges::sized_range
的大小保证在恒定时间内。考虑以下几点:
auto r1 = std::views::iota(0)
| std::views::filter([](int x){ return x % 2 == 0; })
| std::views::take(1'000'000);
r1
显然不是 sized_range
,因为不可能在
常数时间,这也意味着我们使用 ranges::size
来评估其大小也是格式错误的。
但是我偶然发现如果我们在它上面应用views::reverse
,新的范围r2
突然变成了sized_range
,我们可以直接使用ranges::size
得到它的大小正确,godbolt:
auto r2 = r1 | views::reverse;
static_assert(!ranges::sized_range<decltype(r1)>);
static_assert( ranges::sized_range<decltype(r2)>);
std::cout << std::ranges::size(r2) << "\n"; // print 500'000
然而,很明显,新范围 r2
不是 sized_range
,因为我们永远无法在恒定时间内获得它的大小,这似乎违反了标准所说的。
为什么 views::reverse
可以将非sized_range
转换为 sized_range
?显然,这种转换不会对原始范围的大小产生任何影响。这是标准缺陷还是库错误?
解决方法
要求是摊销恒定的,并不总是恒定的。
-
take_view<...>
产生counted_iterator
。 - 所以
reverse_view<take_view<...>>
产生reverse_iterator<counted_iterator<...>>
-
counted_iterator
始终可以减去:您只需减去计数即可。 - 所以
reverse_iterator<counted_iterator<...>>
也总是可以减去。 -
ranges::size
是为迭代器/哨兵模型sized_sentinel_for
的任何范围定义的。这包括reverse_view<take_view<...>>
。
为了满足分摊的常数复杂度要求,reverse_view::begin
会在需要计算源范围的末尾时缓存它(即源范围不常见)。