有没有办法确定范围是否正确构建?

Mar*_*rkB 7 c++ boost c++20 std-ranges

boost::join在混合 的输出时,我遇到了意外的行为std::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;
}
Run Code Online (Sandbox Code Playgroud)

PS 我之所以费心这样做的唯一原因是因为我的一些团队成员受到 Intellisense 的影响而无法在 IDE 上正常工作,这最终是由https://github.com/llvm/llvm-project/issues造成的/44178 . (叹)

Bar*_*rry 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); }
Run Code Online (Sandbox Code Playgroud)

即使 boost 连接适配器超出了范围,因为 boost 转换适配器从 中获取迭代器rng,而这就是所有信息所在的地方。此结果中没有rng任何内容涉及构建后。

但在 range-v3/C++20 范围内,模型有很大不同。我们不“捕获”迭代器,我们捕获范围 - 我们有两种范围:a view(始终通过值捕获)和非view range(如果它们是左值,则通过引用捕获,并且在 C+ 中+20,如果它们是右值则捕获owning_view)。这句话的意思是:

lvalue | std::views::meow
Run Code Online (Sandbox Code Playgroud)

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

在这种情况下:

auto get1b() const { auto rng = join1(); return rng | std::views::transform(square); }
Run Code Online (Sandbox Code Playgroud)

我们的左值是 a view,因此它被复制到结果中transform_view,因此这里没有悬空。这可以。

在这种情况下:

auto get2a() const { return join2() | std::views::transform(square); }
Run Code Online (Sandbox Code Playgroud)

我们的右值总是由值捕获,因此这里也没有悬空(右值视图只是按原样捕获,右值非视图首先包装在 中owning_view,但生命周期的结果是相同的)。

这个案例有问题的原因是:

auto get2b() const { auto rng = join2(); return rng | std::views::transform(square); }
Run Code Online (Sandbox Code Playgroud)

是因为rng它是一个 boost 适应连接范围,它不会将自己宣传为 C++20 view。因此,因为它是左值,所以它是通过引用捕获的。然后它被破坏},因此我们有一个悬空引用。这可以通过修复来解决std::move(rng),这会导致rngtransform_view- 我们将不再保留对它的引用。

Boost.Ranges 可能应该尝试将其所有适应范围标记为views - 它们绝对符合语义标准,因为它们都只是迭代器对。如果他们这样做了,那么上面的内容就会被复制 rng而不是引用它,并且会工作得很好。

但在此之前,最好不要使用 Boost.Ranges。其中有一些适配器在 C++20 或 C++23 中不存在,但在此之前您可以使用 range-v3,或者只是重新实现它们。最好将范围保持在一种模型内。