【问题标题】:Can I return a temporary piped to a range operation?我可以将临时管道返回到范围操作吗?
【发布时间】:2020-03-19 10:00:08
【问题描述】:

假设我有一个模拟rangegenerate_my_range 类(特别是regular)。那么下面的代码是否正确:

auto generate_my_range(int some_param) {    
  auto my_transform_op = [](const auto& x){ return do_sth(x); };
  return my_custom_rng_gen(some_param) | ranges::views::transform(my_transform_op);
}
auto cells = generate_my_range(10) | ranges::to<std::vector>;

my_custom_rng_gen(some_param) 是由(第一个)管道运算符按值获取的,还是在我离开 generate_my_range 范围后有一个悬空引用?

函数调用ranges::views::transform(my_custom_rng_gen(some_param),my_transform_op)会不会一样?

如果我使用左值引用会正确吗?例如:

auto generate_my_range(int some_param) {
  auto my_transform_op = [](const auto& x){ return do_sth(x); };
  auto tmp_ref = my_custom_rng_gen(some_param);
  return tmp_ref | ranges::views::transform(my_transform_op);
}

如果这些操作的值取值范围,那么如果我将左值引用传递给容器,我该怎么办?我应该使用ranges::views::all(my_container) 模式吗?

【问题讨论】:

  • my_custom_rng_gen(some_param) 是否已经有界?你的意思是像 godbolt.org/z/aTF8RN 这样没有 take(5) 吗?
  • @Porsche9II 是的,这是一个有界范围。假设它是一个容器

标签: c++ range-v3


【解决方案1】:

范围库中有两种操作:

  • views 是惰性的,需要底层容器存在。
  • 动作 急切的,并因此产生新的容器(或修改现有的容器)

视图是轻量级的。您通过值传递它们并要求底层容器保持有效且不变。

来自ranges-v3 documentation

视图是一个轻量级的包装器,它呈现底层视图的视图 以某种自定义方式的元素序列,无需变异或复制 它。视图的创建和复制成本低廉,并且具有非拥有引用 语义。

和:

对底层范围进行任何使其迭代器或哨兵无效的操作也会使引用该范围任何部分的任何视图无效。

底层容器的销毁显然会使它的所有迭代器失效。

在您的代码中,您专门使用 views -- 您使用 ranges::views::transform。管道只是一种语法糖,可以很容易地按照它的方式编写。您应该查看管道中的最后一件事以查看您生成的内容 - 在您的情况下,它是一个视图。

如果没有管道运算符,它可能看起来像这样:

ranges::views::transform(my_custom_rng_gen(some_param), my_transform_op)

如果有多个转换以这种方式连接,你会看到它会变得多么丑陋。

因此,如果my_custom_rng_gen 生成某种容器,您转换然后返回该容器,则该容器将被销毁,并且您的视图中有悬空引用。如果my_custom_rng_gen 是位于这些范围之外的容器的另一个视图,那么一切都很好。

但是,编译器应该能够识别出您正在对临时容器应用视图并提示您编译错误。

如果您希望函数将范围作为容器返回,则需要显式“实现”结果。为此,请在函数中使用 ranges::to 运算符。


更新:为了更明确地说明您的评论“文档在哪里说组成范围/管道需要并存储视图?”

Pipe 只是一种语法糖,以易于阅读的表达式连接事物。根据它的使用方式,它可能会或可能不会返回视图。这取决于右手边的论点。你的情况是:

`<some range> | ranges::views::transform(...)`

所以表达式返回views::transform 返回的任何内容。

现在,通过阅读转换的文档:

下面是 Range-v3 提供的惰性范围组合器或视图的列表,以及关于如何使用每个组合器的简介。

[...]

views::transform

给定一个源范围和一元函数,返回一个新范围,其中每个结果元素都是将一元函数应用于源元素的结果。

所以它返回一个范围,但由于它是一个惰性运算符,所以它返回的范围一个视图,具有它的所有语义。

【讨论】:

  • 好的。对我来说有点神秘的是,当我将容器传递给管道(即组合创建的范围对象)时,它是如何工作的。它需要以某种方式存储容器的视图。用ranges::views::all(my_container) 完成了吗?如果将视图传递给管道怎么办?它是否认识到它传递了一个容器或一个视图?需要吗?怎么样?
  • “编译器应该能够识别出你在一个临时容器上应用了一个视图,然后给你一个编译错误”我也是这么想的:如果我做了一些愚蠢的事情,那就意味着类型(作为左值)的合同未履行。类似的事情是由 range-v3 完成的。但在这种情况下,绝对没有问题。它编译并运行。所以可能有未定义的行为,但它没有显示出来。
  • 为了确定您的代码是否意外运行正确或一切正常,我需要查看my_custom_rng_gen 的内容。管道和transform 在引擎盖下究竟如何交互并不重要。整个表达式将一个范围作为参数(容器或某个容器的视图)并返回该容器的不同视图。返回值永远不会拥有容器,因为它是一个视图。
【解决方案2】:

取自ranges-v3 documentation

视图 [...] 具有非拥有引用语义。

拥有单个范围对象允许操作管道。在流水线中,一个范围以某种方式被懒惰地适应或急切地变异,结果立即可用于进一步的适应或变异。惰性适应由视图处理,急切变化由动作处理。

// taken directly from the the ranges documentation
std::vector<int> const vi{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
using namespace ranges;
auto rng = vi | views::remove_if([](int i){ return i % 2 == 1; })
              | views::transform([](int i){ return std::to_string(i); });
// rng == {"2","4","6","8","10"};

在上面的代码中,rng 只是存储了对底层数据以及过滤器和转换函数的引用。在迭代 rng 之前不会完成任何工作。

既然你说临时范围可以被认为是一个容器,你的函数返回一个悬空引用。

换句话说,你需要确保底层范围比视图长,否则你就有麻烦了。

【讨论】:

  • 是的,视图是非拥有的,但是文档在哪里说组成范围/管道需要并存储视图?有可能(我认为,一件好事)有以下策略:如果范围由右值引用给出,则按值存储。
  • @Bérenger 我从范围文档中添加了更多内容。但重点是:视图是非拥有的。它不关心你是否给它一个右值。
猜你喜欢
  • 2018-11-07
  • 2017-04-10
  • 2021-01-30
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-05-22
相关资源
最近更新 更多