有没有一种方法可以确定一个范围是否被正确构建?

7

当我混合使用boost::joinstd::views::transform输出时(如下所示),我遇到了意外的行为。编译器没有发出任何警告。幸运的是,地址检查器在get2b()中检测到了无效的内存访问。

get1b()函数遵循与get2b()相同的模式,但该代码可以正常工作。考虑到 UB 的可能性,我怎样才能确定构建的范围是合法的?我的偏执心希望将get1b()写成return std::move(rng) | ...

https://www.godbolt.org/z/Y77YW3jYb

#include <array>
#include <ranges>
#include <algorithm>
#include <iostream>
#include <iterator>
#include "boost/range/join.hpp"
#include "boost/range/adaptor/transformed.hpp"

inline auto square = [](int x) { return x*x; };

struct A {
    std::array<std::vector<int>, 3> m_data{ std::vector{1, 2}, std::vector{3, 4, 5}, std::vector{6} };
    auto join1() const { return m_data | std::views::join; }
    auto join2() const { return boost::join(m_data[0], boost::join(m_data[1], m_data[2])); }
    auto get1a() const { return join1() | std::views::transform(square); }
    auto get1b() const { auto rng = join1(); return rng | std::views::transform(square); }
#if __GNUC__ >= 12
    auto get2a() const { return join2() | std::views::transform(square); }
#endif
    auto get2b() const { auto rng = join2(); return rng | std::views::transform(square); }
    auto get2c() const { auto rng = join2(); return rng | boost::adaptors::transformed(square); }
};

template<std::ranges::range R>
void print(R&& r)
{
    std::ranges::copy(r, std::ostream_iterator<int>(std::cout, " "));
    std::cout << '\n';
}

int main()
{
    print(A{}.get1a());
    print(A{}.get1b());
#if __GNUC__ >= 12
    print(A{}.get2a());
#endif
    // print(A{}.get2b()); <-- undefined behavior
    print(A{}.get2c());
    return 0;
}

附注:我这么做的唯一原因是因为我的团队成员受到了Intellisense无法在IDE上正常工作的影响,最终是由https://github.com/llvm/llvm-project/issues/44178引起的。 (叹气)


“我的多疑一面想要将get1b()写成return std::move(rng) | ....的形式。” 是的,对于get2b(),你需要这样做。 - 康桓瑋
@康桓瑋,这只是像 get1a() 一样。一个结果是 std::ranges::owning_view,另一个结果是 std::ranges::ref_view。我理解为什么 owning_view 正确工作。然而,原始问题仍然存在。 - MarkB
1个回答

3
混合使用Boost.Ranges和C++20 Ranges/range-v3可能会导致问题 - 这里的模型基本上是根本不同的。
在Boost.Ranges中,所有内容都在迭代器中。 r | adaptors::transformed(f) 的作用是创建一个新对象,该对象基本上包含两个迭代器:从begin(r)生成的转换迭代器和从end(r)生成的转换迭代器。无论范围类型如何,它都会这样做。由于所有信息都在迭代器中,迭代器变得任意大且昂贵,但您不必担心中间范围保持在作用域内。按照C++20的说法,r | adaptors::transformed(f) 基本上创建了ranges::subrange(transform_iterator(ranges::begin(r), f), transform_iterator(ranges::end(r), f))
所以这个可以工作:
auto get2c() const { auto rng = join2(); return rng | boost::adaptors::transformed(square); }

尽管boost join适配器超出作用域,因为boost transformed适配器获取了来自rng的迭代器,这也是所有信息所在的地方。在构建后,结果中没有任何内容与rng相关。
但在range-v3/C++20范围内,模型完全不同。我们不“捕获”迭代器,而是捕获范围 - 我们有两种类型的范围:一个view(总是按值捕获)和一个非viewrange(如果它们是左值,则按引用捕获;在C++20中,如果它们是右值,则按owning_view捕获)。这意味着表达式:
lvalue | std::views::meow

可能会通过引用捕获lvalue,如果lvalue不是一个view,因此这里可能存在生命周期依赖。

在这种情况下:

auto get1b() const { auto rng = join1(); return rng | std::views::transform(square); }

我们的lvalue是一个视图,所以它被复制到结果的transform_view中,因此这里没有悬挂的问题。这很好。

在这种情况下:

auto get2a() const { return join2() | std::views::transform(square); }

我们的rvalue始终被按值捕获,因此这里也不存在悬空指针问题(rvalue视图只是按原样捕获,rvalue非视图首先被包装在owning_view中,但从生命周期的角度来看结果是相同的)。 这种情况存在问题的原因:
auto get2b() const { auto rng = join2(); return rng | std::views::transform(square); }

由于rng是一个boost适配的联合范围,它没有将自己作为C++20view进行广告宣传。因此,因为它是左值,它被通过引用进行捕获。然后在}处被销毁,因此我们有了悬空引用。这可以通过使用std::move(rng)来修复,从而导致rngtransform_view拥有 - 我们不再保留对它的引用。
Boost.Ranges应该尝试将其所有适配范围标记为view - 它们肯定符合语义标准,因为它们只是迭代器对。如果他们这样做了,那么上面的代码将会复制rng而不是引用它,就可以正常工作。
但是,在这样做之前,最好不要使用Boost.Ranges。其中一些适配器在C++20或C++23中不存在,但在此之前,您可以使用range-v3,或者简单地重新实现它们。最好在一个ranges模型内保持一致。

1
“[一种类型] 宣传自己是 C++20 的 view” 是否等同于 “符合 std::ranges::view 概念”? - MarkB
1
@MarkB 是的。因为view是选择加入的,所以它“自己做广告”。 - Barry

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接