Range for
C++11引入了一个新的for语句,用来方便地遍历序列。这种for语句称为范围for语句(range for statement),基本形式如下:
for (declaration : expression)
statement
expression 必须返回一个序列,包括:初始值列表(std::initializer_list),数组,标准容器,以及符合特定条件的自定义类型(见下文)。
declaration 定义了一个变量,该变量必须可以被序列中的元素赋值。该变量称为控制变量。
statement 则是通常的语句,可以是一条语句,也可是用大括号括起的语句块。
该for循环从序列的第一个元素开始迭代,每次迭代从序列中提取一个元素,赋值给declaration定义的控制变量,然后执行statement,在statement中可以使用该控制变量,执行完statement后,进入下一次迭代,取出下一个元素,赋值给控制变量,然后重复这个过程。直到序列中所有的元素都遍历完为止。
例如,以下程序片段打印vector中的值:
std::vector<int> coll = { , , };
for (int i : coll)
{
std::cout << i << " ";
}
// 打印结果:1 2 3
下面逐步分析该for循环中的每个部分。
expression
expression必须是一个可以返回序列的表达式。该序列可以是初始值列表,数组,标准容器,以及符合特定条件的自定义类型。
初始值列表
for (int i : { , , })
{
std::cout << i << " ";
}
// 打印结果:1 2 3
数组
int a[] = { , , };
for (int i : a)
{
std::cout << i << " ";
}
// 打印结果:1 2 3
标准容器
std::vector<std::string> logLevel = { "Fatal", "Error", "Debug" };
for (std::string s : logLevel)
{
std::cout << s << " ";
}
// 打印结果:Fatal Error Debug
自定义类型
有两种方式可以让range for遍历自定义类型。每一种方式都需要满足特定的条件。
提供begin()和end()成员函数
第一种方式是提供begin()和end()成员函数。begin()和end()必须返回支持*,++和!=操作符的迭代器(原因见深度解析)。
下例中,我们定义了一个Range类来表示一组连续的整数,并实现了迭代器以提供*,++和!=操作符:
class RangeIterator
{
public:
RangeIterator(int num) : m_num(num) {}
int &operator ++ () { return ++m_num; }
const int &operator * () const { return m_num; }
bool operator !=(const RangeIterator &rhs) const
{
return m_num != rhs.m_num;
}
private:
int m_num;
};
class Range
{
public:
using const_iterator = const RangeIterator;
Range(int start, int end): m_start(start), m_end(end) {}
const_iterator begin() { return const_iterator(m_start); }
const_iterator end() { return const_iterator(m_end); }
private:
int m_start;
int m_end;
};
for (auto i : Range(, ))
{
std::cout << i << " ";
}
// 打印结果:5 6 7 8 9
提供begin()和end()函数
第二种方式是提供begin()和end()函数。同第一种方式一样,begin()和end()必须返回支持*,++和!=操作符的迭代器(原因见深度解析)。
下面的例子实现了一个自定义的字符串(仅用作示例,不必深究):
#include <iostream>
namespace myutil
{
struct MyString
{
char *start_pos;
char *end_pos;
char buffer[];
};
char *begin(const MyString &str) { return str.start_pos; }
char *end(const MyString &str) { return str.end_pos; }
}
int main()
{
const char *greet = "hello, world!";
const int len = strlen(greet);
myutil::MyString str;
strncpy(str.buffer, greet, len);
str.start_pos = str.buffer;
str.end_pos = str.buffer + len;
// 打印字符串中的每个字符
for (auto i : str)
{
std::cout << i;
}
std::cout << std::endl;
}
注意begin()和end()返回的类型为char*,本身就支持*,++和!=操作符,我们就不需要自己实现这三个操作符了。
临时对象
expression 可以是返回临时对象的表达式(原因见深度解析):
std::vector<int> getVector()
{
return { , , };
}
for (auto i : getVector())
{
std::cout << i << " ";
}
// 打印结果:1 2 3
declaration
declaration 定义了一个变量,该变量必须可以被序列中的元素赋值。方便起见,可以用auto来声明变量。
访问序列元素
std::vector<int> coll = { , , };
for (auto i : coll)
{
std::cout << i << " ";
}
// 打印结果:1 2 3
修改序列元素
为了修改序列中的元素必须将控制变量声明为引用:
std::vector<int> coll = { , , };
for (auto &i : coll)
{
i *= ;
}
statement
statement 是通常的语句,可以是一条语句,也可是用大括号括起的语句块。唯一需要注意的是不能在statement里增加或删除元素,因为C++11在处理range for时会持有end()返回的迭代器直到循环结束。而增加或删除元素可能会使该迭代器失效(详情见深度解析):
std::vector<int> v = { , , };
for (auto i : v)
{
//v.push_back(4); // 错误!
}
深度解析
上文中留了一些问题没有解答:为什么自定义类型需要实现begin()和end()成员函数?为什么begin()和end()返回的迭代器必须实现*,++和!=这三个操作符?为什么不能在循环体内添加/删除元素?为什么表达式可以是临时对象?下面来解答这些问题。
根据C++11的标准,range for 在逻辑上等价于以下普通的for语句:
{
auto && __range = expression;
for ( auto __begin = begin-expr, __end = end-expr;
__begin != __end; ++__begin )
{
declaration = *__begin;
statement
}
}
注意这只是逻辑上的等价,并不是编译器会将range for转换成这些代码。其中
__range
,
__begin
,
__end
也只是用来描述逻辑,并不是真正会定义这些变量。
下面结合标准,一行一行地来分析:
第2行是一个 auto 赋值,将 expression 求值的结果赋值给 __range,注意这里用了一个右值引用,因此 expression 可以是一个返回临时对象的表达式。
第3行和第4行对我们来说相当熟悉,就是一个普通的for头部。
先来看第3行,__begin和__end很好理解,它们分别表示循环控制变量和循环终止变量。 那么begin-expr和end-expr表示什么呢?假设_RangeT是expression的类型,根据标准,begin-expr和end-expr按照以下规则来确定:
- 如果_RangeT是数组,begin-expr和end-expr分别为 __range 和__range + __bound,其中__bound是数组的长度。也就是说,begin-expr和end-expr分别表示指向数组起始元素的指针,以及指向最后一个元素后面的那个元素的指针。这与我们用普通for语句遍历数组时是一样的。
- 如果_RangeT是类类型,并且提供了bgin()和end()成员函数。则begin-expr和end-expr分别为 __range.begin() 和 __range.end()。上面实现自定义类型的第一种方式(提供begin()和end()成员函数)属于这种情况。
- 其他情况则根据实参相关的查找( argument-dependent lookup,ADL)规则,在自定义名称空间和std名称空间中查找begin()和end()函数。begin-expr和end-expr分别为begin(__range)和end(__range)。上面实现自定义类型的第二种方式(提供begin()和end()函数)属于这种情况。
注意第3行中 __end 仅在for循环开始时被赋值为 end-expr,并且在循环过程中保持不变。因此我们不能在循环体中做任何导致__end失效的操作(比如增加或删除元素)。
第4行没什么特别的,和普通的for一样,判断__begin != __end是否成立,如果成立,则执行for循环体,否则退出循环。++__begin使循环控制变量自增。需要注意的是,__begin 必须支持!=和++操作符才能完成上述操作。这也就是为什么自定义类型返回的迭代器(通过begin()和end())必须支持这两个操作符的原因。
第6行将控制变量__begin解引用后赋值给declaration,这和正常的赋值一样。需要注意的是如果declaration是一个auto声明,则会根据 *__begin的类型推导出declaration中声明的变量的类型。
例如定义了一个vector常量:
const std::vector<int> coll = {1, 2, 3};
则
for (auto &x: coll)
相当于在每次迭代时都执行
auto &x = *(coll.begin())
,x的类型为
const int &
。另外,为了支持
*__begin
,自定义类型返回的迭代器类型必须支持
*
操作符。
总结
- Range for可以方便地遍历序列(初始值列表,数组,容器,自定义类型)。
- Range for可以应用于临时序列对象。
- Range for可以改变序列中元素的值,但是不能增加或删除元素。
- Range for可以应用自定义类型。自定义类型必须实现 begin() 和 end() (成员函数或非成员函数)。迭代器必须实现*,++和!=。