c++11 Range-based 循环语法
大多数语言都支持 for-each 语法遍历一个数组或集合中的元素,在 C++ 98/03 规范中,对于一个数组 int arr[10],如果我们想要遍历这个数组,只能使用递增的计数去引用数组中每个元素:
int arr[10] = {0};
for (int i = 0; i < 10; ++i)
{
std::cout << arr[i] << std::endl;
}
在 C++ 11 规范中有了 for-each 语法,我们可以这么写:
int arr[10] = {0};
for (int i : arr)
{
std::cout << i << std::endl;
}
对于 stl 容器 std::map,我们也可以使用这种语法:
std::map<std::string, std::string> seasons;
seasons["spring"] = "123";
seasons["summer"] = "456";
seasons["autumn"] = "789";
seasons["winter"] = "101112";
for (auto iter : seasons)
{
std::cout << iter.second << std::endl;
}
for-each 语法虽然很强大,但是有两个需要注意的地方:
-
for-each 中的迭代器类型与数组或集合中的元素的类型完全一致,而原来使用老式语法迭代 stl 容器(如 std::map)时,迭代器是类型的取地址类型。因此,在上面的例子中,老式语法中,iter是一个指针类型(std::pair*),使用iter->second 去引用键值;而在 for-each 语法中,iter是数据类型(std::pair),使用 iter.second 直接引用键值。
-
for-each 语法中对于复杂数据类型,迭代器是原始数据的拷贝,而不是原始数据的引用。如果需要修改容器中的元素,必须将将迭代器修改成原始数据的引用,例如:
std::vector<std::string> v; v.push_back("zhang"); v.push_back("li"); v.push_back("wang"); v.push_back("ma"); for (auto iter : v) { iter = "hello"; }我们遍历容器 v,意图将 v 中的元素的值都修改成“hello”,但是实际执行时我们却达不到我们想要的效果。这就是上文说的 for-each 中的迭代器是元素的拷贝,所以这里只是将每次拷贝修改成“hello”,原始数据并不会被修改。我们可以将迭代器修改成原始数据的引用:
std::vector<std::string> v; v.push_back("zhang"); v.push_back("li"); v.push_back("wang"); v.push_back("ma"); for (auto& iter : v) { iter = "hello"; }这样我们就达到修改原始数据的目的了。这一点在使用 for-each 比较容易出错,对于容器中是复杂数据类型,我们尽量使用这种引用原始数据的方式,以避免复杂数据类型不必要的调用构造函数的开销。
for-each 循环的实现原理
上述 for-each 循环可抽象成如下伪码:
for (for-range-declaration : for-range-initializer)
statement;
C++ 14 标准是这样解释上面的公式的:
auto && __range = for-range-initializer;
for ( auto __begin = begin-expr, __end = end-expr; __begin != __end; ++__begin )
{
for-range-declaration = *__begin;
statement;
}
在这个循环中,begin-expr 返回的迭代器 begin 需要支持自增操作,且每次循环时会与 end-expr 返回的迭代器 *_end 做判不等比较,在循环内部,通过调用迭代器的解引用(*) 操作取得实际的元素。这就是上文说的迭代子对象需要支持 operator++、operator != 和 operator* 的原因了。
但是上面的公式中,在一个逗号表达式中 auto __begin = begin-expr, __end = end-expr; 由于只使用了一个类型符号 auto 导致其实迭代器 *_begin 和结束迭代器 *_end 是同一个类型,这样不太灵活,在某些设计中,可能希望循环结束时的迭代子是另外一种类型。
因此到了 C++17 标准时,要求编译器解释 for-each 循环成如下形式:
auto && __range = for-range-initializer;
auto __begin = begin-expr;
auto __end = end-expr;
for ( ; __begin != __end; ++__begin ) {
for-range-declaration = *__begin;
statement;
}
代码行 2 和 3 将获取起始迭代器 *_begin 和结束迭代器 *_end 分开来写,这样这两个迭代器就可以是不同的类型了。虽然类型可以不一样,但这两种类型之间仍然要支持 operator!= 操作。C++17 就 C++14 的这种改变,可以让后续的开发更加灵活。
自定义对象支持 Range-based 循环
下面来看下如何让我们自定义的对象支持 Range-based 循环语法?由以上对Range-based 循环实现原理的分析可知,为了让一个对象支持这种语法,这个对象至少需要实现如下两个方法:
//需要返回第一个迭代子的位置
Iterator begin();
//需要返回最后一个迭代子的下一个位置
Iterator end();
Iterator 类型必须支持如下三种操作:
- operator++ (即自增)操作,即可以自增之后返回下一个迭代子的位置;
- operator != (即判不等操作)操作;
- operator* 即解引用(dereference)操作。
#include <iostream>
#include <string>
template<typename T, size_t N>
class A
{
public:
A()
{
for (size_t i = 0; i < N; ++i)
{
m_elements[i] = i;
}
}
~A()
{
}
T* begin()
{
return m_elements + 0;
}
T* end()
{
return m_elements + N;
}
private:
T m_elements[N];
};
int main()
{
A<int, 10> a;
for (auto iter : a)
{
std::cout << iter << std::endl;
}
return 0;
}
上述代码中,迭代器 Iterator 是 T* 指针类型,本身就支持 operator ++ 和 operator != 操作,所有没有提供这两个方法是实现。
本文深入探讨了C++11引入的Range-based循环语法,对比了传统for循环和新式for-each循环的区别,并详细阐述了其工作原理。特别指出,Range-based循环在处理复杂数据类型时,需要通过引用原始数据避免不必要的拷贝。同时,介绍了C++17对此的改进,使得迭代器类型可以不同,增强了灵活性。最后,讨论了如何使自定义对象支持Range-based循环,需要实现begin()和end()方法,以及迭代器需要支持的操作。
448

被折叠的 条评论
为什么被折叠?



