从函数返回范围视图时,带有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库的初学者(到目前为止我很喜欢它!),但在从函数返回范围时遇到了一些问题。我想我发现了这个问题,但在这种情况下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::span
是view
。你需要通过专门的ranges::enable_view
来告诉它。如果没有这一点,范围库会认为它有点像向量,当你将其左值传递给views::all
时,你会得到一个引用本地span
对象的视图,而不是span
的副本。
在最近的一段时间里,range-v3会使用启发式方法来猜测(正确地)span
是一个视图,而您的代码就会起作用。它根据C++委员会的请求进行了更改,委员会不喜欢这种启发式方法。公平地说,它有时会猜错。
- 2D数组来自文本输入,中间有空格
- 为什么 std::span 缺少 cbegin 和 cend 方法?
- std::span<const T> 作为函数模板中的参数
- 如何将唯一指针的 std::vector 转换为原始指针的 std::span?
- 递归形成字符串中所有数字字符的中间和?
- 链表错误的中间
- 使(虚拟)函数在大多数派生类中无法访问中间基类中可访问,定义良好?
- 从函数返回范围视图时,带有std::span:中间对象所有权的C++Ranges-v3
- 如何输入数组的元素,每次获得新元素时,我们将其放在数组的中间?
- 查找中间两个数字的正则表达式的匹配项
- std::span constructor, libcxx vs libstdc++, template vs non-
- 我可以擦除 std::queue 中间的节点吗?
- 如何在标准c ++中流式传输/读取二进制文件的中间部分并写入另一个文件?
- 重塑Microsoft的并发::d iagnostic::span,也可以检测外部跨度
- 如何在 FOR 语句中间阻止 Visual Studio 2017 c++ 自动完成)?
- 给定类型的模板化中间数组
- span可以是constexpr吗
- 如何在文本文件中间插入字符
- 如何组合一个宽字符字符串,中间插入一些空字符
- C++:我可以重用/移动 std::list 元素从中间到结尾吗?