Effective STL 笔记: Item 6--Be alert for C++'s most vexing parse
假设有个文件里面记录的一系列的 int 值,现在我们想把这些数值存到一个 List 里面,结合 Item 5, 我们可能会写出下面的代码:
ifstream dataFile("ints.data"); list<int> data(istream_iterator<int>(dataFile), // Start of iterator istream_iterator()); // End of iterator
这段代码可以编译,但运行时并不工作,它不会去调用 list 的构造函数,从而不会生成我们想要的这个 List。
问题,出在 C++ 对代码的解析上。
假设我们需要声明一个函数,该函数接受 double 类型参数并返回 int 类型,C++ 里面,下面三种方法是等效的:
1: int f(double d); // Old C style. 2: int f(double(d)); // Function style casts. 3: int f(double); // Same as first but skip parameter.
如果我们要声明另外一个函数,该函数同样返回 int,但接受的参数是一个无参数但返回 double 的函数的指针,则下面的声明是等效的:
4: int g(double (*pf)()); // g takes a pinter to a function as paramter. 5: int g(double pf()); // same as above 6: int g(double ()); // same as above, but parameter (function name) skipped.
观察 1 ~ 6 我们可以看到“ 括号”在不同位置时候的不同作用:
- 参数 周围的括号可以被忽略
- 单独 的括号实际上意味着这是一个函数指针的参数列表!
了解了这个区别之后再返回来看最开始的那个声明:
list<int> data(istream_iterator<int>(dataFile), istream_iterator<int>());
这个声明定义了一个返回 list<int> 的函数,该函数接受两个参数:
- 第一个参数名为 dataFile,类型为 istream_iterator<int>,dataFile 两遍的括号可以忽略。
- 第二个参数是一个函数指针,该函数不接受参数但返回一个 istream_iterator<int>。
这个和我们最开始想象的完全不一样,而产生分歧的原因就在于 C++ 对代码的解析上:
只要表达式可以被解析成 函数 ,那么该表达式 就会被编译器解析成函数 !
想象一下下面这段代码,相信很多人都写出来过,但是它能编译么?
7: class Widget 8: { 9: public: 10: Widget(){} 11: virtual ~Widget(){} 12: void Show(){} 13: }; 14: 15: Widget w(); 16: w.Show();
上面片段的第 8 行实际上不是声明了一个 Widget 对象,而是声明了一个用来返回 Widget 对象的函数,第 9 行自然也就出错了。
理解了上面的内容,也就可以想想怎么解决开始时提出的问题了:给形参声明加上括号不合法,但给函数调用的实参加括号是合法的,通过适当的添加括号,问题得以解决:
1: list<int> data((istream_iterator<int>(dataFile)), 2: istream_iterator<int>());
这里第一个参数周围添加了多余的括号,假设编译器仍然认为 data() 是一个函数声明,则第一个形参周围就被添加了括号,这是一个非法的行为,所以编译器会丢掉这种可能,转而匹配下一种可能得匹配,认为该表达式声明了一个 list<data> 的变量,并调用适当的区间函数(Item 5)来进行初始化。
但并非所有的编译器都支持这种匿名对象,如果编译器不支持,我们需要下面的这种显示的写法:
1: ifstream dataFile("ints.dat"); 2: istream_iterator<int> dataBegin(dataFile); 3: istream_iterator<int> dataEnd; 4: list<int> data(dataBegin, dataEnd);
这种写法应该通用。
(使用许可:署名-非商业性使用-相同方式共享 3.0 中国大陆许可协议 。)