假设有一个存储整数的文件, 想把这些整数复制到一个std::list中去, 下面是一个很合理的做法,
std::ifstream_DataFile("/home/Tong_G/Test.txt");
std::list_Data(std::istream_iterator(_DataFile),
std::istream_iterator());
这种做法的思路是, 把一对std::istream_iterator传入std::list的区间构造函数中, 从而把文件的整数复制到std::list中去.
这段代码可以通过编译, 但是运行时它什么也不会做, 它不会从文件中读去任何数据, 不会创建std::list, 这是因为第二条语句并没有声明一个std::list对象, 也没有调用构造函数, 它做了一件很操蛋的事...
但是为什么第二条语句做的事情与我们期望的事情大相径庭呢? 要从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)与独立的括号的区别, 围绕参数名的括号被忽略, 而独立的括号则表明参数列表的存在: 它们说明存在一个函数指针的参数.
在了解了_Func()和_Fuck()的声明后, 我们开始考虑最开始的那段代码的问题:
std::ifstream_DataFile("/home/Tong_G/Test.txt");
std::list_Data(std::istream_iterator(_DataFile),
std::istream_iterator());
这两行代码究竟做了什么? 再重复一下写这段代码时我们的本义:
创 建一个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++编程有一段时间了, 大家应该都遇到过这类错误:
class_Widget{};
intmain(int_Argc,char**_Argv)
{
_Widget_w();
#
return0;
}
好像是我们使用_Widget的默认构造函数构建了一个名为_w的_Widget对象, 但是实际上编译器会把它理解为: 声明了一个名为_w的函数, 该函数不接受任何类型的参数并且返回一个_Widget类型的对象.
这种歧义, 最新版的clang编译器能够察觉, 并抛出警告:
[C++Projects]$clang++-Wall-std=c++11main.cxx
main.cxx:14:15:warning:emptyparenthesesinterpretedasafunction
declaration[-Wvexing-parse]
_Widget_w();
^~
main.cxx:14:15:note:removeparenthesestodeclareavariable
_Widget_w();
^~
1warninggenerated.
[C++Projects]$
这个warning很明确的告诉我们: main.cxx文件的第14行的代码中的空括号被解释为一个函数定义, 但是使用GCC的小伙伴们你们就没有那么幸运了(阴笑).
事实上, 最开始的那段代码在我的机器上使用clang 3.3编译的时候, clang同样会抛出了一个warning:
[C++Projects]$clang++-Wall-std=c++11main.cxx
main.cxx:14:27:warning:parenthesesweredisambiguatedasafunction
declaration[-Wvexing-parse]
std::list_Data(std::istream_iterator(_DataFile),
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
main.cxx:14:29:note:addapairofparenthesestodeclareavariable
std::list_Data(std::istream_iterator(_DataFile),
^
()
1warninggenerated.
[C++Projects]$
但是在GCC 4.8编译的时候,在打开-Wall开关的情况下也没有收到任何警告, Visual C++没试过...
所有这些都很有意思(通过它自己的歪曲方式),但是这并不能帮助我们做自己想做的事情:我们想用文件的内容初始化std::list< int >对象.
现在我们已经知道必须绕过某一种分析机制,剩下的事情就简单了,把形式参数的声明用括号括起来是非法的,但是给函数参数加上括号却是合法的,所以通过增加一队括号,我们可以强迫编译器按照我们的方式来工作:
std::list_Data((std::istream_iterator(_DataFile)),
std::istream_iterator());
这是声明_Data的正确方式,在使用std::iterator_iterator和区间构造函数时,注意到这一点是有益的.
不幸的是,并不是所有编译器都支持这一点,为了满足所有的编译器,我们可以使用一种更好的方式来绕开这种分析机制:更好的方式是再对_Data的声明中避免 使用匿名的std::istream_iterator对象(尽管匿名对象是一种趋势),而是给这些迭代器一个名称,下面的代码应该是总可以工作的:
std::ifstream_DataFile("/home/Tong_G/Test.txt");
std::istream_iterator_DataBegin(_DataFile);
std::istream_iterator_DataEnd;
std::list_Data(_DataBegin,_DataEnd);
这段代码成功的读取了我的位于/home/Tong_G文件夹中Test.txt文件中的内容, 检测一下:
for(constint&_Elem:_Data)
std::cout<<_elem>
std::cout<<:endl>
运行结果是:
C++Projects]$clang++-Wall-std=c++11main.cxx
[C++Projects]$./a.out
112233445567753245234
[C++Projects]$
使用命名的迭代器与通常的
STL
程序猿的风格相违背
,
但是为了使代码对所有的编译器都没有二义性
,
并且使维护代码的人理解起来更容易
,
这一代价是值得的
.