为什么在自定义视图类型中使用 std::move 和 std::list 会导致无限递归?

问题描述 投票:0回答:2

当我决定修改提供的代码示例以查看它是否适用于 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
与自定义视图类型结合使用而违反的语义约束,这会导致明显的未定义行为?

c++ stl c++20 move-semantics std-ranges
2个回答
4
投票

std::list
需要使用哨兵节点作为其结束迭代器(它需要支持
--
)。有两种方法可以做到:

  • 嵌入列表本身的哨兵节点。 libstdc++ 和 libc++ 可以做到这一点。
  • 动态分配的哨兵节点。 MSVC 就是这样做的。

在第一种情况下,当移动列表时,元素的迭代器成为新列表的迭代器,但末尾迭代器仍然指向原始列表。因此,在您的代码中,如果

r
不为空,则
begin_
指向移动后
range_
中的第一个元素,但
end_
仍然指向即将被销毁的
r
。因此,当循环尝试递增
begin_
直到达到
end_
时……好吧,它永远不会这样做,并且您会得到一个无限循环。 (哨兵节点的 next 指针指向
*begin()
很方便,这就是为什么这看起来像是多次迭代列表的原因。)

这只是当您移动列表时的问题,因为

views::all
是左值的按引用,因此在这种情况下,所有内容都在处理相同的
list
对象。

旁注:如果可以复制或移动包含的类,那么同时存储迭代器(或哨兵)及其指向的范围是相当棘手的。默认的成员复制/移动将不起作用 - 您需要为迭代器执行相当于

non-propagating-cache
的操作。


2
投票

遵循@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';
}
© www.soinside.com 2019 - 2024. All rights reserved.