【问题标题】:C++ range-v3 library: 'take'-ing first 3 perfect numbers works and halts; 'take'-ing first 4 doesn't stop after 4C++ range-v3 库:'take'-ing 前 3 个完美数字工作并停止; 'take'-ing 前 4 不会在 4 之后停止
【发布时间】:2018-11-16 16:26:01
【问题描述】:

据我了解,range-v3 库的视图操作(目前需要 C++17,但要成为 C++20 中 STL 的正式部分)提供了可链接的类似 STL 的算法,这些算法是延迟评估的。作为一个实验,我创建了以下代码来评估前 4 个完美数字:

#include <iostream>
#include <range/v3/all.hpp>

using namespace std;
int main(int argc, char *argv[]) {
    auto perfects = ranges::view::ints(1)
                    | ranges::view::filter([] (int x) {
                        int psum = 0;
                        for (int y = 1; y < x; ++y) {
                            if (x % y == 0) psum += y;
                        }
                        return x == psum;})
                    | ranges::view::take(3);
    std::cout << "PERFECT NUMBERS:" << std::endl;
    for (int z : perfects) {
        std::cout << z << std::endl;
    } 
    std::cout << "DONE." << std::endl;
}

代码以可能无限范围的数字 (ranges::view::ints(1)) 开头,但由于视图算法以 ranges::view::take(3) 结尾,它应该在找到通过过滤算法的前三个数字后停止(用于过滤的蛮力算法出完美的数字,故意不那么有效)。由于前三个完美数字 —— 6、28 和 496 —— 相当小,我希望这段代码能够快速找到它们,打印“DONE”。并终止。这正是发生的事情:

coliru -- taking 3 perfect numbers works just fine

但是,假设我想打印前 4 个完全数字,它们仍然很小——6、28、496 和 8128。打印 8128 后程序没有停止,最终不得不终止;大概是徒劳地试图计算第五个完美数,33550336,这超出了这种蛮力算法的有效查找能力。

coliru -- taking 4 perfect numbers tries to take 5+

这对我来说似乎不一致。如果两个测试都失败了,我会理解的(结论是我误解了 range-v3 视图算法的惰性评估),但是 take(3) 成功并停止而 take(4) 对我来说似乎不是一个错误,除非我理解错了。

我已经在 wandbox 上使用多个编译器进行了尝试,它似乎是持久的(尝试过 clang 6.0.1 和 7.0.0,g++ 8.1.0 和 8.2.0)。至少在我最初发现问题的本地计算机上,使用的是 range-v3 的 0.3.6 版本,但我不确定 coliru 和 wandbox。

wandbox link

【问题讨论】:

  • 只有一条评论可以帮助人们避免徒劳无功的查询:如果你在 lambda 函数体中添加 "std::cout
  • 另一个提示:找到完美的数字也会做一些奇怪的事情。保留 cout 语句,将 lambda 的结果改为 !(x==psum),取前 8124 个非完美数(最后一个是 8127,以弥补不包括 6、28 或 426) ,在 for 循环停止之前,仍会针对 8128 和 8129 评估 lambda 函数。我猜这个视图需要一个结束迭代器,所以它会继续评估 lambda 函数,直到它返回“true”来获得一个结束迭代器。
  • 确认上述内容的简单方法,使用更简单的 lambda 函数,对于 x = 米:wandbox link。 lambda 函数被清楚地调用,直到它返回 true,即使在前 n 次成功之后也是如此。现在的问题是为什么它会这样做。这不是我所期望的。它可能会导致意外行为(例如,如果 lambda 函数体不是纯函数体并更改捕获的对象)并且可能会对性能产生重大影响。
  • 我还在 range-v3 的文档中看到一些 cmets 过滤器已被弃用;但只是想确认 remove_if 的行为方式相同。
  • 请发布有关您问题的所有必需信息,不要链接到外部资源。

标签: c++ c++17 lazy-evaluation range-v3


【解决方案1】:

包含n 元素的镜头视图具有n + 1 有效的迭代器值:n 对应于范围内的元素,以及n + 1st 结束迭代器。它旨在迭代镜头视图必然形成每个n + 1 迭代器 - 实际上,提取由镜头视图的end 迭代器调整的底层迭代器值以执行额外的计算很有用。

take_view 不知道它正在适应的范围是一个过滤器,或者您的过滤器谓词非常昂贵 - 它只是假设您的谓词是 O(1),因为它是提供 O(1) 所必需的迭代器操作。 (Although we did forget to make that complexity requirement explicit in C++20.) 这个案例很好地说明了为什么我们有复杂性要求:如果正在适应的范围的迭代器不满足标准的 O(1) 复杂性要求,则视图无法满足其复杂性保证并且对性能进行推理变得不可能。

