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

abithluo  于 11个月前  发布在  其他
关注(0)|答案(2)|浏览(84)

我正在阅读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::movestd::list以及自定义视图类型时,是否存在会导致明显的未定义行为的语义约束?

nvbavucw

nvbavucw1#

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

  • 一个sentinel节点嵌入到列表中。libstdc和libc可以实现。
  • 动态分配的哨兵节点。MSVC就是这样做的。

在第一种情况下,当你移动一个列表时,指向元素的迭代器变成了新列表的迭代器,但是结束迭代器仍然指向原始列表。所以在你的代码中,如果r不为空,那么begin_指向移动后range_中的第一个元素,但是end_仍然指向即将被销毁的r。所以当循环试图递增begin_直到它到达end_时,它永远不会这样做,你会得到一个无限循环。(sentinel节点的下一个指针指向*begin()是很方便的,这就是为什么它看起来像是在多次迭代列表。)
只有当你移动列表时,这才是一个问题,因为views::all是左值上的引用,所以在这种情况下,所有东西都在处理同一个list对象。
旁注:如果包含类可以被复制或移动,那么同时存储迭代器(或sentinel)和它指向的范围是非常棘手的。默认的成员方式复制/移动将不起作用-您需要为迭代器执行non-propagating-cache的等效操作。

vu8f3i0k

vu8f3i0k2#

遵循@273K的建议,确保在成员初始化器列表中将Ranger移动到range_之后,在构造函数体中初始化begin_end_,可以消除未定义的行为。

#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';
}

字符串

相关问题