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

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

当我决定修改提供的代码示例以查看它是否可以与 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
1个回答
0
投票

遵循@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.