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

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

我完全是 Eric Niebler 的 ranges-V3 库(到目前为止我很喜欢它!)的初学者,但是在 returning ranges from 函数时遇到了一些问题。我想我发现了问题,但对这种情况下范围 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::views 的可组合性、懒惰求值以及非拥有性质使得范围看起来像是完成这项任务的完美工具。我想使用 ranges 启用的简洁语法,同时将它们作为非常轻量级的第一个 class 对象在函数之间传递。

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

在我的例子中,范围操作的实际数据是外部所有的,我可以保证它在范围视图的生命周期内持续存在。对于上面的示例,我只是将 my_array 设置为静态,内存范围由函数拥有并且数据在 returns 后持续存在(这可能是有问题的风格,但我相信这对演示)。

要从这个原始 int 数组创建一个范围,它看起来像 span is the tool of choice to easily wrap this bare, contiguous data as an iterator to interface with a range view: it is non-owning and light weight. Since some of the compilers I'm using don't support C++20 yet, I used Martin Moene's span-lite instead of std::span, but had also tested and reproduced the behavior with Tristan Brindle's span 库。

问题: 我不确定这一点,但我相信上面示例的问题是在 ranges::views::all(my_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 值引用以触发移动构造函数并强制视图获得所有权 ranges::views::all(std::move(my_span)),但这似乎没有实现或起作用。

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

None 这些解决方案对我来说似乎特别优雅,它们会增加大量的样板代码和复杂性以在这种情况下使用范围视图(缩短语法并且不必考虑lifetimes 正是我想要实现的目标。

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

span 不是完成任务的正确工具吗?它似乎是为这样的用例创建的?

范围库可能不知道 nonstd::spanview。你需要通过特化 ranges::enable_view 来告诉它。没有它,范围库认为它类似于向量,当您将它的左值传递给 views::all 时,您会返回一个引用本地 span 对象而不是 [= 的副本的视图14=].

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