第6条:当心 C++(www.cppentry.com)编译器最烦人的分析机制。
假设你有一个存有整数( int)的文件,想把这些整数复制到一个 list中。下面是很合理的一种做法:
- ifstream dataFile("ints.dat");
- list<int> data(istream_iterator<int>(dataFile), //小心!结果不会是
- istream_iterator<int>()); //你所想象的那样
这种做法的思路是,把一对 istream_iterator传入到 list的区间构造函数中(见第 5条),从而把文件中的整数复制到 list中。
这段代码可以通过编译,但是在运行时,它什么也不会做。它不会从文件中读取任何数据,它不会创建 list。这是因为第二条语句并没有声明一个 list,也没有调用构造函数。它所做的是……喔,它所做的事情很奇怪,我不敢直接告诉你,因为你不会相信的。我得详细解释一下,一点点地解释。你坐下了吗?如果还没有,可能你得找一把椅子……
我们从最基本的说起。下面这行代码声明了一个带 double参数并返回 int的函数:
- int f(double d);
下面这行代码做了同样的事情。参数 d两边的括号是多余的,会被忽略: int f(double (d)); //同上;d两边的括号被忽略下面这行代码声明了同样的函数。只是它省略了参数名称: int f(double); //同上;参数名被忽略
这三种形式的声明你应当很熟悉,尽管以前你可能不知道可以给参数名加上圆括号(我也是不久前才知道的)。现在让我们再看三个函数声明。第一个声明了一个函数 g,它的参数是一个指向不带任何参数的函数的指针,该函数返回 double值:
int g(double(*pf)()); //g以指向函数的指针为参数有另外一种方式可表明同样的意思。唯一的区别是, pf用非指针的形式来声明(这种形式在 C和 C++(www.cppentry.com)中都有效):
int g(double pf()); //同上;pf为隐式指针跟通常一样,参数名称可以省略,因此下面是 g的第三种声明,其中参数名 pf被省略了: int g(double ()); //同上;省去参数名
请注意围绕参数名的括号(比如对 f的第二个声明中的 d)与独立的括号的区别。围绕参数名的括号被忽略,而独立的括号则表明参数列表的存在;它们说明存在一个函数指针参数。
在熟悉了对 f和 g的声明后,我们开始研究本条款开始时提出的问题。它是这样的:
- list<int> data(istream_iterator<int>(dataFile), istream_iterator<int>());
请你注意。这里声明了一个函数,data,其返回值是 list<int>。这个 data函数有如下两个参数。
第一个参数的名称是 dataFile。它的类型是 istream_iterator<int>。dataFile两边的括号是多余的,会被忽略。
第二个参数没有名称。它的类型是指向不带参数的函数的指针,该函数返回一个 istream_iterator<int>。
这令人吃惊,对吧?但它却与 C++(www.cppentry.com)中的一条普遍规律相符,即尽可能地解释为函数声明。如果你用 C++(www.cppentry.com)编程(www.cppentry.com)已经有一段时间了,你几乎肯定遇到过该规律的另一种表现形式。你曾经多少次见到过下面这种错误?
- class Widget{...}; //假定 Widget有默认构造函数 Widget w(); //哦…
它没有声明名为 w的 Widget,而是声明了一个名为 w的函数,该函数不带任何参数,并返回一个 Widget。学会识别这一类言不达意是成为 C++(www.cppentry.com)程序员的必经之路。
所有这些都很有意思(通过它自己的歪曲的方式),但这并不能帮助我们做自己想做的事情。我们想用文件的内容初始化 list<int>对象。现在我们已经知道必须绕过某一种分析机制,剩下的事情就简单了。把形式参数的声明用括号括起来是非法的,但给函数参数加上括号却是合法的,所以通过增加一对括号,我们强迫编译器按我们的方式来工作:
- list<int> data((istream_iterator<int>(dataFile)), //注意 list构造函数的
- istream_iterator<int>()); //第一参数两边的括号
这是声明 data的正确方式,在使用 istream_iterator和区间构造函数时(同样,见第 5条),注意到这一点是有益的。
不幸的是,并不是所有的编译器都知道这一点。在我测试过的几种编译器中,几乎有一半拒绝接受 data的上述声明方式,除非它被错误地用不带括号的形式来声明。为了满足这类编译器,你可以瞪大眼睛,使用我已经费了半天劲解释的那种不正确的形式,但这是不可移植的、短视的做法。毕竟,现在分析错误的编译器将来会更正的,对吧?(当然!)
更好的方式是在对 data的声明中避免使用匿名的 istream_iterator对象(尽管使用匿名对象是一种趋势),而是给这些迭代器一个名称。下面的代码应该总是可以工作的:
- ifstream dataFile("ints.dat");
- istream_iterator<int> dataBegin(dataFile);
- istream_iterator<int> dataEnd;
- list<int> data(dataBegin, dataEnd);
使用命名的迭代器对象与通常的 STL程序风格相违背,但你或许觉得为了使代码对所有编译器都没有二义性,并且使维护代码的人理解起来更容易,这一代价是值得的。