提问人:neuroevolutus 提问时间:11/4/2023 最后编辑:LoSneuroevolutus 更新时间:11/11/2023 访问量:161
为什么将 std::move 和 std::list 与自定义视图类型一起使用会导致无限递归?
Why does this usage of std::move and std::list with a custom view type cause an infinite recursion?
问:
我正在阅读 Rainer Grimm 的书 C++20:获取有关定义自己的视图类型的详细信息 §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 和标志成功编译,并且在运行时,它会打印数字 1、2 和 3,并按预期正常退出。-std=c++20 -fsanitize=undefined -fsanitize=address -Wall -Wextra -Werror
然而,出于好奇,当我将函数更改为在传递到时将其包装在调用中时,程序似乎陷入了无限递归中,重复打印列表的元素。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';
}
我发现奇怪的是,当我简单地交换 for a 并保持代码的其余部分不变时,程序会打印每个元素一次并优雅地退出: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
答:
按照 @273K 的建议,确保在成员初始值设定项列表中的 into 移动后,在构造函数主体中初始化 and 可以消除未定义的行为。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';
}
std::list
需要将 sentinel 节点用于其终端迭代器(它需要支持 )。有两种方法可以做到这一点:--
- 嵌入在列表本身中的哨兵节点。libstdc++ 和 libc++ 可以做到这一点。
- 动态分配的 Sentinel 节点。MSVC 执行此操作。
在第一种情况下,当您移动列表时,元素的迭代器将成为新列表的迭代器,但结束迭代器仍指向原始列表。因此,在您的代码中,如果 was not 为空,则在移动后指向第一个元素,但仍指向即将销毁的元素。因此,当循环尝试递增直到达到......好吧,它永远不会这样做,你会得到一个无限循环。(哨兵节点的下一个指针指向 很方便,这就是为什么它看起来像是多次迭代列表的原因。r
begin_
range_
end_
r
begin_
end_
*begin()
这仅在移动列表时才有问题,因为这是对左值的引用,因此在这种情况下,一切都在处理同一个对象。views::all
list
旁注:如果包含的类可以被复制或移动,那么同时存储迭代器(或哨兵)和它指向的范围是非常棘手的。默认的成员复制/移动将不起作用 - 您需要对迭代器执行等效的非传播缓存
操作。
评论
std::list
std::views::all
std::ranges::owning_view
std::ranges::ref_view
评论
begin_ { std::begin(range_) }; end_ { std::end(range_) };
range_
begin_
end_
begin_
end_
begin_
end_
begin_
end_
range_
ContainerView(Range r)