【问题标题】:Why does std::views::take_while from the Ranges library require a const predicate?为什么 Ranges 库中的 std::views::take_while 需要 const 谓词?
【发布时间】:2021-07-05 11:52:41
【问题描述】:

TL;DR:我正在使用Ranges library 中的范围 和相应的范围适配器。范围适配器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;
}

输出:

值:8 值:2

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

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

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

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

但是,对于上面的代码,编译器会抛出一个long error message。由于没发现问题,我仔细查看了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;
}

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

值:8 值:2
值:5

Code on Wandbox

这引出了我的问题:为什么 std::views::take_whileconst 谓词,而 std::views::filter 不是?

【问题讨论】:

  • 可能是因为filter 没有模拟random_access_rangetake_while 有。我想这可能需要乱序调用谓词,这可能会抛出一个有状态的谓词。
  • @IgorTandetnik:好点;至少这对我来说听起来很明智。但是,我无法想象谓词会被乱序调用的用例。如果有人知道可能发生的情况,请告诉我们:)
  • @IgorTandetnik 不是这样,不是。看我的回答。

标签: c++ c++20 predicate std-ranges


【解决方案1】:

为什么这是个坏主意

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

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”进行优化来最小化模板实例化的数量:Vsimple-view如果VV const 都是具有相同迭代器/哨兵类型的范围。对于这些情况,适配器不会同时提供begin()begin() const...它们只是提供后者(因为在这些情况下没有区别,并且begin() const 始终有效,所以我们去做吧)。

我们的案例是一个simple-view,因为ref_view&lt;vector&lt;int&gt;&gt; 只提供begin() const。无论我们是否将该类型迭代为const,我们仍然会从中得到vector&lt;int&gt;::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.

【讨论】:

  • 非常感谢!我还不知道 C++20 的底层是什么。你让我读了一些关于概念和哨兵的知识,我想我现在大致理解了你的答案。一件事:我猜“simple-view”是指this,对吧?我可以从声明中看到它必须是range,但是从哪里可以看到VV const 必须具有相同的迭代器/哨兵类型?
  • Barry 的范围确实有一些等同于 std::next 的东西,这会给 op 提供包含 5 的视图?我知道范围很棘手,因为不清楚您是否要在最后移动或扩展窗口...
  • @NoSenseEtAl 如果你想用next,确定我用if (e != v.end()) ++e;写的代码也可以是e = ranges::next(e, 1, v.end());
  • @Barry 是的,这就是我的意思。我希望有一些更短的东西,但我猜范围不想支付知道底层范围的开销 .end()... :/
【解决方案2】:

std::indirect_unary_predicate 必须是std::predicate,而a std::regular_invocable 必须是a std::regular_invocable。而需要:

invoke 函数调用表达式应保持相等 ([concepts.equality]),并且不得修改函数对象或参数。

您的 lambda 不是“保持平等”,因为相同的输入会产生不同的输出,并且它肯定会修改函数对象。由于你的类型不满足概念的非句法条件,所以你的代码有UB。

所以说真的,一个是const 而另一个不是,这并不重要;这些概念实际上要求他们表现得好像他们是const

【讨论】:

  • 只是为了确认我是否正确:您的意思是将我的可变 lambda 传递给 std::views::filter 会导致 UB?如果是这样,我想知道为什么我没有被阻止这样做。至少std::views::take_while 可以;)
  • @honk:使用const Pred 并不能保证谓词是保持平等的。这就是为什么std::regular_invocable 不需要它被赋予的类型是const 或其他东西。所以它不能为任何事情提供任何更大的保证。
  • 我想我开始明白了。我会尽量小心,不要过多地使用谓词。谢谢你让我避免了很多未来的错误! ;)
猜你喜欢
  • 2022-11-24
  • 2022-12-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-01-11
  • 2019-01-28
  • 2021-12-09
  • 2020-11-15
相关资源
最近更新 更多