gpt4 book ai didi

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

转载 作者:行者123 更新时间:2023-12-04 11:23:04 25 4
gpt4 key购买 nike

TL;博士:我正在使用范围和来自 Ranges library 的相应范围适配器。 .两个系列适配器 std::views::take_while std::views::filter 取一个谓词从输入序列中排除某些元素。为什么take_while拿个 const谓词 while 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;
}
输出:

Value: 8
Value: 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;
}
此代码编译并提供所需的输出:

Value: 8
Value: 2
Value: 5


Code on Wandbox
这引出了我的问题:为什么 std::views::take_while const谓词,而 std::views::filter才不是?

最佳答案

为什么这是个坏主意
让我们生成一个编译版本,看看它实际做了什么:

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<vector<int>>只提供 begin() const .我们是否将该类型迭代为 const与否,我们仍然得到 vector<int>::iterator不在了。
结果, 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)它在底层 View 基的末尾或(b)它不在范围的开头并且前一个迭代器匹配底层 View 的结尾。
像这样的东西(仅适用于具有 .base() 的 View ,因为它只对 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++ - 为什么 Ranges 库中的 std::views::take_while 需要 const 谓词?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/67023834/

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com