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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/8164567/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-28 18:06:52  来源:igfitidea点击:

How to make my custom type to work with "range-based for loops"?

c++for-loopc++11customization

提问by ereOn

Like many people these days I have been trying the different features that C++11 brings. One of my favorites is the "range-based for loops".

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

I understand that:

我明白那个:

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

Is equivalent to:

相当于:

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

And that begin()simply returns a.begin()for standard containers.

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

But what if I want to make my custom type "range-based for loop"-aware?

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

Should I just specialize begin()and end()?

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

If my custom type belongs to the namespace xml, should I define xml::begin()or std::begin()?

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

In short, what are the guidelines to do that?

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

采纳答案by Yakk - Adam Nevraumont

The standard has been changed since the question (and most answers) were posted in the resolution of this defect report.

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

The way to make a for(:)loop work on your type Xis now one of two ways:

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

  • Create member X::begin()and X::end()that return something that acts like an iterator

  • Create a free function begin(X&)and end(X&)that return something that acts like an iterator, in the same namespace as your type X.1

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

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

And similar for constvariations. This will work both on compilers that implement the defect report changes, and compilers that do not.

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

The objects returned do not have to actually be iterators. The for(:)loop, unlike most parts of the C++ standard, is specified to expand to something equivalent to:

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

for( range_declaration : range_expression )

becomes:

变成:

{
  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_exprand end_expris the magic that calls begin/end.2

其中以 开头的变量__仅用于说明,begin_expr并且end_expr是调用begin/ end.2的魔法

The requirements on the begin/end return value are simple: You must overload pre-++, ensure the initialization expressions are valid, binary !=that can be used in a boolean context, unary *that returns something you can assign-initialize range_declarationwith, and expose a public destructor.

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

Doing so in a way that isn't compatible with an iterator is probably a bad idea, as future iterations of C++ might be relatively cavalier about breaking your code if you do.

以与迭代器不兼容的方式执行此操作可能是一个坏主意,因为如果您这样做,C++ 的未来迭代可能会相对傲慢地破坏您的代码。

As an aside, it is reasonably likely that a future revision of the standard will permit end_exprto return a different type than begin_expr. This is useful in that it permits "lazy-end" evaluation (like detecting null-termination) that is easy to optimize to be as efficient as a hand-written C loop, and other similar advantages.

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



1 Note that for(:)loops store any temporary in an auto&&variable, and pass it to you as an lvalue. You cannot detect if you are iterating over a temporary (or other rvalue); such an overload will not be called by a for(:)loop. See [stmt.ranged] 1.2-1.3 from n4527.

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

2 Either call the begin/endmethod, or ADL-only lookup of free function begin/end, ormagic for C-style array support. Note that std::beginis not called unless range_expressionreturns an object of type in namespace stdor dependent on same.

2在调用begin/end方法,或ADL-仅查找的自由功能begin/ end魔术C数组的支持。请注意,std::begin除非range_expression返回类型为 innamespace std或依赖于相同类型的对象,否则不会调用。



In c++17the range-for expression has been updated

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

with the types of __beginand __endhave been decoupled.

与类型__begin__end已经解耦。

This permits the end iterator to not be the same type as begin. Your end iterator type can be a "sentinel" which only supports !=with the begin iterator type.

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

A practical example of why this is useful is that your end iterator can read "check your char*to see if it points to '0'" when ==with a char*. This allows a C++ range-for expression to generate optimal code when iterating over a null-terminated char*buffer.

为什么这是非常有用的一个实际的例子是,你的最终迭代器可以读取“检查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 examplein a compiler without full C++17 support; forloop manually expanded.

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

回答by csjpeter

I write my answer because some people might be more happy with simple real life example without STL includes.

我写我的答案是因为有些人可能对没有 STL 的简单现实生活示例更满意。

I have my own plain only data array implementation for some reason, and I wanted to use the range based for loop. Here is my solution:

由于某种原因,我有自己的纯数据数组实现,我想使用基于范围的 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 ...
 };

Then the usage example:

然后是使用示例:

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

回答by Steve Jessop

The relevant part of the standard is 6.5.4/1:

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

if _RangeT is a class type, the unquali?ed-ids begin and end are looked up in the scope of class _RangeT as if by class member access lookup (3.4.5), and if either (or both) ?nds at least one declaration, begin- expr and end-expr are __range.begin()and __range.end(), respectively;

— otherwise, begin-expr and end-expr are begin(__range)and end(__range), respectively, where begin and end are looked up with argument-dependent lookup (3.4.2). For the purposes of this name lookup, namespace std is an associated namespace.

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

