繁体   English   中英

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

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

像现在的许多人一样,我一直在尝试 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()

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

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

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

其中以__开头的变量仅用于说明,而begin_exprend_expr是调用begin / end的魔法。²

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

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

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


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

² 调用begin / end方法,或自由函数begin / end的 ADL-only 查找,C 风格数组支持的魔法。 注意std::begin不会被调用,除非range_expression返回namespace std中类型的对象或依赖于相同的对象。


中,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*时,您的最终迭代器可以读取“检查您的char*以查看它是否指向'0' ”。 这允许 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 中,我必须更加冗长。

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

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

使用模板类推导。

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。

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

标准的相关部分是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成员函数
  • 定义将由 ADL 找到的beginend自由函数(简化版:将它们放在与类相同的命名空间中)
  • 专门std::beginstd::end

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

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

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

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

我应该只专注于 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;
    }
}

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

受 BitTickler 关于如何使其适用于非“容器”类型的评论的启发,这里有一个适用于double的最小示例:

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 和 Clang 在使用优化编译时都会产生非常合理的代码(即-Os或以上-O1用于 GCC 或-O2用于 Clang)。

在这里,我将分享创建自定义类型的最简单示例,该示例将与“基于范围的 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 :)
谢谢你。

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

我想详细说明@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.
//      }
}

我想我没有什么要解释的,因为答案已经做到了。 但我可能不得不引用标准(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- 必须有beginend方法对该结构进行操作,无论是作为成员还是作为独立函数,并将迭代器返回到结构的开头和结尾。

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


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

template <class T>
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 <class T, size_t N>
struct array
{
    typedef iterator<T> 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()
{
    array<std::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));
      //}
}

这个程序的输出是:

第一第二第三第四

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM