当我决定修改提供的代码示例以查看它是否适用于 std::list
时,我正在阅读 Rainer Grimm 的书
C++20:获取有关定义自己的视图类型的详细信息的第 5.1.7.2 节。代码最终如下所示:
#include <concepts>
#include <iostream>
#include <list>
#include <ranges>
#include <vector>
template <std::ranges::input_range Range>
requires std::ranges::view<Range>
class ContainerView : public std::ranges::view_interface<ContainerView<Range>> {
private:
std::ranges::iterator_t<Range> begin_ {};
std::ranges::sentinel_t<Range> end_ {};
Range range_ {};
public:
constexpr ContainerView() = default;
constexpr ContainerView(Range r)
: begin_(std::begin(r))
, end_(std::end(r))
, range_(std::move(r))
{
}
constexpr auto begin() const { return begin_; }
constexpr auto end() const { return end_; }
};
int main()
{
std::list my_list { 1, 2, 3 };
auto my_container_view { ContainerView(std::views::all(my_list)) };
for (auto const& c : my_container_view)
std::cout << c << '\n';
}
该程序使用 x86-64 clang++ v17.0.1 和标志
-std=c++20 -fsanitize=undefined -fsanitize=address -Wall -Wextra -Werror
成功编译,并且在运行时,它会打印数字 1、2 和 3,并按预期正常退出。
但是,出于好奇,当我更改
main
函数以将 my_list
包装在对 std::move
的调用中并将其传递给 std::views::all
时,程序似乎陷入了无限递归,重复打印以下元素名单。
int main()
{
std::list my_list { 1, 2, 3 };
auto my_container_view { ContainerView(std::views::all(std::move(my_list))) };
for (auto const& c : my_container_view)
std::cout << c << '\n';
}
我发现奇怪的是,当我简单地将
std::list
替换为 std::vector
并保持其余代码不变时,程序会打印每个元素一次并优雅退出:
int main()
{
std::vector my_vec { 1, 2, 3 };
auto my_container_view { ContainerView(std::views::all(std::move(my_vec))) };
for (auto const& c : my_container_view)
std::cout << c << '\n';
}
当我删除
ContainerView
类型的间接并直接迭代 std::list
时,我也没有得到异常行为:
int main()
{
std::list my_list { 1, 2, 3 };
for (auto const& c : std::move(my_list))
std::cout << c << '\n';
}
鉴于代码的每个变体都会编译并不会产生编译时间约束错误,是否存在我将
std::move
和 std::list
与自定义视图类型结合使用而违反的语义约束,这会导致明显的未定义行为?
std::list
需要使用哨兵节点作为其结束迭代器(它需要支持--
)。有两种方法可以做到:
在第一种情况下,当移动列表时,元素的迭代器成为新列表的迭代器,但末尾迭代器仍然指向原始列表。因此,在您的代码中,如果
r
不为空,则 begin_
指向移动后 range_
中的第一个元素,但 end_
仍然指向即将被销毁的 r
。因此,当循环尝试递增 begin_
直到达到 end_
时……好吧,它永远不会这样做,并且您会得到一个无限循环。 (哨兵节点的 next 指针指向 *begin()
很方便,这就是为什么这看起来像是多次迭代列表的原因。)
这只是当您移动列表时的问题,因为
views::all
是左值的按引用,因此在这种情况下,所有内容都在处理相同的 list
对象。
旁注:如果可以复制或移动包含的类,那么同时存储迭代器(或哨兵)及其指向的范围是相当棘手的。默认的成员复制/移动将不起作用 - 您需要为迭代器执行相当于
non-propagating-cache
的操作。
遵循@273K的建议,确保在将
begin_
end_
移动到成员初始值设定项列表中的Range
之后,在构造函数主体中初始化r
和range_
可以消除未定义的行为。
#include <concepts>
#include <iostream>
#include <list>
#include <ranges>
#include <vector>
template <std::ranges::input_range Range>
requires std::ranges::view<Range>
class ContainerView : public std::ranges::view_interface<ContainerView<Range>> {
private:
std::ranges::iterator_t<Range> begin_ {};
std::ranges::sentinel_t<Range> end_ {};
Range range_ {};
public:
constexpr ContainerView() = default;
constexpr ContainerView(Range r)
: range_(std::move(r))
{
begin_ = std::begin(range_);
end_ = std::end(range_);
}
constexpr auto begin() const { return begin_; }
constexpr auto end() const { return end_; }
};
int main()
{
std::list my_list { 1, 2, 3 };
auto my_container_view { ContainerView(std::views::all(std::move(my_list))) };
for (auto const& c : my_container_view)
std::cout << c << '\n';
}