— 否则,begin-expr 和 end-expr 分别是begin(__range)end(__range),其中 begin 和 end 使用依赖于参数的查找(3.4.2)进行查找。出于此名称查找的目的,命名空间 std 是关联的命名空间。

So, you can do any of the following:

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

  • define beginand endmember functions
  • define beginand endfree functions that will be found by ADL (simplified version: put them in the same namespace as the class)
  • specialize std::beginand std::end
  • 定义beginend成员函数
  • 定义beginend释放将被 ADL 找到的函数(简化版:将它们放在与类相同的命名空间中)
  • 专精std::beginstd::end

std::begincalls the begin()member function anyway, so if you only implement one of the above, then the results should be the same no matter which one you choose. That's the same results for ranged-based for loops, and also the same result for mere mortal code that doesn't have its own magical name resolution rules so just does using std::begin;followed by an unqualified call to begin(a).

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

If you implement the member functions andthe ADL functions, though, then range-based for loops should call the member functions, whereas mere mortals will call the ADL functions. Best make sure they do the same thing in that case!

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

If the thing you're writing implements the container interface, then it will have begin()and end()member functions already, which should be sufficient. If it's a range that isn't a container (which would be a good idea if it's immutable or if you don't know the size up front), you're free to choose.

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

Of the options you lay out, note that you must notoverload std::begin(). You are permitted to specialize standard templates for a user-defined type, but aside from that, adding definitions to namespace std is undefined behavior. But anyway, specializing standard functions is a poor choice if only because the lack of partial function specialization means you can only do it for a single class, not for a class template.

在您布置的选项中,请注意您不得重载std::begin(). 您可以为用户定义的类型专门化标准模板,但除此之外,将定义添加到命名空间 std 是未定义的行为。但是无论如何,如果仅仅因为缺少部分函数专业化意味着您只能为单个类而不是类模板进行专业化,那么专业化标准函数是一个糟糕的选择。

回答by B?ови?

Should I just specialize begin() and end() ?

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

As far as I know, that is enough. You also have to make sure that incrementing the pointer would get from the begin to the end.

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

Next example (it is missing const version of begin and end) compiles and works fine.

下一个示例(缺少开始和结束的 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;
    }
}

Here is another example with begin/end as functions. They have tobe in the same namespace as the class, because of ADL :

这是另一个将开始/结束作为函数的示例。由于 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;
    }
}

回答by Chris Redford

In case you want to back a class's iteration directly with its std::vectoror std::mapmember, here is the code for that:

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

回答by RajibTheKing

Here, I am sharing the simplest example of creating custom type, that will work with "range-based for loop":

在这里,我将分享创建自定义类型的最简单示例,它将与“基于范围的 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;
}

Hope, it will be helpful for some novice developer like me :p :)
Thank You.

希望,这对像我这样的新手开发人员会有所帮助:p :)
谢谢。

回答by user2366975

Chris Redford's answer also works for Qt containers (of course). Here is an adaption (notice I return a constBegin(), respectively constEnd()from the const_iterator methods):

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

回答by Rick

I would like to elaborate some parts of @Steve Jessop's answer, for which at first I didn't understand. Hope it helps.

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

std::begincalls the begin()member function anyway, so if you only implement one of the above, then the results should be the same no matter which one you choose. That's the same results for ranged-based for loops, and also the same result for mere mortal code that doesn't have its own magical name resolution rules so just does using std::begin;followed by an unqualified call to begin(a).

If you implement the member functionsandthe ADL functions, though, then range-based for loops should call the member functions, whereas mere mortals will call the ADL functions. Best make sure they do the same thing in that case!

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

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



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

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

  • If ...
  • If range_expressionis an expression of a class type Cthat has both a member named beginand a member named end(regardless of the type or accessibility of such member), then begin_expris __range.begin() and end_expris __range.end();
  • Otherwise, begin_expris begin(__range)and end_expris end(__range), which are found via argument-dependent lookup (non-ADL lookup is not performed).
  • 如果 ...
  • 如果range_expressionC同时具有命名成员begin和命名成员end(无论此类成员的类型或可访问性如何)的类类型的表达式,则begin_expris __range.begin() 和end_expris __range.end();
  • 否则,begin_exprisbegin(__range)end_expris end(__range),它们是通过依赖于参数的查找找到的(不执行非 ADL 查找)。

For range-based for loop, member functions are selected first.

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

But for

但对于

using std::begin;
begin(instance);

ADL functions are selected first.

首先选择 ADL 功能。



Example:

例子:

#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.
//      }
}