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

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


当前回答

我想我没有什么要解释的,因为答案已经说明了这一点。但我可能不得不引用标准(N4885)中的这句话:

(支撑。[远程]/1:(强调我的)

The range-based for statement for ( init-statement(opt) for-range-declaration : for-range-initializer ) statement(possibly curly-braced) is equivalent to: { // 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 where (1.1) if the for-range-initializer is an expression, it is regarded as if it were surrounded by parentheses (so that a comma operator cannot be reinterpreted as delimiting two init-declarators); (1.2) range, begin, and end are variables defined for exposition only; and (3.1) begin-expr and end-expr are determined as follows: (1.3.1) if the for-range-initializer is an expression of array type R, begin-expr and end-expr are range and range+N, respectively, where N is the array bound. If R is an array of unknown bound or an array of incomplete type, the program is ill-formed; (1.3.2) if the for-range-initializer is an expression of class type C, and [class.member.lookup] in the scope of C for the names begin and end each find at least one declaration, begin-expr and end-expr are range.begin() and range.end(), respectively; (1.3.3) otherwise, begin-expr and end-expr are begin(range) and end(range), respectively, where begin and end undergo argument-dependent lookup ([basic.lookup.argdep]).


请注意,字符串、数组和所有STL容器都是可迭代的数据结构,因此已经可以使用基于范围的for循环对它们进行迭代。为了使数据结构可迭代,它必须类似于现有的STL迭代器:

1-必须有begin和end方法对该结构进行操作,可以作为成员,也可以作为独立函数,并且返回结构的开始和结束的迭代器。

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

这个程序的输出是:

第一个 第二个 第三 第四

其他回答

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

让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。

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

受到BitTickler关于如何使它适用于非“容器”类型的评论的启发,这里有一个适用于双精度对象的最小示例:

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是正的,并且不适用于更一般的范围。我使用整数索引来防止浮点错误的传播,但在其他方面力求简单。

这可以用于:

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

GCC和Clang在进行优化编译时都能生成非常合理的代码(例如,GCC的-Os或高于-O1, Clang的-O2)。

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

if _RangeT is a class type, the unqualified-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) finds 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.

所以,你可以做以下任何一件事:

定义开始和结束成员函数 定义ADL可以找到的开始和结束自由函数(简化版本:将它们放在与类相同的命名空间中) 专门化std::begin和std::end

Std::begin调用begin()成员函数,所以如果你只实现了上面的一个,那么无论你选择哪一个,结果都应该是一样的。这对于基于范围的for循环也是一样的结果,对于没有自己神奇的名称解析规则的普通代码也是一样的结果,所以只使用std::begin;接着是开始(a)的不合格调用。

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

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

对于所布局的选项,请注意不能重载std::begin()。允许为用户定义的类型专门化标准模板,但除此之外,向名称空间std添加定义是未定义的行为。但无论如何,专门化标准函数是一个糟糕的选择,因为缺乏部分函数专门化意味着您只能为单个类而不是类模板这样做。