首页 > 解决方案 > 带有 std::span 的 C++ Ranges-v3:从函数返回范围视图时的中间对象的所有权

问题描述

我是 Eric Niebler 的 range-V3 库(到目前为止我很喜欢!)的初学者,但是在从函数返回 range 时遇到了一些问题。我想我找到了问题所在,但在这种情况下对范围 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;
}

我想要实现的目标: 我的程序中有一些持久(且恒定)的连续数据,我试图通过范围对其进行操作。range::views 的可组合性、惰性求值以及非拥有性质使 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)测试了这个代码通常似乎可以工作,但这只是因为以前的my_span对象的位仍然存在并且没有当范围视图评估在main.

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

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

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

这些解决方案对我来说似乎都不是特别优雅,它们会为在这种情况下使用范围视图添加大量样板代码和复杂性(缩短语法并且不必考虑生命周期正是我想要做的事情达到)。

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

span不是任务的正确工具?似乎它是为诸如此类的用例而创建的?

标签: c++range-v3

解决方案


有范围库可能不知道那nonstd::span是一个view. 你需要通过专业来告诉它ranges::enable_view。没有它,范围库认为它就像一个向量,当你将它的左值传递给views::all你时,你会得到一个引用本地span对象而不是span.

在最近的过去,range-v3 会使用启发式方法来猜测(正确地)这span是一个视图,并且您的代码会正常工作。它根据 C++ 委员会的要求进行了更改,该委员会不喜欢这种启发式方法。公平地说,它有时会猜错。


推荐阅读