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

ere*_*eOn 230 c++ customization for-loop c++11

像许多人一样,我一直在尝试C + 11带来的不同功能.我最喜欢的一个是"基于范围的循环".

我明白那个:

for(Type& v : a) { ... }
Run Code Online (Sandbox Code Playgroud)

相当于:

for(auto iv = begin(a); iv != end(a); ++iv)
{
  Type& v = *iv;
  ...
}
Run Code Online (Sandbox Code Playgroud)

而这begin()只是返回a.begin()标准容器.

但是,如果我想让我的自定义类型"基于范围的循环" - 意识到什么?

如果我只是专注begin()end()

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

简而言之,这样做的准则是什么?

Yak*_*ont 163

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

现在,for(:)循环适用于您的类型X的方法有两种:

  • 创建成员X::begin()X::end()返回一个像迭代器一样的东西

  • 创建一个免费的功能begin(X&)end(X&)这回的东西,就像一个迭代器,在同一个命名空间为您的类型X

const变化类似.这对于实现缺陷报告更改的编译器和不执行缺陷报告更改的编译器都有效.

返回的对象实际上不必是迭代器.for(:)与C++标准的大多数部分不同,该循环被指定扩展为等效于:

for( range_declaration : range_expression )
Run Code Online (Sandbox Code Playgroud)

变为:

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

以变​​量开头的变量__仅用于展示,begin_expr而且end_expr是调用begin/.的end魔力

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

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

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


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

²可以调用begin/ end方法,也可以调用ADL-only free function begin/ end,或者魔术来支持C风格的数组.请注意,std::begin除非range_expression返回类型为对象namespace std或依赖于对象的对象,否则不会调用它.


,范围表达式已更新

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

与类型__begin__end已解耦.

这允许结束迭代器与begin不是同一类型.您的结束迭代器类型可以是"sentinel",它只支持!=begin迭代器类型.

一个有用的原因的一个实际例子是你的结束迭代器可以读取"检查你char*是否指向'0'"时==的a char*.这允许C++ range-for表达式在迭代空终止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;
  }
};
Run Code Online (Sandbox Code Playgroud)

在没有完全C++ 17支持的编译器中的实例 for循环手动扩展.

  • 这个答案确实会受益于可以复制和实现的模板示例。 (9认同)
  • @ZacTaylor 它不适合非 C++ 程序员。其目的是说实话。我可以对你撒谎。我可以编写样板代码,你可以复制粘贴但无法理解。但如果您自己编写,则应该了解上面第一个“div”之前的所有内容。在某些情况下(当您包装另一个容器或另一个容器的一部分时),您只需要知道前 3 段和 2 个项目符号。 (3认同)
  • @RichieHH 足够公平了。不过要明确的是,我上面所做的事情是拥有 10 年以上经验的专业 C++ 程序员可能从未接触过的事情,因为没有必要。C++ 很有深度,您无需深入了解 99.9% 的代码。在Java中,事实上你可以编写一个字节码汇编器来将你的lua风格的脚本代码转换为“本地”java,动态加载它,替换gc,这样你就可以在需要替换时卸载脚本,以及其他疯狂的东西并不是什么你需要关心其中任何一个。 (3认同)

Ste*_*sop 52

标准的相关部分是6.5.4/1:

如果_RangeT是一个类类型,则在类_RangeT的范围内查找unquali fi--id begin和end,就像通过类成员访问查找(3.4.5)一样,如果其中任何一个(或两者)找到至少一个声明,则开始- expr和end-expr分别为__range.begin()__range.end();

- 否则,begin-expr和end-expr分别是begin(__range)end(__range)依赖于参数的查找(3.4.2)查找开始和结束的地方.出于此名称查找的目的,名称空间std是关联的名称空间.

因此,您可以执行以下任何操作:

  • 定义beginend成员函数
  • 定义beginend释放ADL将发现的函数(简化版本:将它们放在与类相同的命名空间中)
  • 专注std::beginstd::end

std::beginbegin()无论如何都要调用成员函数,所以如果你只实现上面的一个,那么无论你选择哪一个,结果应该是相同的.对于基于范围的for循环来说,这是相同的结果,对于没有自己的魔法名称解析规则的凡人代码也是如此,因此只需要using std::begin;进行非限定的调用begin(a).

但是,如果实现了成员函数 ADL函数,那么基于范围的for循环应该调用成员函数,而凡人将调用ADL函数.最好确保在这种情况下他们做同样的事情!

如果您正在编写的东西实现了容器接口,那么它将具有begin()end()成员函数,这应该就足够了.如果它是一个不是容器的范围(如果它是不可变的,或者如果你不知道前面的大小,这将是一个好主意),你可以自由选择.

在您列出的选项中,请注意您不能超载std::begin().您可以为用户定义的类型专门化标准模板,但除此之外,向namespace std添加定义是未定义的行为.但无论如何,专门的标准函数是一个糟糕的选择,只是因为缺少部分函数专业化意味着你只能为单个类而不是类模板.

  • 这需要更新http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#1442. (3认同)
  • @Pubby:看看6.5.4,我认为InputIterator就足够了.但实际上我并不认为返回的类型*具有*作为基于范围的迭代器.该语句在标准中由它的等价物定义,因此它只足以实现标准中代码中使用的表达式:运算符`!=`,前缀`++`和一元`*`.实现`begin()`和`end()`成员函数或返回除迭代器之外的任何东西的非成员ADL函数可能是*不明智的*,但我认为这是合法的.我认为专门研究`std :: begin`来返回非迭代器是UB. (2认同)

