如何使我的自定义类型与“基于范围的 for 循环”一起使用?

新手上路,请多包涵

像现在的许多人一样,我一直在尝试 C++11 带来的不同功能。我的最爱之一是“基于范围的 for 循环”。

我明白那个:

 for(Type& v : a) { ... }

相当于:

 for(auto iv = begin(a); iv != end(a); ++iv)
{
  Type& v = *iv;
  ...
}

begin() 只返回 a.begin() 用于标准容器。

但是,如果我想让 我的自定义类型“基于范围的 for 循环”感知 呢?

我应该只专注 begin()end() 吗?

如果我的自定义类型属于命名空间 xml ,我应该定义 xml::begin() 还是 std::begin()

简而言之,这样做的指导方针是什么?

原文由 ereOn 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 1.1k
2 个回答

自从问题(和大多数答案)发布 在此缺陷报告的解决方案中 以来,标准已更改。

使 for(:) 循环在您的类型 X 上工作的方法现在是以下两种方法之一:

  • 创建成员 X::begin()X::end() 返回类似于迭代器的东西

  • 创建一个自由函数 begin(X&)end(X&) 返回类似于迭代器的东西,在与您的类型相同的命名空间中 X

const 变化类似。这将适用于实现缺陷报告更改的编译器和不实现缺陷报告更改的编译器。

返回的对象不一定是迭代器。与 C++ 标准的大多数部分不同, for(:) 循环被 指定为扩展为相当于

 for( range_declaration : range_expression )

变成:

 {
  auto && __range = range_expression ;
  for (auto __begin = begin_expr,
            __end = end_expr;
            __begin != __end; ++__begin) {
    range_declaration = *__begin;
    loop_statement
  }
}

where the variables beginning with __ are for exposition only, and begin_expr and end_expr is the magic that calls begin / end

对开始/结束返回值的要求很简单:您必须重载 pre- ++ ,确保初始化表达式有效,二进制 != 可以在布尔上下文中使用,一元 * 返回可以分配初始化的东西 range_declaration 并公开一个公共析构函数。

以与迭代器不兼容的方式这样做可能是一个坏主意,因为如果你这样做,C++ 的未来迭代可能会相对比较随意地破坏你的代码。

顺便说一句,该标准的未来修订版很有可能允许 end_expr 返回与 begin_expr 不同的类型。这很有用,因为它允许“延迟”评估(如检测空终止),易于优化,与手写 C 循环一样高效,以及其他类似优点。


¹ 请注意, for(:) 循环将任何临时存储在 auto&& 变量中,并将其作为左值传递给您。您无法检测是否正在迭代临时(或其他右值);这样的重载不会被 for(:) 循环调用。参见 n4527 中的 [stmt.ranged] 1.2-1.3。

² Either call the begin / end method, or ADL-only lookup of free function begin / end , or magic for C-样式数组支持。请注意 std::begin 不会被调用,除非 range_expression 返回类型为 namespace std 或依赖于相同类型的对象。


c++17 中,range-for 表达式已更新

{
  auto && __range = range_expression ;
  auto __begin = begin_expr;
  auto __end = end_expr;
  for (;__begin != __end; ++__begin) {
    range_declaration = *__begin;
    loop_statement
  }
}

__begin__end 的类型已解耦。

这允许结束迭代器与开始的类型不同。您的结束迭代器类型可以是“哨兵”,它仅支持带有开始迭代器类型的 !=

