首先贴两则旧闻:一个是上个月底,IS0 C++委员会正式批准了C++编程语言国际标准最终草案(FDIS)。

标准本身已经完成,接下来将是根据委员会会议修改意见更新工作草案,预计将用三周时间完成FDIS草案,然后交给日内瓦的ITTF,最新的C++标准将在夏天发布,先前被临时命名为C++0x的新标准将被称为C++ 2011。从2003年发布的C++03到2011年的C++ 2011,新标准的制定历经了8年时间。GCC和Visual C++编译器都已加入了C++2011/C++0x的支持。

另一个则是紧接着在3月25日,GCC 4.6.0发布。

GNU项目和GCC开发者正式宣布发布GNU编译器4.6.0版本。 GCC 4.6.0的新特性包括:支持Go语言,改进C++0x支持,可伸缩全程序优化器已能可靠使用,新的-Ofast选项, 无效命令行的严格检查,改进编译时间和内存占用,等等。

而这其中正包括了倍受期待的foreach了,在C++0x,它的正式名字是Range-based for-loop

熟悉一些比较高级的语言的童鞋们一定不会对foreach感到陌生,甚至习惯了foreach带来的便利。不用说那些灵活的脚本语言,就连打死也不愿意支持运算符重载的Java也在1.5版本后开始支持foreach了:

public class Foreach {
	public static void main(String[] args) {
		int[] a = new int[]{2, 3, 5, 7, 11, 13, 17, 19};
		for (int i : a) {
			System.out.print(i + " ");
		}
	}
};

有关C++0x中要加入foreach早有耳闻,也见过一些样例,不过一直没动手尝试过。事实上C++0x的foreach有着和Java类似的语法。

#include <cstdio>

int main() {
	int p[8] = {2, 3, 5, 7, 11, 13, 17, 19};
	for (int i: p) {
		printf("%d ", i);
	}
	return 0;
}

当然,C++0x引入的foreach也只不过是一个语法糖,它虽然不能让C++更强大,但无疑会让我们写代码时舒服很多,试比较:

#include <cstdio>
#include <vector>

using std::vector;

int main() {
	const vector<int> v = {2, 3, 5, 7, 11, 13, 17, 19};
	for (vector<int>::const_iterator it = v.begin(); it != v.end(); ++it) {
		printf("%d ", *it);
	}
	puts("");
	for (auto i: v) {
		printf("%d ", i);
	}
	return 0;
}

这两个完成相同功能的for循孰优孰劣,一目了然。顺便提一下:auto是C++0x中的一个新关键字,用于编译期的自动类型推断,在这里,它用来代替int。而能够像第7行这样初始化一个vector<int>,也是得益于C++0x引入的一个新特性std::initializer_list

于是很容易要问,需要满足什么条件才能在C++中使用foreach,如何为自己的类型提供foreach。很容易想到,类似C#和Java中的Enumerator,C++中隐藏在背后的可能是begin, enditerator。事实是怎样的呢?用文档说话。在最新的C++标准草案中,range-based for-loop的语法如下:

for (for-range-declaration : expression) statement
for (for-range-declaration : braced-init-list) statement

它等价于:

{
	auto&& __range = range-init;
	for (auto __begin = begin-expr, __end = end-expr; __begin != __end; ++__begin) {
		for-range-declaration = *__begin;
		statement
	}
}

上面的&&又是C++0x的一个新feature——Rvalue-reference(右值引用)range-init就是前面的expressionbraced-init-list。而begin-exprend-expr服从如下规则:

  1. 如果_RangeT是数组类型,那么begin-exprend-expr分别是__range__range + __bound。这要求编译期数组的类型和大小是确定的,比如下面的代码会编译错误:
  2. #include <cstdio>
    
    int main() {
    	int n;
    	scanf("%d", &n);
    	int a[n];
    	for (int& i: a) {
    		i = 0;
    	}
    	return 0;
    }
    
  3. 如果_RangeT是一个类,并有beginend方法,那么begin-exprend-expr分别是__range.begin()__range.end()
  4. 否则,begin-exprend-expr分别是begin(__range)end(__range)

在实现上,似乎前两种情况都转为了第三种处理,即转为对begin(__range)end(__range)这两个函数的调用。在libstdc++的源代码中可以看到,对于数组有:

// <range_access.h>
template<class _Tp , size_t _Nm> _Tp * begin (_Tp(&__arr)[_Nm]) { return __arr; }
template<class _Tp , size_t _Nm> _Tp * end (_Tp(&__arr)[_Nm]) { return __arr + _Nm; }

对于容器有:

// <range_access.h>
template<class _Container > auto begin (const _Container &__cont)-> decltype(__cont.begin()) { return __cont.begin(); }
template<class _Container > auto end (const _Container &__cont)-> decltype(__cont.end()) { return __cont.end(); }

template<class _Container > auto begin (_Container &__cont)-> decltype(__cont.begin()) { return __cont.begin(); }
template<class _Container > auto end (_Container &__cont)-> decltype(__cont.end()) { return __cont.end(); }

对于braced-init-list,其实是作为std::initializer_list处理:

// <initializer_list>
template<class _Tp > constexpr const _Tp * begin (initializer_list< _Tp > __ils) { return __ils.begin(); }
template<class _Tp > constexpr const _Tp * end (initializer_list< _Tp > __ils) { return __ils.end(); }

此外还有std::valarray的模板特化版本:

// <valarray>
template<class _Tp > _Tp * begin (valarray< _Tp > &__va) { return std::__addressof(__va[0]); }
template<class _Tp > _Tp * end (valarray< _Tp > &__va) { return std::__addressof(__va[0]); }

template<class _Tp > const _Tp * begin (const valarray< _Tp > &__va) { return std::__addressof(__va[0]) + __va.size(); }
template<class _Tp > const _Tp * end (const valarray< _Tp > &__va) { return std::__addressof(__va[0]) + __va.size(); }

于是我们便可以直接对这些类型使用range-based for-loop了,下面的例子说明了这一点:

#include <map>
#include <cctype>
#include <cstdio>
#include <string>
#include <valarray>

using namespace std;

int main() {
	for (char c: "heLLo ") {
		putchar(toupper(c));
	}
	for (string s: {" WO", "RLD", "!\n"}) {
		printf("%s", s.c_str());
	}

	valarray<int> a = {0, 1, 2, 3, 4};
	for (auto& i: a) {
		i *= 2;
		++i;
	}
	for (const int i: a) {
		printf("%d ", i);
	}
	puts("");

	map<int, long long> m = {{2, 3}, {3, 5}, {4, 7}, {5, 11}, {1, 2}};
	for (auto i: m) {
		printf("%d:%lld ", i.first, i.second);
	}
	puts("");

	return 0;
}

/*
HELLO  WORLD!
1 3 5 7 9
1:2 2:3 3:5 4:7 5:11
*/

当然,根据前面的规则和上面已有的模板函数,我们也能创造自己的foreach。比如下面的代码展示了如何利用规则3,用一个伪迭代器和begin, end函数来实现python中的xrange(stop)循环:

// #!python
// for i in xrange(n):
//   print i,
#include <cstdio>

struct __i {
	int i;
	__i(int i) : i(i) { }
	int operator*() { return i; }
	__i& operator++() { ++i; return *this; }
	bool operator!=(const __i& j) const { return i < j.i; }
};

namespace std {
	__i begin(int n) {
		return __i(0);
	}

	__i end(int n) {
		return __i(n);
	}
}

int main() {
	int n;

	scanf("%d", &n);
	for (int i: n) {
		printf("%d ", i);
	}

	return 0;
}

更简单的办法是利用规则2,通过我们自己的类来模拟python中的xrange(start, stop)循环:

// #!python
// for i in xrange(a, b):
//   print i,
#include <cstdio>
#include <iterator>	// begin, end

using namespace std;

template<typename T>
struct range {
	T a, b;
	range(T a, T b) : a(a), b(b) { }
	T operator*() const { return a; }
	range& operator++() { ++a; return *this; }
	bool operator!=(const range& r) const { return a < b; }
	range begin() { return *this; }
	range end() { return *this; }
};

int main() {
	int a, b;

	scanf("%d%d", &a, &b);
	for (int i: range<int>(a, b)) {
		printf("%d ", i);
	}

	return 0;
}

再加上稍许的改动,就可以模拟python中的xrange(start, stop, step)循环了。

C++0x中的Range-based for-loop确实对用户来说是个好东东,不过似乎对开发人员来说还不是那么友好。我是指相比C#提供了强大的yeild return,C++0x要比较好的提供自己的foreach还是要花点代码量的,而且往往要写一个新的类/迭代器。不过有了foreach以后,就再也不用担心一个遍历容器的for循环还要折三行写了>___<

9 Responses to “C++0x — Range-based for-loop (foreach) 试用心得”
  1. owensss says:

    现在回过头来看感觉基本能看懂 && 赞同不会把语言变得更麻烦的观点~

  2. owensss says:

    学长有 ISO C++ 0x 的 pdf吗?网上一片2003版-. -

  3. xpycc says:

    赞一个先~

    其实一直在刷 bs 的 c++0x faq 页,却一直没有更新,还以为什么事都没发生呢。。。

    语言级别的 foreach 终于整合在 gcc 里了,看来新标准真是离我们越来越近了哈。

    刚看了下,nullptr 也加进来了,终于可以告别 NULL 了,呵呵~

    话说,到时候新标准出了,新的 icc 编译器应该也快了吧~

  4. Navi says:

    虽然这几个特性不错… 但还是隐隐感到c++越来越疼了… 顺便右值引用挺神奇的

    • watashi says:

      不觉得越来越疼啊~
      很多新特性都很好啊,或者有些特性原来没有才比较疼吧……

      • owensss says:

        >_< 这不是给初学者添麻烦嘛

        • watashi says:

          我不这么认为……一个强大友好的语言本来就应该有很多特性和语法糖,初学者有必要都了解么……即便是python这个经常被举例的“简单”语言的代表也有很多有趣而不广泛为人所知的特性……
          只有以“应试”的角度来看这些向前兼容的新特性才可能成为“麻烦”,那大可放心了,20年内这些东西都不可能出现在天朝的教科书里
          作为user来说,这些新特性只会带来遍历才是;而且有些特性是为了弥补原先的缺陷而添加的,更像是一个bug的补丁

  5. fancy says:

    赞shi哥技术帖!

  6.  
Leave a Reply