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 . (叹)
混合和匹配 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)
,这会导致rng
由transform_view
- 我们将不再保留对它的引用。
Boost.Ranges 可能应该尝试将其所有适应范围标记为view
s - 它们绝对符合语义标准,因为它们都只是迭代器对。如果他们这样做了,那么上面的内容就会被复制 rng
而不是引用它,并且会工作得很好。
但在此之前,最好不要使用 Boost.Ranges。其中有一些适配器在 C++20 或 C++23 中不存在,但在此之前您可以使用 range-v3,或者只是重新实现它们。最好将范围保持在一种模型内。