复杂范围的多个迭代器
Multiple iterators to a complex range
我正在尝试让多个迭代器到达更复杂的范围(使用range-v3库)——使用filter
、for_each
和yield
手动实现笛卡尔乘积。然而,当我试图将多个迭代器保持在这样的范围内时,它们共享一个共同的值。例如:
#include <vector>
#include <iostream>
#include <range/v3/view/for_each.hpp>
#include <range/v3/view/filter.hpp>
int main() {
std::vector<int> data1{1,5,2,7,6};
std::vector<int> data2{1,5,2,7,6};
auto range =
data1
| ranges::v3::view::filter([](int v) { return v%2; })
| ranges::v3::view::for_each([&data2](int v) {
return data2 | ranges::v3::view::for_each([v](int v2) {
return ranges::v3::yield(std::make_pair(v,v2));
});
});
auto it1 = range.begin();
for (auto it2 = range.begin(); it2 != range.end(); ++it2) {
std::cout << "[" << it1->first << "," << it1->second << "] [" << it2->first << "," << it2->second << "]n";
}
return 0;
}
我期望迭代器it1
一直指向范围的开头,而迭代器it2
遍历整个序列。令我惊讶的是,it1
也增加了!我得到以下输出:
[1,1] [1,1]
[1,5] [1,5]
[1,2] [1,2]
[1,7] [1,7]
[1,6] [1,6]
[5,1] [5,1]
[5,5] [5,5]
[5,2] [5,2]
[5,7] [5,7]
[5,6] [5,6]
[7,1] [7,1]
[7,5] [7,5]
[7,2] [7,2]
[7,7] [7,7]
[7,6] [7,6]
- 为什么
- 我该如何避免这种情况
- 如何保持多个独立迭代器指向范围的不同位置
- 我应该以不同的方式实现笛卡尔乘积吗?(这是我之前的问题)
虽然它没有反映在上面的MCVE中,但考虑一个用例,其中有人试图实现类似于std::max_element
的东西——试图将迭代器返回到叉积中的最高值对。在寻找最高值时,您需要将迭代器存储到当前的最佳候选者。它在搜索时无法更改,如果您需要该范围的副本(如其中一个答案所示),则管理迭代器会很麻烦。
将整个叉积具体化也不是一种选择,因为它需要大量内存。毕竟,将范围与过滤器和其他动态转换一起使用的全部目的是避免这种物质化。
结果视图存储的状态似乎是单次通过的。您只需制作所需的视图副本即可解决此问题:
int main() {
std::vector<int> data1{1,5,2,7,6};
std::vector<int> data2{1,5,2,7,6};
auto range =
data1
| ranges::v3::view::filter([](int v) { return v%2; })
| ranges::v3::view::for_each([&data2](int v) {
return data2 | ranges::v3::view::for_each([v](int v2) {
return ranges::v3::yield(std::make_pair(v,v2));
});
});
auto range1= range; // Copy the view adaptor
auto it1 = range1.begin();
for (auto it2 = range.begin(); it2 != range.end(); ++it2) {
std::cout << "[" << it1->first << "," << it1->second << "] [" << it2->first << "," << it2->second << "]n";
}
std::cout << 'n';
for (; it1 != range1.end(); ++it1) { // Consume the copied view
std::cout << "[" << it1->first << "," << it1->second << "]n";
}
return 0;
}
另一种选择是将视图具体化为注释中提到的容器。
记住前面提到的单程视图的限制,实现max_element
并不困难函数返回迭代器,其重要缺点是必须计算一次半序列。
这里有一个可能的实现:
template <typename InputRange,typename BinaryPred = std::greater<>>
auto my_max_element(InputRange &range1,BinaryPred &&pred = {}) -> decltype(range1.begin()) {
auto range2 = range1;
auto it1 = range1.begin();
std::ptrdiff_t pos = 0L;
for (auto it2 = range2.begin(); it2 != range2.end(); ++it2) {
if (pred(*it2,*it1)) {
ranges::advance(it1,pos); // Computing again the sequence as the iterator advances!
pos = 0L;
}
++pos;
}
return it1;
}
这里发生了什么
这里的整个问题源于这样一个事实,即std::max_element
要求其参数为LeccyForwardIterator,而ranges::v3::yield
创建的范围显然(显然?)只提供LeccyInputIterator。不幸的是,range-v3文档没有明确提到可以预期的迭代器类别(至少我没有发现它被提及)。这确实是一个巨大的增强,因为所有标准库算法都明确说明了它们需要什么迭代器类别。
在std::max_element
的特殊情况下,您不是第一个偶然发现ForwardIterator
而不仅仅是InputIterator
这一违反直觉的要求的人,请参阅为什么std::max_element需要ForwardIterator?例如总之,这是有意义的,因为std::max_element
并不是(尽管名称暗示它)返回max元素,而是返回max元素的迭代器。因此,特别是InputIterator
上缺少多通路保证,以便使std::max_element
与之一起工作
因此,许多其他标准库函数也不适用于std::max_element
,例如std::istreambuf_editor,这真的很遗憾:您无法从具有现有标准库的文件中获取max元素!您要么必须先将整个文件加载到内存中,要么必须使用自己的max算法。
标准库只是缺少一个真正返回max元素的算法,而不是一个指向max元素的迭代器。这种算法同样适用于CCD_ 18s。当然,这可以很容易地手动实现,但由标准库提供这一点仍然很方便。我只能推测为什么它不存在。也许一个原因是,它需要value_type
是可复制的,因为InputIterator
不需要返回对元素的引用,而max算法制作副本可能违反直觉。。。
那么,现在关于您的实际问题:
为什么(即,为什么您的范围只返回InputIterator
秒?)
显然,yield
会动态创建值。这是经过设计的,这正是人们想要使用yield的原因:不必预先创建(从而存储)范围。因此,我不知道如何以满足多路径保证的方式实现收益,尤其是第二个项目让我头疼:
- 如果a和b比较相等(a==b在上下文中可转换为true),则它们要么都是不可引用的,要么*a和*b是绑定到同一对象的引用
从技术上讲,我可以想象,可以用一种方式实现yield
,即从一个范围创建的所有迭代器共享一个公共的内部存储,该存储在第一次遍历期间动态填充。然后,不同的迭代器可以为您提供对底层对象的相同引用。但是std::max_element
会无声地消耗O(n²)
内存(笛卡尔乘积的所有元素)。因此,在我看来,最好不要这样做,而是让用户自己实现范围,让他们意识到这一点。
如何避免这种情况
好吧,正如metalfox已经说过的,您可以复制您的视图,这将导致不同的范围,从而产生独立的迭代器。不过,这并不能使std::max_element
发挥作用。因此,鉴于yield
的性质,不幸的是,这个问题的答案是:使用yield
或任何其他动态创建值的技术都无法避免这种情况。
如何保持多个独立迭代器指向范围的不同位置
这与前面的问题有关。基本上,这个问题本身就回答了:如果你想在的不同位置中指向独立的迭代器,这些位置必须存在于内存中的某个地方。因此,您至少需要实现那些曾经有迭代器指向它们的元素,在std::max_element
的情况下,这意味着您必须实现所有这些元素。
我应该以不同的方式实现笛卡尔乘积吗
我可以想象许多不同的实现。但它们中没有一个能够同时提供这两种特性:
- 返回
ForwardIterator
s - 需要少于
O(n²)
的内存
从技术上讲,可以实现一个专门用于std::max_element
的迭代器,这意味着它只在内存中保留当前的最大元素,以便可以引用它。。。但这会有点可笑,不是吗?我们不能指望像range-v3这样的通用库能够产生这样高度专业化的迭代器类别。
摘要
你说的是
毕竟,我不认为我的用例是如此罕见的异常值和范围计划添加到C++20标准中,因此应该一些合理的方法来实现这一点而不设陷阱。。。
我绝对同意"这不是一个罕见的异常值"!然而,这并不一定意味着"应该有一些合理的方法在没有陷阱的情况下实现这一目标"。例如,考虑NP难题。面对这样的异类并不罕见。尽管如此,在多项式时间内求解它们是不可能的(除非P=NP)。在您的情况下,在没有ForwardIterator
s的情况下根本不可能使用std::max_element
。而且,在不消耗O(n²)
内存的情况下也不可能在笛卡尔乘积上实现ForwardIterator
(由标准库定义)。
对于std::max_element
的特殊情况,我建议只实现您自己的版本,该版本返回max元素,而不是指向它的迭代器。
然而,如果我正确理解你的问题,你的担忧更为普遍,std::max_element
只是一个例子。所以,我不得不让你失望。即使使用现有的标准库,由于迭代器类别不兼容,一些琐碎的事情也是不可能的(同样,std::istreambuf_iterator
是一个现有的例子)。所以,如果range-v3恰好被添加,那么就会有更多这样的例子。
因此,最后,我的建议是,如果可能的话,只使用你自己的算法,否则就吞下实现观点的药丸。
迭代器是指向向量中某个元素的指针,在这种情况下,it1指向向量的开头。因此,如果您试图将迭代器指向向量的相同位置,它们将是相同的。但是,可以有多个迭代器指向向量的不同位置。希望这能回答你的问题。
- 转到基于范围的 for 循环中的下一个迭代器
- C++:返回一个基于范围 for 循环迭代器,其中包含继承对象
- 迭代器范围的平衡分区,没有LegacyRandomAccessIterator
- 为什么范围算法与 std 的迭代器不兼容?
- Deque 迭代器超出范围
- unordered_set范围插入与迭代器
- 类型不可知的抽象以使用相同的运行时接口处理正向和反向迭代器和范围?
- C 迭代器范围使用指针到数组
- 将迭代器范围复制到矢量而不重复
- 在 std::vector 中插入迭代器范围
- 有效的迭代器范围从堆栈上的char
- 获取迭代器范围的每个第 n 个元素
- 是否可以将基于范围的for循环与迭代器范围一起使用
- <algorithm>在同一输入迭代器范围内并排运行两个
- std::不带模板的数组迭代器范围
- 迭代器范围矢量构造或矢量擦除更快
- 返回迭代器范围(主列表的子集)
- 为什么标准不允许 std::for_each 对无效的随机访问迭代器范围有明确定义的行为?
- 使用不同类型容器的迭代器范围初始化容器
- 多态过滤器迭代器范围