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


252

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

我明白那个:

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

等效于:

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

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

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

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

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

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


通过定义begin/endstatic或free 的成员或朋友,这是可能的begin/end。请注意放置免费功能的命名空间是什么:stackoverflow.com/questions/28242073/…–
alfC

任何人都可以用不是容器的浮点值范围示例发布答案for( auto x : range<float>(0,TWO_PI, 0.1F) ) { ... }。我很好奇您如何解决难以定义的'operator!=()``这一事实。*__begin在这种情况下,解引用()又如何呢?我认为这将是一个很大的贡献,如果有人向我们展示了做!
BitTickler '18 -10-13

Answers:


183

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

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

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

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

和类似的const变化。这将对实现缺陷报告更改的编译器和未实现缺陷报告的编译器都起作用。

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

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_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-仅查找的自由功能begin/ end魔术C数组的支持。请注意,std::begin除非range_expression返回类型为in 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迭代器类型。

一个为什么有用的实际示例是,当使用时,最终迭代器可以读取“检查您的代码char*以查看是否指向'0'” 。当在以空终止的缓冲区上进行迭代时,这允许C ++范围表达式生成最佳代码。==char*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;
  }
};

没有完全C ++ 17支持的编译器中的实时示例for循环手动展开。


如果基于范围的for使用不同的查找机制,则可能有可能安排该基于范围的for获得beginend正常代码中可用的函数和函数不同的一对。也许它们然后可能会非常专门化,以表现出不同的行为(即,通过忽略end参数来获得最大的优化可能更快。)但是我对命名空间的把握还不够,无法确定如何做到这一点。
亚伦·麦克戴德

@AaronMcDaid不太实用。您很容易最终得到令人惊讶的结果,因为某些调用begin / end的方法将以基于范围的begin / end结尾,而其他方法则不会。无害的更改(从客户端)将获得行为更改。
Yakk-亚当·内夫罗蒙特2015年

1
您不需要begin(X&&)。临时对象通过auto&&基于范围的for 悬挂在空中,并且begin始终使用左值(__range)进行调用。
TC

2
这个答案将真正受益于可以复制和实现的模板示例。
托马什Zato -恢复莫妮卡

我宁愿强调迭代器类型(*,++,!=)的属性。我应该要求您改写此答复,以使迭代器类型规范更粗体。
Red.Wave

62

我写我的答案,是因为有些人可能对不带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);

2
该示例具有begin()和end()方法,还具有一个基本的(易于理解的)示例迭代器类,可以轻松地针对任何自定义容器类型对其进行调整。比较std :: array <>和任何可能的替代实现是一个不同的问题,我认为与基于范围的for循环无关。
csjpeter 2015年

这是一个非常简洁实用的答案!这正是我想要的!谢谢!
Zac Taylor

1
删除的const 返回限定符const DataType& operator*(),让用户选择使用const auto&还是更合适auto&?无论如何,谢谢,很好的回答;)
Rick

53

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

如果_RangeT是类类型,则在_RangeT类的范围内查找不合格的id开始和结束,就像通过类成员访问查找(3.4.5)一样,并且如果其中一个(或两个)找到至少一个声明,则开始-expr和end-expr分别是__range.begin()__range.end()

—否则,begin-expr和end-expr分别为begin(__range)end(__range),其中begin和end使用与参数相关的查找(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()。允许您为用户定义的类型专用标准模板,但是除此之外,将定义添加到名称空间std是未定义的行为。但是无论如何,仅由于缺乏局部功能专门化意味着只能对单个类而不是对类模板进行操作,对标准函数进行专门化是一个糟糕的选择。


迭代器是否满足某些要求?即是一个ForwardIterator或类似的东西。
Pubby 2012年

2
@Pubby:从6.5.4来看,我认为InputIterator就足够了。但实际上我不认为返回的类型是在所有基于范围的迭代器。该语句在标准中的定义与之等效,因此仅实现标准代码中使用的表达式就足够了:operator !=,prefix ++和unary *。实现并返回除迭代器以外的任何内容的成员函数或非成员ADL函数可能是不明智的,但我认为这是合法的。我认为,专门归还非迭代者的是UB。begin()end()std::begin
史蒂夫·杰索普

您确定不能重载std :: begin吗?我问,因为标准库本身在少数情况下也这样做。
ThreeBit

@ThreeBit:是的,我确定。标准库实现的规则与程序规则不同。
史蒂夫·杰索普


34

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

据我所知,这就足够了。您还必须确保递增指针将从头到尾。

下一个示例(缺少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;
    }
}

这是一个以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;
    }
}

1
@ereOn在定义类的相同名称空间中。见第二个例子
BЈовић

2
还要恭喜:)第二个示例可能值得一提的是术语自变量依赖查找(ADL)或Koenig查找(以解释为什么自由函数应该与其操作的类位于同一名称空间中)。
Matthieu M.

1
@ereOn:实际上,您不是。ADL涉及将范围扩展到查找,以自动包括参数所属的名称空间。ACCU上有一篇很好的文章,涉及过载解析,但不幸的是,该文章跳过了名称查找部分。名称查找涉及收集候选函数,您首先需要查看当前范围+参数的范围。如果没有找到匹配的名称,则上移到当前作用域的父作用域,然后再次搜索...直到到达全局作用域。
Matthieu M.

1
@BЈовић对不起,但是出于何种原因,您在end()函数中返回危险的指针?我知道它可行,但是我想了解它的逻辑。数组的末尾是v [9],为什么还要返回v [10]?
gedamial

1
@gedamial,我同意。我认为应该是return v + 10&v[10]在数组之后取消引用内存位置。
米莉·史密斯

16

如果您想直接使用其类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;
}

2
值得一提的是,const_iterator也可以在访问auto(C ++ 11)兼容的方式通过cbegincend等等
underscore_d

2

在这里,我分享了创建自定义类型的最简单示例,该示例将与“ 基于范围的for循环 ”一起使用:

#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 :)
谢谢。


为什么不分配一个额外的元素以避免在您的end方法中取消引用无效的内存?
AndersK

@Anders因为几乎所有最终迭代器指向其含有结构的端部。该end()函数本身显然不会取消引用不正确的存储位置,因为它仅采用此存储位置的“地址”。添加一个额外的元素意味着您将需要更多的内存,并且your_iterator::end()以任何方式使用都会取消引用该值的方法,因此无论如何都无法与任何其他迭代器一起使用,因为它们的构建方式相同。
Qqwy

@Qqwy他的结束方法取消引用- return &data[sizeofarray]恕我直言,它应该只返回地址数据+ sizeofarray,但我知道什么
AndersK,

@安德斯你是正确的。感谢您让我敏锐:-)。是的,data + sizeofarray这将是正确的编写方式。
19qqwy

1

Chris Redford的答案也适用于Qt容器(当然)。这是一种改编(注意,我constBegin()分别constEnd()从const_iterator方法返回a ):

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(); }
};

0

我想详细说明@Steve Jessop的答案的某些部分,起初我不理解。希望能帮助到你。

std::beginbegin()无论如何都会调用成员函数,因此,如果仅实现上述之一,则无论选择哪个结果,结果都应该相同。对于基于范围的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_expris begin(__range)end_expris end(__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.
//      }
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.