复杂范围的多个迭代器

Multiple iterators to a complex range

本文关键字:迭代器 范围 复杂      更新时间:2023-10-16

我正在尝试让多个迭代器到达更复杂的范围(使用range-v3库)——使用filterfor_eachyield手动实现笛卡尔乘积。然而,当我试图将多个迭代器保持在这样的范围内时,它们共享一个共同的值。例如:

#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的情况下,这意味着您必须实现所有这些元素。

我应该以不同的方式实现笛卡尔乘积吗

我可以想象许多不同的实现。但它们中没有一个能够同时提供这两种特性:

  • 返回ForwardIterators
  • 需要少于O(n²)的内存

从技术上讲,可以实现一个专门用于std::max_element的迭代器,这意味着它只在内存中保留当前的最大元素,以便可以引用它。。。但这会有点可笑,不是吗?我们不能指望像range-v3这样的通用库能够产生这样高度专业化的迭代器类别。


摘要

你说的是

毕竟,我不认为我的用例是如此罕见的异常值和范围计划添加到C++20标准中,因此应该一些合理的方法来实现这一点而不设陷阱。。。

我绝对同意"这不是一个罕见的异常值"!然而,这并不一定意味着"应该有一些合理的方法在没有陷阱的情况下实现这一目标"。例如,考虑NP难题。面对这样的异类并不罕见。尽管如此,在多项式时间内求解它们是不可能的(除非P=NP)。在您的情况下,在没有ForwardIterators的情况下根本不可能使用std::max_element。而且,在不消耗O(n²)内存的情况下也不可能在笛卡尔乘积上实现ForwardIterator(由标准库定义)。

对于std::max_element的特殊情况,我建议只实现您自己的版本,该版本返回max元素,而不是指向它的迭代器。

然而,如果我正确理解你的问题,你的担忧更为普遍,std::max_element只是一个例子。所以,我不得不让你失望。即使使用现有的标准库,由于迭代器类别不兼容,一些琐碎的事情也是不可能的(同样,std::istreambuf_iterator是一个现有的例子)。所以,如果range-v3恰好被添加,那么就会有更多这样的例子。

因此,最后,我的建议是,如果可能的话,只使用你自己的算法,否则就吞下实现观点的药丸。

迭代器是指向向量中某个元素的指针,在这种情况下,it1指向向量的开头。因此,如果您试图将迭代器指向向量的相同位置,它们将是相同的。但是,可以有多个迭代器指向向量的不同位置。希望这能回答你的问题。