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

时间:2011-11-17 09:08:35

标签: c++ for-loop c++11 customization

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

我理解:

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

相当于:

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

begin()只返回标准容器的a.begin()

但是,如果我想使我的自定义类型“基于范围的循环”-aware 怎么办?

我应该专攻begin()end()吗?

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

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

9 个答案:

答案 0 :(得分:145)

自问题(和大多数答案)发布in the resolution of this defect report以来,标准已发生变化。

使for(:)循环适用于您的类型X的方法现在是以下两种方式之一:

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

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

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

返回的对象实际上不必是迭代器。与C ++标准的大多数部分不同,for(:)循环是specified to expand to something equivalent to

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
  }
}

__开头的变量仅用于展示,begin_exprend_expr是调用begin / end的魔力.²

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

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

顺便说一句,未来的标准修订版很可能允许range_declaration返回与end_expr不同的类型。这很有用,因为它允许“懒惰结束”评估(如检测空终止),这种评估易于优化,与手写C循环一样高效,并具有其他类似优势。

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

²可以调用for(:) / begin方法,也可以调用仅限ADL的自由函数end / begin魔法C风格的阵列支持。请注意,除非end返回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
  }
}

__begin__end的类型已经解耦。

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

为什么这有用的一个实际示例是,您的结束迭代器可以在char* '0'时查看“检查==是否指向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;
  }
};
编译器中的

live example没有完整的C ++ 17支持;手动扩展for循环。

答案 1 :(得分:52)

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

  

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

     

- 否则,begin-expr和end-expr为begin(__range)和   end(__range)分别用于查找开头和结尾   参数依赖查找(3.4.2)。出于此名称的目的   lookup,namespace std是一个关联的命名空间。

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

  • 定义beginend成员函数
  • 定义将由ADL找到的beginend个免费函数(简化版本:将它们放在与类相同的命名空间中)
  • 专攻std::beginstd::end
无论如何,

std::begin调用begin()成员函数,因此如果您只实现上述之一,那么无论您选择哪一个,结果都应该相同。对于基于范围的for循环,这是相同的结果,对于没有自己的神奇名称解析规则的凡人代码也是如此,因此using std::begin;后跟对begin(a)的无限制调用

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

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

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

答案 2 :(得分:36)

我写了我的答案,因为有些人可能对没有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 ...
 };

然后是用法示例:

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

答案 3 :(得分:31)

  

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

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

下一个示例(缺少const和begin的版本)编译并正常工作。

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

这是另一个以开头/结尾为功能的例子。由于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;
    }
}

答案 4 :(得分:12)

如果你想直接用它的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;
}

答案 5 :(得分:1)

在这里,我将分享创建自定义类型的最简单示例,该示例将与&#34; 基于范围的for循环&#34;:

#include<iostream>
using namespace std;

template<typename T, int sizeOfArray>
class MyCustomType
{
private:
    T *data;
    int indx;
public:
    MyCustomType(){
        data = new T[sizeOfArray];
        indx = -1;
    }
    ~MyCustomType(){
        delete []data;
    }
    void addData(T newVal){
        data[++indx] = newVal;
    }

    //write definition for begin() and end()
    //these two method will be used for "ranged based loop idiom"
    T* begin(){
        return &data[0];
    }
    T* end(){
        return  &data[sizeOfArray];
    }
};
int main()
{
    MyCustomType<double, 2> numberList;
    numberList.addData(20.25);
    numberList.addData(50.12);
    for(auto val: numberList){
        cout<<val<<endl;
    }
    return 0;
}

希望,对我这样的新手开发者来说会有所帮助:p :)
谢谢。

答案 6 :(得分:1)

Chris Redford的回答也适用于Qt容器(当然)。这是一个自适应(注意我从const_iterator方法中分别返回constBegin() constEnd()}:

class MyCustomClass{
    QList<MyCustomDatatype> data_;
public:    
    // ctors,dtor, methods here...

    QList<MyCustomDatatype>::iterator begin() { return data_.begin(); }
    QList<MyCustomDatatype>::iterator end() { return data_.end(); }
    QList<MyCustomDatatype>::const_iterator begin() const{ return data_.constBegin(); }
    QList<MyCustomDatatype>::const_iterator end() const{ return data_.constEnd(); }
};

答案 7 :(得分:1)

受BitTickler关于如何使其适用于非“容器”类型的评论的启发,这是一些适用于double s的事物的最小示例:

class dranged {
    double start, stop, step, cur;
    int index;

public:
    dranged(double start, double stop, double step) :
        start(start), stop(stop), step(step),
        cur(start), index(0) {}

    auto begin() { return *this; }
    auto end() { return *this; }

    double operator*() const { return cur; }

    auto& operator++() {
        index += 1;
        cur = start + step * index;
        return *this;
    }

    bool operator!=(const dranged &rhs) const {
        return cur < rhs.stop;
    }
};

请注意,在<运算符中使用!=可以保持正确的不变性,但显然假设step是正数,并不适合在更广泛的范围内使用。我使用了整数index来防止浮点错误的传播,但为简化起见。

这可以用作:

double sum() {
    double accum = 0;
    for (auto val : dranged(0, 6.28, 0.1)) {
        accum += val;
    }
    return accum;
}

经过优化(例如,对于GCC,-Os或高于-O1或对于Clang,-O2进行编译时,GCC和Clang都产生非常高的reasonable code

答案 8 :(得分:0)

我想详细说明@Steve Jessop的答案的某些部分,起初我不理解。希望对您有所帮助。

std::begin仍会调用begin()成员函数,因此如果您 仅实施上述一项,则结果应相同 无论您选择哪一个。相同的结果 基于范围的for循环,对于普通代码也具有相同的结果 没有自己神奇的名称解析规则,所以 using std::begin;,然后是对begin(a)的无条件呼叫。

但是,如果实现成员函数 ADL函数, 然后基于范围的for循环应调用成员函数,而 只有凡人会调用ADL函数。最好确保他们这样做 在那种情况下也是一样!


https://en.cppreference.com/w/cpp/language/range-for

  • 如果...
  • 如果range_expression是类类型C的表达式,它既有一个名为begin的成员又有一个名为end的成员(无论 该成员的类型或可访问性),则begin_expr__range.begin()和end_expr__range.end();
  • 否则,begin_exprbegin(__range)end_exprend(__range),这是通过与参数相关的查找(非ADL 查找未执行。)

对于基于范围的for循环,首先选择成员函数。

但是

using std::begin;
begin(instance);

首先选择ADL功能。


示例:

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

namespace Foo{
    struct A{
        //member function version
        int* begin(){
            cout << "111";
            int* p = new int(3);  //leak I know, for simplicity
            return p;
        }
        int *end(){
            cout << "111";
            int* p = new int(4);
            return p;
        }
    };

    //ADL version

    int* begin(A a){
        cout << "222";
        int* p = new int(5);
        return p;
    }

    int* end(A a){
        cout << "222";
        int* p = new int(6);
        return p;
    }

}

int main(int argc, char *args[]){
//    Uncomment only one of two code sections below for each trial

//    Foo::A a;
//    using std::begin;
//    begin(a);  //ADL version are selected. If comment out ADL version, then member functions are called.


//      Foo::A a;
//      for(auto s: a){  //member functions are selected. If comment out member functions, then ADL are called.
//      }
}