为什么这很有用的一个实际示例是,您的最终迭代器可以读取“检查您的 char* 以查看它是否指向 '0' ”当 == 带有 char* 。这允许 C++ range-for 表达式在迭代以 null 结尾的 char* 缓冲区时生成最佳代码。

 struct null_sentinal_t {
  template<class Rhs,
    std::enable_if_t<!std::is_same<Rhs, null_sentinal_t>{},int> =0
  >
  friend bool operator==(Rhs const& ptr, null_sentinal_t) {
    return !*ptr;
  }
  template<class Rhs,
    std::enable_if_t<!std::is_same<Rhs, null_sentinal_t>{},int> =0
  >
  friend bool operator!=(Rhs const& ptr, null_sentinal_t) {
    return !(ptr==null_sentinal_t{});
  }
  template<class Lhs,
    std::enable_if_t<!std::is_same<Lhs, null_sentinal_t>{},int> =0
  >
  friend bool operator==(null_sentinal_t, Lhs const& ptr) {
    return !*ptr;
  }
  template<class Lhs,
    std::enable_if_t<!std::is_same<Lhs, null_sentinal_t>{},int> =0
  >
  friend bool operator!=(null_sentinal_t, Lhs const& ptr) {
    return !(null_sentinal_t{}==ptr);
  }
  friend bool operator==(null_sentinal_t, null_sentinal_t) {
    return true;
  }
  friend bool operator!=(null_sentinal_t, null_sentinal_t) {
    return false;
  }
};

活生生的例子

最小的测试代码是:

 struct cstring {
  const char* ptr = 0;
  const char* begin() const { return ptr?ptr:""; }// return empty string if we are null
  null_sentinal_t end() const { return {}; }
};

cstring str{"abc"};
for (char c : str) {
    std::cout << c;
}
std::cout << "\n";


这是一个简单的例子。

 namespace library_ns {
  struct some_struct_you_do_not_control {
    std::vector<int> data;
  };
}

你的代码:

 namespace library_ns {
  int* begin(some_struct_you_do_not_control& x){ return x.data.data(); }
  int* end(some_struct_you_do_not_control& x){ return x.data.data()+x.data.size(); }
  int const* cbegin(some_struct_you_do_not_control const& x){ return x.data.data(); }
  int* cend(some_struct_you_do_not_control const& x){ return x.data.data()+x.data.size(); }
  int const* begin(some_struct_you_do_not_control const& x){ return cbegin(x); }
  int const* end(some_struct_you_do_not_control const& x){ return cend(x); }
}

这是一个示例,您可以如何将无法控制的类型扩充为可迭代。

在这里,我将指针作为迭代器返回,隐藏了我在引擎盖下有一个向量的事实。

对于您拥有的类型,您可以添加方法:

 struct egg {};
struct egg_carton {
  auto begin() { return eggs.begin(); }
  auto end() { return eggs.end(); }
  auto cbegin() const { return eggs.begin(); }
  auto cend() const { return eggs.end(); }
  auto begin() const { return eggs.begin(); }
  auto end() const { return eggs.end(); }
private:
  std::vector<egg> eggs;
};

这里我重用了 vector 的迭代器。为了简洁起见,我使用 auto ;在 c++11 中,我必须更加冗长。

这是一个快速而肮脏的可迭代范围视图:

 template<class It>
struct range_t {
  It b, e;
  It begin() const { return b; }
  It end() const { return e; }
  std::size_t size() const { return end()-begin(); } // do not use distance: O(n) size() is toxic
  bool empty() const { return begin()==end(); }

  range_t without_back() const {
    if(emptty()) return *this;
    return {begin(), std::prev(end())};
  }
  range_t without_back( std::size_t n ) const {
    auto r=*this;
    while(n-->0 && !r.empty())
      r=r.without_back();
    return r;
  }
  range_t without_front() const {
    if(empty()) return *this;
    return {std::next(begin()), end()};
  }
  range_t without_front( std::size_t n ) const {
    auto r=*this;
    while(n-->0 && !r.empty())
      r=r.without_front();
    return r;
  }
  decltype(auto) front() const { return *begin(); }
  decltype(auto) back() const { return *(std::prev(end())); }
};
template<class It>
range_t(It,It)->range_t<It>;
template<class C>
auto make_range( C&& c ) {
  using std::begin; using std::end;
  return range_t{ begin(c), end(c) };
}

使用 c++17 模板类推导。

 std::vector<int> v{1,2,3,4,5};
for (auto x : make_range(v).without_front(2) ) {
  std::cout << x << "\n";
}

打印 3 4 5,跳过前 2。

原文由 Yakk - Adam Nevraumont 发布,翻译遵循 CC BY-SA 4.0 许可协议

我想我没有什么要解释的,因为答案已经做到了。但我可能不得不引用标准(N4885)中的这句话:

[stmt.ranged]/1:( 强调我的)

基于范围的 for 语句

> for ( init-statement(opt) for-range-declaration :
>       for-range-initializer
>     ) statement(possibly curly-braced)
>
> ```
>
> 相当于:
>
> ```
>     { // starts namespace scope of for-range-initializer
>
>        init-statement; (opt)
>        auto &&range = for-range-initializer ;
>        auto begin = begin-expr ;
>        auto end = end-expr ;
>        for ( ; begin != end; ++begin )
>        {
>           for-range-declaration = * begin ;
>           statement ;
>        }
>
>     } // ends namespace scope of for-range-initializer
>
> ```
>
> 在哪里
>
> (1.1) 如果 for-range-initializer 是一个表达式,则认为它好像被括号包围(这样逗号运算符就不能重新解释为分隔两个 init-declarators);
>
> (1.2) range、begin 和 end 是仅为说明而定义的变量;和
>
> (3.1) begin-expr 和 end-expr 确定如下:
>
> (1.3.1) 如果 for-range-initializer 是数组类型 R 的表达式,则 begin-expr 和 end-expr 分别是 range 和 range+N,其中 N 是数组边界。如果 R 是一个未知边界的数组或一个不完整类型的数组,则程序是非良构的;
>
> (1.3.2) 如果 for-range-initializer 是类类型 C 的表达式,并且 \[class.member.lookup\] 在 C 的范围内为名称 begin 和 end 每个找到至少一个声明,begin-expr 和end-expr 分别是 range.begin() 和 range.end();
>
> (1.3.3) 否则,begin-expr 和 end-expr 分别是 begin(range) 和 end(range),其中 begin 和 end 进行参数相关查找 (\[basic.lookup.argdep\])。

* * *

请注意,字符串、数组和所有 STL 容器都是可迭代的数据结构,因此它们已经可以使用基于范围的 for 循环进行迭代。为了使数据结构可迭代,它必须类似于现有的 STL 迭代器:

1- 必须有 `begin` 和 `end` 作为成员或独立函数对该结构进行操作的方法,并将迭代器返回到结构的开头和结尾。

2- 迭代器本身必须支持 `operator*()` 方法、 `operator !=()` 方法和 `operator++(void)` 方法,无论是作为成员还是作为独立函数。

* * *

#include #include #define print(me) std::cout << me << std::endl

template struct iterator { iterator(T* ptr) : m_ptr(ptr) {}; bool operator!=(const iterator& end) const { return (m_ptr != end.m_ptr); } T operator*() const { return *m_ptr; } const iterator& operator++() { ++m_ptr; return *this; }

private: T* m_ptr; };

template struct array { typedef iterator iterator;

array(std::initializer_list<T> lst)
{

    m_ptr = new T[N]{};
    std::copy(lst.begin(), lst.end(), m_ptr);
};

iterator begin() const { return iterator(m_ptr); }
iterator end() const { return iterator(m_ptr + N); }

~array() { delete[] m_ptr; }

private: T* m_ptr; };

int main() { arraystd::vector<std::string, 2> str_vec{ {“First”, “Second”}, {“Third”, “Fourth”} }; for(auto&& ref : str_vec) for (size_t i{}; i != ref.size(); i++) print(ref.at(i));

  //auto &&range = str_vec;
  //auto begin = range.begin();
  //auto end = range.end();
  //for (; begin != end; ++begin)
  //{
     // auto&& ref = *begin;
     // for (size_t i{}; i != ref.size(); i++)
     //     print(ref.at(i));
  //}

}

”`

这个程序的输出是:

第一第二第三第四

原文由 mada 发布,翻译遵循 CC BY-SA 4.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题