为什么这是个坏主意
让我们生成一个可以编译的版本,看看它实际做了什么:
struct MutablePredicate {
mutable bool flag = true;
auto operator()(int i) const -> bool {
if (flag) {
flag = (i != 5);
return true;
} else {
return false;
}
}
};
std::vector<int> v = {8, 2, 5, 6};
auto r = v | std::views::take_while(MutablePredicate{});
fmt::print("First: {}\n", r);
fmt::print("Second: {}\n", r);
这将根据需要首次打印{8, 2, 5}。然后{} 第二次。因为当然,我们修改了谓词,所以我们得到了完全不同的行为。这完全破坏了这个范围的语义(因为你的谓词不能保持相等性),结果各种操作都完全失败了。
生成的take_view 是一个随机访问范围。但是想想当你在其中使用迭代器时会发生什么:
std::vector<int> v = {8, 2, 5, 6};
auto r = v | std::views::take_while(MutablePredicate{});
auto it = r.begin();
it += 2; // this is the 5
assert(it != r.end()); // does not fire, because we're not at the end
assert(it == r.end()); // does not fire, because we're at the end??
这很奇怪,无法推理。
为什么会有不同的约束
C++20 中的范围适配器尝试通过围绕“simple-view”进行优化来最小化模板实例化的数量:V 是 simple-view如果V 和V const 都是具有相同迭代器/哨兵类型的范围。对于这些情况,适配器不会同时提供begin() 和begin() const...它们只是提供后者(因为在这些情况下没有区别,并且begin() const 始终有效,所以我们去做吧)。
我们的案例是一个simple-view,因为ref_view<vector<int>> 只提供begin() const。无论我们是否将该类型迭代为const,我们仍然会从中得到vector<int>::iterators。
因此,take_while_view 为了支持begin() const 需要要求Pred const 是一元谓词,而不仅仅是Pred。由于Pred 无论如何都必须保持平等,因此只要求Pred const 是一元谓词而不是潜在地支持begin() /* non-const */ 如果仅 Pred 而不是 Pred const 是一元谓词。这不是一个值得支持的有趣案例。
filter_view 不是const-iterable,所以不必考虑这个问题。它只用作非const,因此没有Pred const 可以有意义地视为谓词。
你应该怎么做
所以如果你实际上不需要惰性求值,我们可以急切地计算结束迭代器:
auto e = std::ranges::find_if(v, [](int i){ return i == 5; });
if (e != v.end()) {
++e;
}
auto r = std::ranges::subrange(v.begin(), e);
// use r somehow
但如果您确实需要惰性评估,一种方法是创建您自己的适配器。对于双向+范围,我们可以定义一个标记,以便我们匹配迭代器,如果(a)它在底层视图的基数的末尾或(b)它不在范围的开头并且前一个迭代器匹配底层视图的结束。
这样的事情(仅适用于具有 .base() 的视图,因为它仅对 and_one 一个适应范围有意义):
template <std::ranges::bidirectional_range V>
requires std::ranges::view<V>
class and_one_view {
V base_ = V();
using B = decltype(base_.base());
class sentinel {
friend and_one_view;
V* parent_ = nullptr;
std::ranges::sentinel_t<V> end_;
std::ranges::sentinel_t<B> base_end_;
sentinel(V* p)
: parent_(p)
, end_(std::ranges::end(*parent_))
, base_end_(std::ranges::end(parent_->base()))
{ }
public:
sentinel() = default;
auto operator==(std::ranges::iterator_t<V> it) const -> bool {
return it == base_end_ ||
it != std::ranges::begin(*parent_) && std::ranges::prev(it) == end_;
}
};
public:
and_one_view() = default;
and_one_view(V b) : base_(std::move(b)) { }
auto begin() -> std::ranges::iterator_t<V> { return std::ranges::begin(base_); }
auto end() -> sentinel { return sentinel(&base_); }
};
为了演示的目的,我们可以使用 libstdc++ 的内部实现管道化:
struct AndOne : std::views::__adaptor::_RangeAdaptorClosure
{
template <std::ranges::viewable_range R>
requires std::ranges::bidirectional_range<R>
constexpr auto operator()(R&& r) const {
return and_one_view<std::views::all_t<R>>(std::forward<R>(r));
}
};
inline constexpr AndOne and_one;
现在,因为我们遵守所有库组件的所有语义约束,我们可以只使用调整后的范围作为范围:
std::vector<int> v = {8, 2, 5, 6};
auto r = v | std::views::take_while([](int i){ return i != 5; })
| and_one;
fmt::print("First: {}\n", r); // prints {8, 2, 5}
fmt::print("Second: {}\n", r); // prints {8, 2, 5} as well
Demo.