像许多人一样,这些天我一直在尝试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() ?

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


当前回答

我想详细解释一下@Steve Jessop回答的一些部分,一开始我不明白。希望能有所帮助。

std::begin calls 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 functions and the 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!


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

如果…… 如果range_expression是类类型C的表达式,该表达式同时具有一个名为begin的成员和一个名为end的成员 ),则begin_expr为 __range.begin()和end_expr是__range.end(); 否则,begin_expr是begin(__range), end_expr是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.
//      }
}

其他回答

自从问题(和大多数答案)被张贴在这个缺陷报告的决议中,标准已经改变了。

让for(:)循环在你的类型X上工作的方法现在有两种:

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

const变量也是一样。这既适用于实现缺陷报告更改的编译器,也适用于不实现缺陷报告更改的编译器。

返回的对象不一定是迭代器。for(:)循环,

for( range_declaration : range_expression )

与c++标准的大多数部分不同,它被指定扩展为等价于:

{
  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.²的魔法

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

以一种与迭代器不兼容的方式这样做可能是一个坏主意,因为如果您这样做了,c++未来的迭代可能相对不会破坏您的代码。

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


¹注意for(:)循环将任何临时值存储在一个auto&&变量中,并将其作为左值传递给你。你无法检测你是否正在迭代一个临时值(或其他右值);这样的重载不会被for(:)循环调用。看到[支撑。范围从n4527的1.2-1.3。

²要么调用begin/end方法,要么只查找adl的自由函数begin/end,要么神奇地支持c风格的数组。注意,std::begin不会被调用,除非range_expression返回一个类型在命名空间std或依赖于same的对象。


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

__begin和__end类型已经解耦。

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

为什么这样做很有用的一个实际例子是,当==带有char*时,你的end迭代器可以读取“检查你的char*,看看它是否指向'0'”。这允许c++范围表达式在遍历以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;在c++11中,我必须更详细。

下面是一个快速且肮脏的可迭代范围视图:

template<class It>
struct range_t {
  It b, e;
  It begin() const { return b; }
  It end() const { return e; }

  std::size_t size() const
  // C++20 only line: (off C++20 it generates a hard error)
  requires std::random_access_iterator<It>
  {
    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
  // C++20 only line: (see below)
  requires !std::random_access_iterator<It>
  {
    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
  // C++20 only line: (see below)
  requires !std::random_access_iterator<It>
  {
    auto r=*this;
    while(n-->0 && !r.empty())
      r=r.without_front();
    return r;
  }

  // C++20 section:
  range_t without_back( std::size_t n ) const
  requires std::random_access_iterator<It>
  {
    n = (std::min)(n, size());
    return {b, e-n};
  }
  range_t without_front( std::size_t n ) const
  requires std::random_access_iterator<It>
  {
    n = (std::min)(n, size());
    return {b+n, e};
  }
  // end C++20 section


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

使用c++17模板类演绎。

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。

我想详细解释一下@Steve Jessop回答的一些部分,一开始我不明白。希望能有所帮助。

std::begin calls 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 functions and the 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!


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

如果…… 如果range_expression是类类型C的表达式,该表达式同时具有一个名为begin的成员和一个名为end的成员 ),则begin_expr为 __range.begin()和end_expr是__range.end(); 否则,begin_expr是begin(__range), end_expr是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.
//      }
}

Chris Redford的答案当然也适用于Qt容器。下面是一个调整(注意我返回了constBegin(),分别从const_iterator方法返回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(); }
};

我应该专门化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;
    }
}

我写下我的答案是因为有些人可能更喜欢简单的现实生活的例子,没有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);