csj*_*ter 43

我写了我的答案,因为有些人可能对没有STL包含的简单现实生活例子更满意.

由于某种原因,我有自己的普通数据阵列实现,我想使用基于范围的for循环.这是我的解决方案:

 template <typename DataType>
 class PodArray {
 public:
   class iterator {
   public:
     iterator(DataType * ptr): ptr(ptr){}
     iterator operator++() { ++ptr; return *this; }
     bool operator!=(const iterator & other) const { return ptr != other.ptr; }
     const DataType& operator*() const { return *ptr; }
   private:
     DataType* ptr;
   };
 private:
   unsigned len;
   DataType *val;
 public:
   iterator begin() const { return iterator(val); }
   iterator end() const { return iterator(val + len); }

   // rest of the container definition not related to the question ...
 };
Run Code Online (Sandbox Code Playgroud)

然后是用法示例:

PodArray<char> array;
// fill up array in some way
for(auto& c : array)
  printf("char: %c\n", c);
Run Code Online (Sandbox Code Playgroud)

  • 该示例具有begin()和end()方法,还具有一个基本的(易于理解的)示例迭代器类,可以轻松地针对任何自定义容器类型进行调整。比较std :: array &lt;&gt;和任何可能的替代实现是一个不同的问题,我认为与基于范围的for循环无关。 (2认同)
  • for correctness operator!=应该是const (2认同)
  • 删除 `const DataType&amp; operator*()` 的 `const` 返回限定符,并让用户选择使用 `const auto&amp;` 或 `auto&amp;` 是否更合适?不管怎样,谢谢,很好的答案;) (2认同)
  • @MehrshadFarahani:它可能会运行,但这样做是非常不明智的,因为您期望 ++ 运算符返回对 self 的引用,因此 `for (iterator i=0; i != 20; ++i);` 会起作用但 `for(iterator i=0; ++i != 20; );` 不会。 (2认同)

BЈо*_*вић 33

我应该专门研究begin()和end()吗?

据我所知,这已经足够了.您还必须确保递增指针将从开始到结束.

下一个例子(它缺少开始和结束的const版本)编译并正常工作.

#include <iostream>
#include <algorithm>

int i=0;

struct A
{
    A()
    {
        std::generate(&v[0], &v[10], [&i](){  return ++i;} );
    }
    int * begin()
    {
        return &v[0];
    }
    int * end()
    {
        return &v[10];
    }

    int v[10];
};

int main()
{
    A a;
    for( auto it : a )
    {
        std::cout << it << std::endl;
    }
}
Run Code Online (Sandbox Code Playgroud)

这是另一个以begin/end为函数的示例.由于ADL,它们必须与类位于同一名称空间中:

#include <iostream>
#include <algorithm>


namespace foo{
int i=0;

struct A
{
    A()
    {
        std::generate(&v[0], &v[10], [&i](){  return ++i;} );
    }

    int v[10];
};

int *begin( A &v )
{
    return &v.v[0];
}
int *end( A &v )
{
    return &v.v[10];
}
} // namespace foo

int main()
{
    foo::A a;
    for( auto it : a )
    {
        std::cout << it << std::endl;
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 恭喜:)可能值得一提的是第二个例子中的Argument Dependent Lookup(ADL)或Koenig Lookup这两个术语(解释*为什么*自由函数应该与它所操作的类在同一个命名空间中). (2认同)

Chr*_*ord 13

如果您想直接与背面类的迭代std::vectorstd::map成员,这里是该代码:

#include <iostream>
using std::cout;
using std::endl;
#include <string>
using std::string;
#include <vector>
using std::vector;
#include <map>
using std::map;


/////////////////////////////////////////////////////
/// classes
/////////////////////////////////////////////////////

class VectorValues {
private:
    vector<int> v = vector<int>(10);

public:
    vector<int>::iterator begin(){
        return v.begin();
    }
    vector<int>::iterator end(){
        return v.end();
    }
    vector<int>::const_iterator begin() const {
        return v.begin();
    }
    vector<int>::const_iterator end() const {
        return v.end();
    }
};

class MapValues {
private:
    map<string,int> v;

public:
    map<string,int>::iterator begin(){
        return v.begin();
    }
    map<string,int>::iterator end(){
        return v.end();
    }
    map<string,int>::const_iterator begin() const {
        return v.begin();
    }
    map<string,int>::const_iterator end() const {
        return v.end();
    }

    const int& operator[](string key) const {
        return v.at(key);
    }
    int& operator[](string key) {
        return v[key];
    } 
};


/////////////////////////////////////////////////////
/// main
/////////////////////////////////////////////////////

int main() {
    // VectorValues
    VectorValues items;
    int i = 0;
    for(int& item : items) {
        item = i;
        i++;
    }
    for(int& item : items)
        cout << item << " ";
    cout << endl << endl;

    // MapValues
    MapValues m;
    m["a"] = 1;
    m["b"] = 2;
    m["c"] = 3;
    for(auto pair: m)
        cout << pair.first << " " << pair.second << endl;
}
Run Code Online (Sandbox Code Playgroud)

  • 值得一提的是,`const_iterator` 也可以通过 `cbegin`、`cend` 等以 `auto`(C++11)兼容的方式访问。 (2认同)