C++程序员应了解的那些事(36)Effective STL第6条:当心C++编译器中最烦人的分析机制 --- 调用构造函数被误认为是函数声明的问题
<示例代码导入-1>
#include <iostream>
using namespace std;
struct Calculate
{
Calculate(int& i);
int value;
};
struct Gadget
{
Gadget(Calculate& clc);
Calculate clc;
};
Calculate::Calculate(int& i)
{
i = i*100;
std::cout << i << std::endl;
}
Gadget::Gadget(Calculate& clc):clc(clc){}
void fun(int& i)
{
//warning:parentheses were disambiguated as a function declaration
Gadget g(Calculate(i));//变量定义?or 函数声明?
}
int main()
{
int i = 3;
fun(i);
std::cout << i << endl;
}
上述代码运行时:Gadget g(Calculate(i))被解释为一个函数声明,构造函数Calculate::Calculate(int& i)并没有被调用,所以main()中打印 i =3。
修改为Gadget g{Calculate(i)}后,可以保证该条语句被解释为调用构造函数声明一个变量。
********************************* 问题分析 ***********************************
C++是较为底层的面相对象语言,在底层的语法规则分析中,有很多隐藏的分析机制。
(1)C++中的普遍规律,即尽可能地解释为函数声明。
(2)把形式参数的声明用括号括起来是非法的,但给函数参数加上括号却是合法的,所以通过增加一对括号,我们强迫编译器按我们的方式来工作。
<演示说明①>
假设我们有一个存放整型(int)的文件,你想把这些整数拷贝到一个list中,那么你可能会使用下面的做法:
std::ifstream dataFile("ints.dat");
//使用list的区间构造函数来初始化list(C++编译器可能会识别错误)
list<int> data(istream_iterator<int>(dataFile), istream_iterator<int>());
这种做法的思路是, 把一对std::istream_iterator传入std::list的区间构造函数中, 从而把文件的整数复制到std::list中去。
这段代码可以通过编译, 但是运行时它什么也不会做, 它不会从文件中读去任何数据, 不会创建std::list, 这是因为第二条语句并没有定义一个std::list对象, 也没有调用构造函数。
这段代码的本义:创 建一个std::list< int >对象data, 构造该对象的方法是: 把一对std::istream_iterator传入std::list< int >的区间构造函数中, 从而把文件中的整数复制到std::list< int >中去。
但是编译器其实是这么理解的:这里声明了一个函数data(), 其返回值为std::list< int >, 这个_Data()函数有如下两个参数:第一个参数的名称是dataFile,,它的类型是std::istream_iterator< int >, dataFile两边的括号是多余的, 会被忽略。第二个参数没有名称,它的类型是一个“不带任何参数并返回一个std::istream_iterator< int >的函数”的指针。
蛋碎!!!!!!但是这个解释却与C++中的一条普通规律相符: 尽可能的解释为函数声明。
- C++编译器可能会上面list的构造函数理解为一种函数:包含两个参数和一个list<int>返回值。
- 因为编译器可能会把上面那种形式的构造函数理解为一种函数,因此上面的data不会做任何事情,因此data为空。
<演示说明②>
- 下面的演示案例也介绍了C++的分析机制可能会把构造函数误认为函数!!!
- 编译器可能会把下面的对象构造语句误认为:函数名为w,无参数,返回值为Widget类型的函数。
class Widget
{
//假设Widget使用默认构造函数
};
int main()
{
//我们原本是想使用Widget的默认构造函数,但是可能会被误认为函数
Widget w();
}
<解决办法①>
- 多增加一对括号来跳过编译器的分析机制!(将形参的声明用括号括起来是非法的,但给函数参数加上括号却是合法的)
- 下面对list构造函数的第一个参数增加了一对括号:
std::ifstream dataFile("ints.dat");
list<int> data((istream_iterator<int>(dataFile)), istream_iterator<int>());
不同编译器的分析机制不同,不同的编译器对此种解析办法的分析可能会不一致,因此你还需要下面的解决办法②。
<解决办法②>:根据上面的代码,我们可以为istream_interator迭代器取名(而不是使用匿名)
std::ifstream dataFile("ints.dat");
istream_iterator<int> dataBegin(dataFile);
istream_iterator<int> dataEnd;
list<int> data(dataBegin, dataEnd);
说明:
<1>实际编程中都建议使用匿名的istream_interator对象,但是此处为了跳过编译器的分析机制,这种代价是值得的。
<2>使用临时命名迭代器来分步完成需求,虽然这样做与标准STL使用有点违背了,但是为了没有二义性和提高代码可读性和方便维护是比较提倡的。
<解决办法③> ※使用大括号“{}”初始化
现在C++标准已经提供了使用大括号“{}”来初始化,因此上面的所有问题都可以得到解决。
<注>:C++语法分析机制
下面这行代码声明了一个带double参数并返回int的函数:
int _Func( double _InDouble );
下面这行代码做了同样的事情,参数_InDouble两边的括号是多余的, 会被忽略:
int _Func( double ( _InDouble ) );
下面这行代码声明了同样的函数, 只是它省略了参数名称:
int _Func( double );
再来看三个函数声明, 第一个声明了一个函数_Fuck(), 他的参数是一个不接收任何参数并返回一个double类型的函数的指针:
int _Fuck( double( *_ptrFunc ) () );
有另外一种方式可以表明同样的意思, 唯一的区别是, _ptrFunc用非指针的形式来声明(这种形式在C语言和C++中都有效):
int _Fuck( double _ptrFunc() );
跟通常一样, 参数名称可以忽略, 因此下面是_Fuck()的第三种声明, 其中参数名_ptrFunc被省略了:
int _Fuck( double () );
☆☆请注意围绕参数名的括号(比如_Func()的第二个声明中的_InDouble)与独立的括号的区别, 围绕参数名的括号被忽略, 而独立的括号则表明参数列表的存在: 它们说明存在一个函数指针的参数。
参考链接: