从上一节的示例中可以看出,range-based for的使用是比较简单的。但是再简单的使用方法也有一些需要注意的细节。
首先,看一下使用range-based for对map的遍历方法:
#include <iostream> #include <map> int main(void) { std::map<std::string, int> mm = { { "1", 1 }, { "2", 2 }, { "3", 3 } }; for(auto& val : mm) { std::cout << val.f?irst << " -> " << val.second << std::endl; } return 0; }
这里需要注意两点:
1)for循环中val的类型是std::pair。因此,对于map这种关联性容器而言,需要使用val.f?irst或val.second来提取键值。
2)auto自动推导出的类型是容器中的value_type,而不是迭代器。
关于上述第二点,我们再来看一个对比的例子:
std::map<std::string, int> mm = { { "1", 1 }, { "2", 2 }, { "3", 3 } }; for(auto ite = mm.begin(); ite != mm.end(); ++ite) { std::cout << ite->f?irst << " -> " << ite->second << std::endl; } for(auto& val : mm) // 使用基于范围的for循环 { std::cout << val.f?irst << " -> " << val.second << std::endl; }
从这里就可以很清晰地看出,在基于范围的for循环中每次迭代时使用的类型和普通for循环有何不同。
在使用基于范围的for循环时,还需要注意容器本身的一些约束。比如下面这个例子:
#include <iostream> #include <set> int main(void) { std::set<int> ss = { 1, 2, 3 }; for(auto& val : ss) { // error: increment of read-only reference 'val' std::cout << val++ << std::endl; } return 0; }
例子中使用auto&定义了std::set<int>中元素的引用,希望能够在循环中对set的值进行修改,但std::set的内部元素是只读的——这是由std::set的特征决定的,因此,for循环中的auto&会被推导为const int &。
同样的细节也会出现在std::map的遍历中。基于范围的for循环中的std::pair引用,是不能够修改f?irst的。
接下来,看看基于范围的for循环对容器的访问频率。看下面这段代码:
#include <iostream> #include <vector> std::vector<int> arr = { 1, 2, 3, 4, 5 }; std::vector<int>& get_range(void) { std::cout << "get_range ->: " << std::endl; return arr; } int main(void) { for(auto val : get_range()) { std::cout << val << std::endl; } return 0; } 输出结果: get_range ->: 1 2 3 4 5
从上面的结果中可以看到,不论基于范围的for循环迭代了多少次,get_range()只在第一次迭代之前被调用。
因此,对于基于范围的for循环而言,冒号后面的表达式只会被执行一次。
最后,让我们看看在基于范围的for循环迭代时修改容器会出现什么情况。比如,下面这段代码:
#include <iostream> #include <vector> int main(void) { std::vector<int>arr = { 1, 2, 3, 4, 5 }; for(auto val : arr) { std::cout << val << std::endl; arr.push_back(0); // 扩大容器 } return 0; } 执行结果(32位mingw4.8): 1 5189584 -17891602 -17891602 -17891602
若把上面的vector换成list,结果又将发生变化。
这是因为基于范围的for循环其实是普通for循环的语法糖,因此,同普通的for循环一样,在迭代时修改容器很可能会引起迭代器失效,导致一些意料之外的结果。由于在这里我们是看不到迭代器的,因此,直接分析对基于范围的for循环中的容器修改会造成什么样的影响是比较困难的。
其实对于上面的基于范围的for循环而言,等价的普通for循环如下:
#include <iostream> #include <vector> int main(void) { std::vector<int> arr = { 1, 2, 3, 4, 5 }; auto && __range = (arr); for (auto __begin = __range.begin(), __end = __range.end(); __begin != __end; ++__begin) { auto val = *__begin; std::cout << val << std::endl; arr.push_back(0); // 扩大容器 } return 0; }
从这里可以很清晰地看到,和我们平时写的容器遍历不同,基于范围的for循环倾向于在循环开始之前确定好迭代的范围,而不是在每次迭代之前都去调用一次arr.end()。
当然,良好的编程习惯是尽量不要在迭代过程中修改迭代的容器。但是实际情况要求我们不得不这样做的时候,通过理解基于范围的for循环的这个特点,就可以方便地分析每次迭代的结果,提前避免算法的错误。