从函数返回范围视图时,带有std::span:中间对象所有权的C++Ranges-v3

C++ Ranges-v3 with std::span: ownership of intermediate objects when returning range views from functions

本文关键字:中间 span 对象 所有权 C++Ranges-v3 std 带有 返回 函数 范围 视图      更新时间:2023-10-16

我是Eric Niebler的ranges-V3库的初学者(到目前为止我很喜欢它!),但在从函数返回范围时遇到了一些问题。我想我发现了这个问题,但在这种情况下API范围的默认行为让我有点惊讶。由于我在其他地方没有找到任何关于这个问题的参考,而且这花了我相当多的时间,我把我的问题写得比较广泛,希望这对未来的其他人有帮助。

这个问题出现在下面的最小示例中,它导致了未定义的行为。

#include <iostream>
#include "range/v3/all.hpp"
#include "nonstd_span.h"
auto from_span() {  
// make this static for the array to persist after the fct returns
static int my_array[10] = { 1,2,3,4,5,6,7,8,9,10 };
auto my_span = nonstd::span<int>(my_array, 10); 
return ranges::views::all(my_span);
}
int main() {
std::cout << from_span() << std::endl;
return 0;
}

我试图实现的目标:我的程序中有一些持久(和恒定)的连续数据,我试图通过范围对其进行操作。可组合性、懒惰的评估,加上ranges::视图的非拥有性,使ranges看起来是完成任务的完美工具。我想使用range所支持的简洁语法,并将它们作为非常轻的第一类对象在函数之间传递。

在大多数演示范围的代码示例中,范围操作的对象都是在与范围本身相同的范围内创建的,因此,一旦范围完成评估,它们就会一起被销毁,一切都很好。

在我的情况下,范围操作的实际数据是外部所有的,我可以保证它在范围视图的生命周期内持续存在。对于上面的例子,我只是简单地将my_array设为静态,即内存范围归函数所有,一旦返回,数据就会保持不变(这可能是一种有问题的风格,但我相信这对演示来说没有错)。

要从这个原始int数组创建一个范围,span似乎是一个选择的工具,可以轻松地将这个裸露的、连续的数据包装为迭代器,以与范围视图接口:它是非拥有的,重量很轻。由于我使用的一些编译器还不支持C++20,所以我使用了Martin Moene的span lite而不是std::span,但也使用Tristan Brindle的span库测试并复制了这种行为。

问题:我对此不确定,但我认为上面示例的问题是,在ranges::views::all(my_span)中,范围视图对象不拥有span对象的所有权。尽管当在main函数中调用范围时,底层数据(int数组)仍然存在,但当函数退出时,my_span对象将被析构函数(我可以看到在评估视图之前调用了span析构函数)。在平台和各种编译器上,我用(g++7.4.0,Clang 6.0.0,MSVC 16.5.5)测试了这一点,代码通常似乎是有效的,但这只是因为当在main中触发范围视图评估时,以前的my_span对象的位仍然挂在内存中,并且没有被覆盖。

行为/API我本希望由于span应该非常轻,并且ranges::views被设计为非拥有数据的视图,我本希望ranges::views::all(my_span)创建的视图复制span对象并拥有其副本的所有权。这将允许用户在编写视图时不考虑所有中间对象的寿命,并在函数和范围之间传递它们,只要底层数据持续存在(也许我作为一个对范围的天真新手的期望在这里是有缺陷的?)。此外,当从其他视图组成新视图时,是否需要担心保持较低级别的视图处于活动状态,以防它们超出范围,而新组成的视图没有?

我尝试强制转换为r值引用来触发move构造函数并强制视图获得ranges::views::all(std::move(my_span))的所有权,但这似乎没有实现或工作。

我尝试过的其他一些解决方案:

  • 在外部范围内拥有my_span,并通过引用将其传递到from_span中。这是有效的
  • 从函数返回my_span和范围,例如通过std::unique_ptr澄清所有权并防止返回时复制

    auto from_span() {
    using namespace ranges;
    static int my_array[10] = { 1,2,3,4,5,6,7,8,9,10 }; 
    auto span_ptr = std::make_unique<nonstd::span<int>>(my_array, 10);  
    return std::make_tuple(views::all(*span_ptr), std::move(span_ptr));
    }
    
    int main() {
    auto [rng, my_span_ptr] = from_span();  
    std::cout << rng << std::endl;
    return 0;
    }
    
  • 还可以为跨度构建一个小型内存/寿命管理系统,这些系统由外部拥有。

这些解决方案对我来说都不是特别优雅,它们会给在这种情况下使用范围视图添加大量的样板代码和复杂性(缩短语法和不必考虑寿命正是我试图实现的目标)。

我觉得我可能遗漏了一些东西,应该有一个更优雅的解决方案,范围视图拥有/复制它所组成的轻量级对象(如跨度或其他视图)。

span不是执行任务的合适工具吗?它似乎是为像这样的用例创建的?

范围库可能不知道nonstd::spanview。你需要通过专门的ranges::enable_view来告诉它。如果没有这一点,范围库会认为它有点像向量,当你将其左值传递给views::all时,你会得到一个引用本地span对象的视图,而不是span的副本。

在最近的一段时间里,range-v3会使用启发式方法来猜测(正确地)span是一个视图,而您的代码就会起作用。它根据C++委员会的请求进行了更改,委员会不喜欢这种启发式方法。公平地说,它有时会猜错。