技术: C++11 RangeFor探究

range based for 可以看做 boost 的 FOR_EACH 或者 java 的增强版本的 for, 但它不同于stl 的for_each算法.

方式对比

旧方式

原来的循环:

1
2
3
4
int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };  
for (int i = 0; i < 10; i++) {}
cout << arr[i];
}

以及迭代器版本:

1
2
3
std::vector<int> vec {1,2,3,4,5,6,7,8,9,10};  
for (std::vector<int>::iterator itr = vec.begin(); itr != vec.end(); ++itr)
std::cout << *itr;

不管上面哪一种方法,都必须明确的确定for循环开头以及结尾条件

range for

range for循环(或者成为for each)基本用法: 遍历字符串,数组,map,vector等容器, 语法如下:

1
2
3
for (decl : container) {
//statement;
}

例如, 用于容器

1
2
3
4
5
6
7
string s("hello,world");

//for(auto c:s)//对于s中的每个字符的副本
for(auto &c:s) {//对于s中的每个字符,c是一个引用,赋值语句将会改变s中字符的值
c=toupper(c);
}
cout<<s<<endl;

当然也可以这么用:

1
2
3
for(int i: {1,2,3,4,5}){
std::cout << i << std::endl;
}

迭代器(推导)问题

貌似使用range_for时, 根本没有涉及到迭代器:

但是编译器底层还是把它转换成我们一般使用迭代器的版本, 只是不需要我们手动那么写了.

range for 里面, 直接推导出, value_type, 作为auto &c部分, 而不去涉及迭代器.

map就明白了:

1
2
3
4
std::map<string, int> map{{"a", 1}, {"b",2}};
for(auto &val: map) {
std:: cout << val.firtst << "->" << val.second << std::endl;
}

也就是说, auto &val 其实是 std::pair<string, value>;

对比for_each算法

range for 不同于 for_each

for_each用于逐个遍历容器元素,它对迭代器区间[first,last)所指的每一个元素,执行由单参数函数对象f所定义的操作

模板函数for_each,大概是这样的:

1
2
3
4
5
template<typename InputIterator, typename Function>  
Function for_each(InputIterator beg, InputIterator end, Function f) {
while(beg != end)
f(*beg++);
}

可用于遍历, 使用案例: (当然你用functor或者funtional对象等代替函数也是可以的)

1
2
3
4
5
6
7
8
9
10
void print(int& elem) {  
cout << elem << endl;
}

int main() {
int ia[] = {1, 2, 3};
vector<int> ivec(ia, ia + sizeof(ia) / sizeof(int));

for_each(ivec.begin(), ivec.end(), print);
}

注意事项

因range for还是会被编译器编译成带有迭代器版本的for, 所以迭代器可能存在的问题, 它也会有.

下面说说注意事项:

用于关联容器

请不要对关联式容器, 比如map, set等, 通过迭代器改变容器元素内容

他们的实现借助红黑树, 维持有序的, 所以应该是先删除, 再添加, 最好你只用range for来进行只读操作,for(const auto &c:s).

举个具体的例子:

1
2
3
std::map<string, int>  map{ { "a", 1 }, { "b", 2 }, { "c", 3 } };  
for (const auto &val : map)
cout << val.first << "->" << val.second << endl;

或者std::set, 你去修改, 直接编译器就报错了, 即便你用的是它的引用.

只读情况(元素只读), 请写 const auto&

元素兼容问题

另外还有一点注意, range for左边的元素应该和容器里面的元素, 类型兼容或者一致

否则编译器尝试的转换可能就会失败, 比如下面的就会进行隐式转换, 导致报错:

1
2
3
4
5
6
7
8
9
10
class C
{
public:
explicit C(const string &c);
};

vector<string> vs;
for(const C& elem:vs) {
std::cout << elem << std::endl;
}

迭代器失效问题

下面说说改变容器的情况,(不同于改变容器元素)

不要在遍历途中, 修改容器属性(比如增加元素

例如:

1
2
3
4
5
6
7
8
9
10
vector<int> vec = { 1, 2, 3, 4, 5, 6 };  

int main()
{
for (auto &n : vec)
{
cout << n << endl;
vec.push_back(0); // 作死的节奏
}
}

因为底层还是会被编译器编译成迭代器版本, 改变了容器, 意味着迭代器失效, 出任何莫名奇妙的bug都不奇怪.

关于迭代器失效问题, 可以参考我的文章 《STL迭代器失效问题》, 当然也可以参考 《C++ primer 5e》

函数返回值问题

在range for 容器的位置, 放了函数表达式? 没问题的, 只要函数的返回值是容器类型, 一样好使.

例如下面的代码, 正常工作, 没有任何问题;

1
2
3
4
5
6
7
8
9
10
11
12
set<int> ss = { 1, 2, 3, 4, 5, 6 };  
const set<int>& getSet()
{
cout << "GetSet" << endl;
return ss;
}

int main()
{
for (auto &n : getSet())
cout << n << endl;
}

高级话题

如果你要自定义一个类(相当于自定义一个容器), 然后可以使用 range_for来遍历这个类; 这个时候, 你就会深刻体会到 for each需要容器支持(设计容器的时候, 同时设计迭代器).

稍稍总结一下, 你知道要在容器&迭代器中做哪些工作: (最好你理解迭代器设计模式)

  • 定义自定义类的迭代器 //可以简单到只是一个裸指针, 保存容器索引即可
  • 该类型拥有begin() 和 end() 成员方法(或者重载全局的begin() 和 end() 函数也可以),返回值为迭代器
  • 自定义迭代器的 != 比较操作
  • 自定义迭代器的 ++ 前置自增操作,显然该操作要是迭代器对象指向该容器的下一个元素
  • 自定义迭代器 * 解引用操作,显然解引用操作必须容器对应元素的引用,否则引用遍历时将会出错

遍历终止点, 遍历范围,都是依靠begin, end函数的. 所以:

偷懒的话, 只要设计一下, begin, end, 解引用以及前置自增即可; 但一定要容器和迭代器配合

给一个简单的示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
template <typename T>  
class Myiterator
{
public:
Myiterator(T val) :_value(val){}
bool operator!=(const Myiterator<T>& other) const
{
return this->_value != other._value;
}

const T & operator*()
{
return _value;
}

T operator++()
{
return ++_value;
}
private:
T _value; // 这里也可以保留Range容器的头指针
};

template<typename T>
class Range
{
public:
Range(T begin, T end) :__begin(begin), __end(end){}
Myiterator<T>& begin()
{
return Myiterator<T>(__begin);
}

Myiterator<T>& end()
{
return Myiterator<T>(__end);
}
private:
T __begin;
T __end;
};

int main()
{
for (auto i : Range<int>(1, 10))
cout << i << " ";
}

输出为:

1
1 2 3 4 5 6 7 8 9

迭代器, 操作容器, 其余范围问题, 都交给了 range for 本身.

文章目录
  1. 1. 方式对比
    1. 1.1. 旧方式
    2. 1.2. range for
    3. 1.3. 迭代器(推导)问题
    4. 1.4. 对比for_each算法
  2. 2. 注意事项
    1. 2.1. 用于关联容器
    2. 2.2. 元素兼容问题
    3. 2.3. 迭代器失效问题
    4. 2.4. 函数返回值问题
  3. 3. 高级话题
|