[英]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
要求其参数为LecacyForwardIterators,而ranges::v3::yield
创建的ranges::v3::yield
显然(显然是?)仅提供LecacyInputIterators 。 遗憾的是, range-v3文档没有明确提到可以预期的迭代器类别(至少我没有发现它被提及)。 这确实是一个巨大的增强,因为所有标准库算法都明确说明了它们所需的迭代器类别。
在std::max_element
的特定情况下,您不是第一个ForwardIterator
违反直觉要求的ForwardIterator
而不仅仅是InputIterator
,请参阅为什么std :: max_element需要ForwardIterator? 例如。 总而言之,它确实有意义,因为std::max_element
不会 (尽管名称暗示它)返回max元素,但是返回max元素的迭代器。 因此,特别是InputIterator
上缺少多 InputIterator
保证 ,以使std::max_element
与它std::max_element
工作。
出于这个原因,许多其他标准库函数也不能与std::max_element
使用,例如std :: istreambuf_iterator真的很遗憾:你无法从现有标准库的文件中获取max元素! 您必须先将整个文件加载到内存中,或者必须使用自己的最大算法。
标准库只是缺少一个真正返回max元素而不是指向max元素的迭代器的算法。 这样的算法也适用于InputIterator
。 当然,这可以非常容易地手动实现,但仍然可以方便地使用标准库。 我只能推测它为什么不存在。 也许有一个原因是,它需要value_type
是可复制构造的,因为InputIterator
不需要返回对元素的引用,并且它可能反过来违反最大算法来制作副本......
所以,现在关于你的实际问题:
为什么是这样? (也就是为什么你的范围只返回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
O(n²)
内存 从技术上讲,有可能实现一个专门用于std::max_element
的迭代器,这意味着它只保留内存中的当前max元素,以便可以引用它......但这有点荒谬,不会是吗? 我们不能指望像range-v3这样的通用库能够提供如此高度专业化的迭代器类别。
摘要
你是说
毕竟,我不认为我的用例是如此罕见的异常值,并且计划将范围添加到C ++ 20标准中 - 因此应该有一些合理的方法来实现这一点而不需要陷阱...
我绝对同意“这不是一个罕见的异常值”! 然而,这并不一定意味着“应该有一些合理的方法来实现这一目标,而不是陷阱”。 考虑例如NP难问题。 面对一个并不是一个罕见的异常值。 但是,在多项式时间内解决它们是不可能的(除非P = NP)。 在你的情况下, std::max_element
没有ForwardIterator
就不可能使用std::max_element
。 并且不可能在笛卡尔积上实现ForwardIterator
(由标准库定义)而不消耗O(n²)
内存。
对于std::max_element
的特殊情况,我建议只实现自己的版本,返回max 元素而不是指向它的迭代器。
但是,如果我正确地理解了您的问题,那么您的关注点会更加普遍,而std::max_element
就是一个例子。 所以,我不得不让你失望。 即使使用现有的标准库,由于不兼容的迭代器类别,一些微不足道的事情也是不可能的(同样, std::istreambuf_iterator
是一个现有的例子)。 因此,如果恰好添加了range-v3,那么会有更多这样的例子。
所以,最后,我的建议是尽可能使用自己的算法,然后吞下实现视图的药丸。
迭代器是指向向量中元素的指针,在这种情况下,it1指向向量的开头。 因此,如果您试图将迭代器指向向量的相同位置,它们将是相同的。 但是,您可以使用多个迭代器指向向量的不同位置。 希望这能回答你的问题。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.