我正在阅读Rainer Grimm的书 C++20:Get the Details 中关于定义自己的视图类型的第5.1.7.2节,当时我决定修改所提供的代码示例,看看它是否适用于std::list
。代码最终看起来像这样:
#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
Package 在对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
以及自定义视图类型时,是否存在会导致明显的未定义行为的语义约束?
2条答案
按热度按时间nvbavucw1#
std::list
需要使用一个sentinel节点作为它的结束迭代器(它需要支持--
)。有两种方法可以做到这一点:在第一种情况下,当你移动一个列表时,指向元素的迭代器变成了新列表的迭代器,但是结束迭代器仍然指向原始列表。所以在你的代码中,如果
r
不为空,那么begin_
指向移动后range_
中的第一个元素,但是end_
仍然指向即将被销毁的r
。所以当循环试图递增begin_
直到它到达end_
时,它永远不会这样做,你会得到一个无限循环。(sentinel节点的下一个指针指向*begin()
是很方便的,这就是为什么它看起来像是在多次迭代列表。)只有当你移动列表时,这才是一个问题,因为
views::all
是左值上的引用,所以在这种情况下,所有东西都在处理同一个list
对象。旁注:如果包含类可以被复制或移动,那么同时存储迭代器(或sentinel)和它指向的范围是非常棘手的。默认的成员方式复制/移动将不起作用-您需要为迭代器执行
non-propagating-cache
的等效操作。vu8f3i0k2#
遵循@273K的建议,确保在成员初始化器列表中将
Range
r
移动到range_
之后,在构造函数体中初始化begin_
和end_
,可以消除未定义的行为。字符串