【讨论】:

  • 啊,谢谢你的解释!最终迭代器通常允许访问底层迭代器确实有意义,并且这需要观察到的行为。我很高兴我对该功能的滥用可以作为阐明复杂性要求的示例:-)
  • @jwimberley 不客气。感谢让我意识到我们忘记对filter_viewtransform_view使用的函数施加必要的复杂性要求。
  • 不客气!更一般地说,开发人员的复杂性要求可能不是确保对于谓词 P,while (!P(*iter)) { ++iter; } 是 O(1)?当从整数 [1,infinity) 中取 4 或更多时,像 O(1) 谓词 x &lt;= 4 这样的可怕误用将具有与谓词 perfect(x) 基本相同的行为。更现实地说,可能有一个看起来非常合理的谓词,但如果传递谓词的条目不是随机分布在容器中,则增加迭代器的下一个通过谓词的条目可能不是 O(1)。
【解决方案2】:

道歉:

我(部分)回答了我自己的问题,因为我认为我已经从机械上了解了这里发生的事情,并且因为额外的细节不适合发表评论。我不确定礼节,所以如果这会更好地作为问题的编辑 --- 还有一个悬而未决的问题 为什么 图书馆是这样设计的 --- 请建议在 cmets 中,我很乐意将它移到那里。

过滤直到找到结束迭代器

我不太了解 range-v3 的内部细节,所以我可能没有完全正确的术语。简而言之,这里没有不一致的行为。当在调用ranges::view::filter(或ranges::view::remove_if)之后调用ranges::view::take 时,生成的视图对象必须在迭代期间的某个点设置结束迭代器以跳出for 循环。如果我考虑过,我会想象基于范围的 for 循环仍然会扩展为类似

for (auto it = std::begin(perfects); it != std::end(perfects); ++it) {
    ...
}

(顺便说一句,在我的示例中行为相同)并且在它找到所需数量的元素之后,在随后的operator++ 调用it 的开头,会有特殊的逻辑来产生结果等于std::end(perfects),因此循环退出而不做任何额外的工作。但是,从实现的角度来看,这是有道理的,结束迭代器实际上对应于 filter/remove_if 视图返回的下一个元素。 filter 谓词继续循环遍历ranges::view::ints(1),直到找到谓词返回true 的一个;大概这将成为结束迭代器,因为它没有在范围 for 循环中打印。

以下代码提供了一个简单的演示。这里有两个可配置整数nmfilter中的谓词函数对x &lt;= n返回true,falsen &lt; x &lt; n+m返回true,truex &gt;= m返回:

#include <iostream>
#include <range/v3/all.hpp>

using namespace std;
int main(int,char**) {
    int n = 5;
    int m = 3;
    auto perfects = ranges::view::ints(1)
                    | ranges::view::filter([&n,&m] (int x) {
                        std::cout << "Checking " << x << "... ";
                        if (x <= n) {
                            return true;
                        } else if (x <= n + m) {
                            std::cout << std::endl;
                            return false;
                        }
                        return true;})
                    | ranges::view::take(n);
    std::cout << "First " << n << " numbers:" << std::endl;
    for (int z : perfects) {
        std::cout << " take it!" << std::endl;
    }
    std::cout << "DONE." << std::endl;
}

您可以在此处为nm 的不同值运行此代码:wandbox。默认输出如下:

First 5 numbers:
Checking 1...  take it!
Checking 2...  take it!
Checking 3...  take it!
Checking 4...  take it!
Checking 5...  take it!
Checking 6... 
Checking 7... 
Checking 8... 
Checking 9... DONE.

(我没有重命名变量perfects;显然它不再是一组完美数字了)。即使在取得第一个n 成功之后,也会调用lambda 谓词,直到它返回true。由于返回 true 的整数 9 没有被打印出来,它必须是 std::end(perfects) 打破了范围 for 循环。

对我来说剩下的谜团是它为什么这样做。这不是我所期望的。它可能导致意外的行为(例如,如果 lambda 函数体不是纯的并改变捕获的对象)并且它可能会产生很大的性能影响,如原始示例所示,它必须执行大约 10^15 模运算才能达到整数 33550336。

【讨论】:

  • 不要为自己想出答案而道歉。我很高兴你这样做了,我相信其他人也会这样做。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-05-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多