为什么 Ranges 库中的 std::views::take_while 需要 const 谓词?

hon*_*onk 5 c++ predicate c++20 std-ranges

TL; DR:我玩弄范围和相应的一系列的适配器范围图书馆。范围适配器std::views::take_whilestd::views::filter谓词都从输入序列中排除某些元素。为什么take_while采取const谓词而filter没有?

背景故事

我有一个std::vector<int>并想对其进行迭代,但我想在击中时停止迭代5。通过使用范围适配器,std::views::take_while我可以实现如下:

std::vector<int> v { 8, 2, 5, 6 };

for (int i : v | std::views::take_while([](int i) { return i != 5; })) {
    std::cout << "Value: " << i << std::endl;
}
Run Code Online (Sandbox Code Playgroud)

输出:

价值:8
价值:2

但是,我现在也想处理5,因此循环必须进一步运行一个迭代步骤。我没有找到合适的范围适配器,所以我写了以下有状态的 lambda 表达式:

auto cond = [b = true](int i) mutable {
    return b ? b = (i != 5), true : false;
};
Run Code Online (Sandbox Code Playgroud)

此 lambda 表达式会记住何时i != 5违反条件并false在下一次调用时返回。然后我将它传递给std::views::take_while如下:

for (int i : v | std::views::take_while(cond)) {
    std::cout << "Value: " << i << std::endl;
}
Run Code Online (Sandbox Code Playgroud)

但是,对于上面的代码,编译器会抛出很长的错误信息。由于找不到问题,我仔细检查了声明,std::views::take_while发现谓词Pred必须是const. 寻找替代方案,我检查了std::views::filter. 有趣的Pred是,并不需要在const这里。所以我将上面的可变 lambda 传递给范围适配器std::views::filter,如下所示:

for (int i : v | std::views::filter(cond)) {
    std::cout << "Value: " << i << std::endl;
}
Run Code Online (Sandbox Code Playgroud)

此代码编译并提供所需的输出:

价值:8
价值:2
价值:5

Wandbox 上的代码

这使我对我的问题:为什么std::views::take_while一个const谓语,而std::views::filter没有?

Bar*_*rry 5

为什么这是一个坏主意

让我们生成一个可以编译的版本,看看它实际做了什么:

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);
Run Code Online (Sandbox Code Playgroud)

这是{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??
Run Code Online (Sandbox Code Playgroud)

这很奇怪,并且使得推理变得不可能。

为什么约束条件不同

C++20 中的范围适配器尝试通过围绕“”进行优化来最小化模板实例化的数量simple-viewV如果simple-viewV都是V const具有相同迭代器/哨兵类型的范围,则 is a。对于这些情况,适配器不提供两者,begin()并且begin() const......它们提供后者(因为这些情况没有区别,并且begin() const总是有效,所以我们就这样做)。

我们的案例是一个simple-view,因为ref_view<vector<int>>只提供了begin() const。无论我们是否迭代该类型const,我们仍然会vector<int>::iterator从中得到 s 。

因此,take_while_view为了支持begin() constneed to require thatPred const是一元谓词,而不仅仅是Pred. 由于Pred无论如何都必须保持相等,因此只要求 thatPred const是一元谓词比潜在支持begin() /* non-const */if only Pred 但 not Pred const是一元谓词更简单。这并不是一个值得支持的有趣案例。

filter_view不是const可迭代的,因此不必考虑这一点。它只被用作 non- 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
Run Code Online (Sandbox Code Playgroud)

但如果您确实需要惰性评估,一种方法是创建您自己的适配器。对于双向+范围,我们可以定义一个哨兵,以便在以下情况下匹配迭代器:(a)它位于基础视图基数的末尾,或者(b)它不在范围的开头并且前一个迭代器与基础视图的匹配结尾。

像这样的东西(只适用于具有 a 的视图,因为它只对适应范围.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_); }
};
Run Code Online (Sandbox Code Playgroud)

为了演示的目的,我们可以使用 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;
Run Code Online (Sandbox Code Playgroud)

现在,因为我们遵守所有库组件的所有语义约束,所以我们可以使用调整后的范围作为范围:

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
Run Code Online (Sandbox Code Playgroud)

演示