【问题标题】:C++20: Why can't range adaptors and ranges be combined in one expression?C++20:为什么范围适配器和范围不能组合在一个表达式中?
【发布时间】:2021-07-21 15:01:23
【问题描述】:

我在看下面的sn-p:

std::vector<int> elements{ 1,2,3 };
// this won't compile:
elements
    | std::views::filter([](auto i) { return i % 2 == 0; })
    | std::ranges::for_each([](auto e) {std::cout << e << std::endl; });
// but this compiles:
auto view  = 
elements
    | std::views::filter([](auto i) { return i % 2 == 0; });
std::ranges::for_each(view, [](auto e) {std::cout << e << std::endl; });
 

而且我看不出范围适配器(视图)和通过管道运算符的范围算法的组合无法编译的原因。我想这与for_each 似乎在我的编译器(msvc 19.29)报告时返回的std::ranges::dangling 类型有关,但我不确定......

【问题讨论】:

  • 因为for_each 不适用于范围适配器对象。你可以在它周围做一个包装器。

标签: c++ c++20 std-ranges


【解决方案1】:

为什么不能将范围适配器和范围组合在一个表达式中?

他们可以。你只是把它们结合起来了。您将elements 用作范围,并将其与范围适配器std::views::filter 结合使用。

std::ranges::for_each 既不是范围,也不是范围适配器。它是一个接受范围作为参数的函数(模板)。如果你真的想的话,你可以将它保留在一个表达式中,尽管在我看来是以可读性为代价的:

std::ranges::for_each(
    elements
      | std::views::filter([](auto i) { return i % 2 == 0; }),
    [](auto e) {std::cout << e << std::endl; }
);

【讨论】:

  • 说到可读性:像elements | std::views::filter([](auto i) { return i % 2 == 0; }) | std::ranges::for_each([](auto e) {std::cout &lt;&lt; e &lt;&lt; std::endl; }); 这样的东西难道不是所有版本中可读性最强的吗?或者换一种说法:为什么没有范围适配器可以接受一系列值并产生像打印它们一样的副作用?
【解决方案2】:

实际上并没有任何内在的设计原因为什么某些算法集确实具有管道支持(视图、操作和 to - 尽管 C++20 到目前为止只有视图)而其他算法则没有(其他一切)在&lt;algorithm&gt;)。管道到 for_each 与管道到 filter 一样有意义,只是 range-v3(以及因此,C++20 Ranges)只对某些事情这样做,而不是其他事情。

截至目前,还没有选择加入机制来使某些东西在 Ranges 意义上“可管道化”(请参阅​​P2387 来改变它)。但是一旦那篇论文或它的某个变体获得批准,编写一个算法适配器就变得容易了,它可以简单地使算法可管道化。例如,您可以这样做:

elements
  | std::views::filter([](auto i) { return i % 2 == 0; })
  | adaptor(std::ranges::for_each)([](auto e) {std::cout << e << std::endl; });

Demo(请注意,目前这取决于 libstdc++ 内部)。

库机制解决此问题的方法的一个问题是模棱两可 - 这取决于确定如果f(x, y) 是可调用的,则意图是使其成为完整调用而不是部分调用。从这个意义上说,对于某些算法,您可能无法区分部分调用和完整调用(这本身可能是不让它们隐式可管道化的一个动机)。对于某些视图来说,这肯定是一个已知问题(zipconcatjoin 采用分隔符就是示例),并且它本身就是为这个问题提供语言解决方案的动机之一(例如 P2011) .

当然,这只影响你是否可以写| some_algo(args...),我在这里描述的选择加入机制可以很容易地拼写为| partial(ranges::for_each, args...)而不是| adaptor(ranges::for_each)(args...),然后就没有了模棱两可。在我看来,这只是一个更奇怪的拼写,这就是我以我的方式展示它的原因。

【讨论】:

    【解决方案3】:

    filter 是一个范围适配器:它接受一个范围的值并将其转换为不同的值范围。

    for_each 是一个操作。它采用一系列值并对其进行处理。那个“某事”并没有导致一系列价值的产生。所以它没有适应任何东西。它正在消耗范围。

    范围适配器组合是关于构建范围,而不是消耗它们。范围组合的乘积始终是范围。

    您的问题与想知道为什么可以将std::string 添加到另一个,但不能将std::string 添加到std::ifstream 以打开文件没有什么不同。打开文件不会产生字符串;它产生一个文件。这里也是一样:在一个范围内执行任意操作不会产生一个范围。

    这就是ranges::transform 有一个范围适配器views::transform 版本的原因:因为transform 在概念上会产生一个新的值范围。 for_each 不产生任何东西;它只是消耗一个范围。

    【讨论】:

    • '范围适配器组合是关于构建范围,而不是消耗它们。'但是,理论上,可能有一个只消耗的范围适配器,对吧?这些类型的适配器将始终是组合范围适配器管道中的最后一个。
    • @Angle.Bracket:它不会是“适配器”。是的,您可以使语法在某些方面起作用,但您不会将其称为“适配器”。在 C++ 中,我们通常不会通过 | 运算符执行您认为的处理操作。它们是函数调用。运算符不应该有副作用。
    猜你喜欢
    • 2020-10-08
    • 2021-06-21
    • 1970-01-01
    • 2021-01-14
    • 1970-01-01
    • 2021-02-24
    • 2021-09-21
    • 2020-04-02
    • 1970-01-01
    相关资源
    最近更新 更多