【问题标题】:Multiple iterators to a complex range复杂范围的多个迭代器
【发布时间】:2019-01-04 22:40:18
【问题描述】:

我正在尝试将多个迭代器用于更复杂的范围(使用 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 的东西 - 尝试将迭代器返回到叉积中的最高值对。在寻找最高值时,您需要将迭代器存储到当前最佳候选者。它在您搜索时无法更改,如果您需要范围的副本(如其中一个答案中所建议),则管理迭代器会很麻烦。

实体化整个叉积也不是一种选择,因为它需要大量内存。毕竟,使用具有过滤器和其他即时转换的范围的全部目的是避免这种实现。

【问题讨论】:

  • range 是一个代理对象,主要被认为是在遍历时进行延迟评估。如果您想多次遍历范围,最好先将其转移到容器中。
  • @metalfox 我不介意在必要时多次重新评估东西,但是将交叉产品具体化到容器中 - 这将不必要地占用大量内存。

标签: c++ range-v3


【解决方案1】:

结果视图似乎存储状态,结果证明它是单次传递。您可以通过简单地根据需要制作尽可能多的视图副本来解决此问题:

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;
}

另一种选择是将视图具体化为 cmets 中提到的容器。


记住前面提到的单遍视图的限制,实现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; 
}

【讨论】:

  • 嗯,它适用于我显示初始问题的最小工作示例,但假设您想在该范围内实现 maximum 函数,将 it2 返回到找到的最大值,同时使用it1。在一定条件下(找到更高的值)it2需要设置为it1的当前值。我认为将其设置为“与it1 相同但在副本范围内的元素”会很麻烦吗?例如,我可以记住我需要前进多远。但那时我在问自己,如果使用这种范围很奇怪,为什么还要使用它们?!
  • @CygnusX1 请注意,您正在动态(懒惰地)组合data1data2 的元素的所有可能组合。这是非常强大的。 decltype(range)::iterator 成为 InputIterator 在我看来是一个合理的代价。一些视图将输入范围的iterator_category 降级。例如,view::filter 最多可以是 BidirectionalRange,即使输入范围提供随机访问。在您的示例中,您正在创建临时对 (v,v2)。对我来说,range 是单通道是很合乎逻辑的。
  • @CygnusX1 你真的需要返回一个迭代器吗?你不能只返回它包装的 value 吗?
  • std::max_element 对于常规容器返回一个迭代器。我正在尝试在这里实现类似的东西。叉积在我看来是一个微不足道的操作,我真的不认为它比它组合的迭代器类型更小。但我是一个新手 range-v3 用户,上述行为 - 每个范围一个迭代器 - 似乎完全违反直觉和意外。我花了一些时间来调试它并找出库欺骗了我导致上面的问题。
  • @CygnusX1 我同意缓存begin 可能是违反直觉的。我在答案中添加了一些我发现的基本原理。
【解决方案2】:

这是怎么回事?

这里的整个问题源于std::max_element 要求其参数为LecacyForwardIteratorsranges::v3::yield 创建的范围显然(显然?)只提供LecacyInputIterators。不幸的是,range-v3 docs 没有明确提及人们可以期待的迭代器类别(至少我没有发现它被提及)。这确实是一个巨大的改进,因为所有标准库算法都明确说明了它们需要哪些迭代器类别。

std::max_element 的特殊情况下,您不是第一个发现ForwardIterator 而不仅仅是InputIterator 的违反直觉要求的人,例如,请参阅Why does std::max_element require a ForwardIterator?。总而言之,它确实有意义,因为std::max_element (尽管名称暗示了它)返回最大元素,而是返回最大元素的迭代器。因此,为了使 std::max_element 与它一起工作,InputIterator 上特别缺少 multipass guarantee

因此,许多其他标准库函数也不适用于std::max_element,例如std::istreambuf_iterator 真的很遗憾:您无法从现有标准库的文件中获取最大元素!您要么必须先将整个文件加载到内存中,要么必须使用自己的最大算法。

标准库只是缺少一个真正返回最大元素的算法,而不是一个指向最大元素的迭代器。这样的算法也可以与InputIterators 一起使用。当然,这可以很容易地手动实现,但是由标准库提供它仍然很方便。我只能推测为什么它不存在。也许一个原因是,它需要value_type 是可复制构造的,因为InputIterator 不需要返回对元素的引用,而对于最大算法进行复制可能又违反直觉......


所以,现在关于您的实际问题:

这是为什么?(即为什么你的范围只返回InputIterators?)

显然,yield 会即时创建值。这是设计使然,这正是人们想要使用 yield 的原因:不必预先创建(并因此存储)范围。因此,我看不出如何以满足multipass guarantee 的方式实现 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-hard 问题。面对一个异常值并不罕见。尽管如此,在多项式时间内解决它们是不可能的(除非 P=NP)。在您的情况下,如果没有ForwardIterators,根本不可能使用std::max_element。并且不可能在不消耗O(n²) 内存的情况下在笛卡尔积上实现ForwardIterator(由标准库定义)。

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

但是,如果我正确理解了您的问题,您的担忧就更笼统了,std::max_element 只是一个例子。所以,我不得不让你失望。即使使用现有的标准库,由于迭代器类别不兼容,一些琐碎的事情也是不可能的(同样,std::istreambuf_iterator 是一个现有的示例)。所以,如果碰巧添加了 range-v3,就会有更多这样的例子。

所以,最后,我的建议是尽可能使用您自己的算法,否则就吞下实体化视图的药丸。

【讨论】:

  • 非常感谢您的详细回答。这让我看到了我不知道的迭代器的某些方面。我现在可以看到您在多次通过保证中引用的项目符号是有问题的。但是,如果我们将其替换为稍弱的东西,则叉积可以实现为一对ForwardIterator-s,并且引用它将返回一个对象,该对象包含两个对每个组件引用的引用。我认为它可以与std::max_element 和其他功能一起使用。限制因素似乎是形式化,而不是一般算法无法做到这一点。
【解决方案3】:

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

【讨论】:

  • 但是我增加了it2,而我没有增加it1。然而,经过多次迭代,it1 仍然指向与it2 相同的位置。
猜你喜欢
  • 2014-09-08
  • 2016-07-19
  • 2015-10-04
  • 1970-01-01
  • 2013-07-08
  • 1970-01-01
  • 2021-09-24
  • 2012-11-27
  • 2014-10-14
相关资源
最近更新 更多