当心C++编译器最烦人的分析机制

假设有一个存储整数的文件, 想把这些整数复制到一个std::list中去, 下面是一个很合理的做法,

 

std::ifstream _DataFile( "/home/Tong_G/Test.txt" );

 

std::list< int > _Data( std::istream_iterator< int >( _DataFile ),

                        std::istream_iterator< int >() );

 

这种做法的思路是, 把一对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< int > _Data( std::istream_iterator< int >( _DataFile ),

                        std::istream_iterator< int >() );

 

这两行代码究竟做了什么? 再重复一下写这段代码时我们的本义:

创 建一个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 {};

int main( int _Argc,  char* *_Argv )

    {  

    _Widget _w();

#

    return 0;

    }

 

好像是我们使用_Widget的默认构造函数构建了一个名为_w的_Widget对象, 但是实际上编译器会把它理解为: 声明了一个名为_w的函数, 该函数不接受任何类型的参数并且返回一个_Widget类型的对象.

 

这种歧义, 最新版的clang编译器能够察觉, 并抛出警告:

 

[Tong_G@localhost C++ Projects]$ clang++ -Wall -std=c++11 main.cxx

main.cxx:14:15: warning: empty parentheses interpreted as a function

      declaration [-Wvexing-parse]

    _Widget _w();

              ^~

main.cxx:14:15: note: remove parentheses to declare a variable

    _Widget _w();

              ^~

1 warning generated.

[Tong_G@localhost C++ Projects]$

 

这个warning很明确的告诉我们: main.cxx文件的第14行的代码中的空括号被解释为一个函数定义, 但是使用GCC的小伙伴们你们就没有那么幸运了(阴笑).

 

 

事实上, 最开始的那段代码在我的机器上使用clang 3.3编译的时候, clang同样会抛出了一个warning:

 

[Tong_G@localhost C++ Projects]$ clang++ -Wall -std=c++11 main.cxx

main.cxx:14:27: warning: parentheses were disambiguated as a function

      declaration [-Wvexing-parse]

    std::list< int > _Data( std::istream_iterator< int >( _DataFile ),

                          ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

main.cxx:14:29: note: add a pair of parentheses to declare a variable

    std::list< int > _Data( std::istream_iterator< int >( _DataFile ),

                            ^

                            (                                        )

1 warning generated.

[Tong_G@localhost C++ Projects]$

但是在GCC 4.8编译的时候, 在打开-Wall开关的情况下也没有收到任何警告, Visual C++没试过...

 

所有这些都很有意思(通过它自己的歪曲方式), 但是这并不能帮助我们做自己想做的事情: 我们想用文件的内容初始化std::list< int >对象.

现在我们已经知道必须绕过某一种分析机制, 剩下的事情就简单了, 把形式参数的声明用括号括起来是非法的 , 但是给函数参数加上括号却是合法的, 所以通过增加一队括号, 我们可以强迫编译器按照我们的方式来工作:

std::list< int > _Data( ( std::istream_iterator< int >( _DataFile ) ),

                        std::istream_iterator< int >() );

 

这是声明_Data的正确方式, 在使用std::iterator_iterator和区间构造函数时, 注意到这一点是有益的.

 

不幸的是, 并不是所有编译器都支持这一点, 为了满足所有的编译器, 我们可以使用一种更好的方式来绕开这种分析机制: 更好的方式是再对_Data的声明中避免 使用匿名的std::istream_iterator对象(尽管匿名对象是一种趋势), 而是给这些迭代器一个名称, 下面的代码应该是总可以工作的:

 

std::ifstream _DataFile( "/home/Tong_G/Test.txt" );

std::istream_iterator< int > _DataBegin( _DataFile );

std::istream_iterator< int > _DataEnd;

std::list< int > _Data( _DataBegin, _DataEnd );

 

这段代码成功的读取了我的位于/home/Tong_G文件夹中Test.txt文件中的内容, 检测一下:

 

for ( const int& _Elem : _Data )

    std::cout << _Elem << "  ";

std::cout << std::endl;

 

 

运行结果是:

[Tong_G@localhost C++ Projects]$ clang++ -Wall -std=c++11 main.cxx

[Tong_G@localhost C++ Projects]$ ./a.out

11  22  33  44  55  6  775  3245  234 

[Tong_G@localhost C++ Projects]$

 

 

 

使用命名的迭代器与通常的 STL 程序猿的风格相违背 , 但是为了使代码对所有的编译器都没有二义性 , 并且使维护代码的人理解起来更容易 , 这一代价是值得的 .

你可能感兴趣的:(C++,STL,C++11)