double parenthesis trick

The Most Vexing Parse

#include <iostream>
#include <vector>
#include <iterator>
#include <algorithm>
using namespace std;

int main()
{
vector<int> v(istream_iterator<int>(cin),
istream_iterator<int>()); // 这里编译不通过, 为何?

copy(v.begin(), v.end(), ostream_iterator<int>(cout, "\n"));

return 0;
}

上述问题是由于编译器将 v 理解为一个函数声明. 请看解释:

 

假设你有一个int的文件,你想要把那些int拷贝到一个list中。这看起来像是一个合理的方式:

ifstream dataFile("ints.dat");
list<int> data(istream_iterator<int>(dataFile), // 警告!这完成的并不是像你想象的那样这里的想法是传一对 
// istream_iterator 给 list 的区间构造函数 ,
// 因此把int从文件拷贝到list中.
istream_iterator<int>());  


这段代码可以编译,但在运行时,它什么都没做。它不会从文件中读出任何数据。它甚至不会建立一个list。那是因为第二句并不声明list,而且它也不调用构造函数。其实它做的是……,它做得很奇怪。我不敢直接告诉你,因为你可能不相信我。取而代之的是,我得一点一点展开这个解释。你坐下了吗?如果没有,你可能要找找附近有没有椅子…… 我们会从最基本的开始。

这行声明了一个函数 f 带有一个 double 而且返回一个 int :

  1. int f(double d);          // 第二行作了同样的事情。名为d的参数左右的括号是多余的,被忽略:
  2. int f(double (d));    // 同上;d左右的括号被忽略下面这行声明了同样的函数。它只是省略了参数名
  3. int f(double);             // 同上;参数名被省略你应该很熟悉这三种声明形式了吧,虽然可以把括号放在参数名左右这一点可能比较新。(在不久以前我也觉得它新。)

现在让我们再看看三个函数声明

第一个声明了一个函数 g,它带有一个参数,那个参数是指向一个没有参数、返回double的函数的指针:

  1. int g(double (*pf)());      // g 带有一个指向函数的指针作为参数这是完成同一件事的另一种方式。
  2. int g(double pf());      // 同上;唯一的不同是 pf 使用非指针语法来声明(一个在C和C++中都有效的语法).pf其实是一个指针照常. 
  3. int g(double ());            // 同上;参数名可以省略,所以这是g的第三种声明,去掉了pf这个名字. 参数名省略注意参数名左右的括号(就像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++里的一条通用规则——几乎任何东西都可能被分析成函数声明。如果你用C++编程有一段时间了,你应该会遇到另一个这条规则的表象。有多少次你会看见这个错误?

class Widget {...}; // 假设Widget有默认构造函数

Widget w(); // 嗯哦…… 这并没有声明一个叫做 w 的 Widget它声明了一个叫作 w 的没有参数且返回Widget的函数

 

学会识别这个失言(faux pas)是成为C++程序员的一个真正的通过仪式。

所有这些都很有趣(以它自己的扭曲方式),但它没有帮我们说出我们想要说的,也就是应该用一个文件的内容来初始化一个list<int>对象。现在我们知道了我们必须战胜的解析,那就很容易表示了。用括号包围一个实参的声明是不合法的但用括号包围一个函数调用的观点是合法的,所以通过增加一对括号,我们强迫编译器以我们的方式看事情:

list<int> data((istream_iterator<int>(dataFile)),     // 注意在list构造函数
istream_iterator<int>()); // 的第一个实参左右的新括号.


这是可能的声明数据方法,给予 istream_iterators 的实用性和区间构造函数,值得知道它是怎样完成的。

不幸的是,目前并非所有编译器都知道它。在我测试的几种中,几乎一半拒绝接受数据的声明,除非它错误地接受没有附加括号形式的声明!为了安慰这样的编译器,你可以滚你眼睛并且使用我辛辛苦苦解释的错误的数据声明,但那将不可移植而且目光短浅。毕竟,目前编译器存在的分析错误肯定会在将来被修正,是吧?(肯定的!)

 

一个更好的解决办法是在数据声明中从时髦地使用匿名 istream_iterator 对象后退一步,仅仅给那些迭代器名字。以下代码到哪里都能工作

istream_iterator<int> dataBegin(cin);
istream_iterator<int> dataEnd;
list<int> data(dataBegin, dataEnd);

 

区别 Macro Call 和 Function Call

double parenthesis trick 还可以用作函数的区分.

  • #define Multiline(x)    for (int i = 0; i < x; ++i)  puts("")
  • void Multiline(int x){  for (int i = 0; i < x; ++i)  puts("");  }

两者都可以以

Multiline(10);            // Macro Call.

进行调用, 但实际上, 如果同时存在同名函数, 则会调用宏. 因为宏替换过程是在在 compilation 之前, 所以在运行时, Multiline 早已被替换成相应的语句了.

那么如何避免同名的宏引发的调用覆盖问题呢?

我们可以通过将宏名用 parenthesis 括起来. 因为宏调用函数声明类似, 周围不能有 parenthesis. 然而, 函数调用的时候可以使用 parenthesis 包围.

(Multiline)(10);         // Function Call.


summary

class T
{
public:
void operator()(); // 我们能写出这样的函数
};

上述函数调用实际上是:

T t;

(t)(); // 用颜色区分, 对应上述声明中的颜色. 这就是一个典型的 Function Call

我认为 't' 实际上代表了一个地址.

又如:

void foo()
{}

int main()
{
int a = 0;

(a); // Ok. Only evaluation, do nothing. Variable.
(foo); // Ok. Only evaluation, no function called. Function Name.
(int b); // Error. Declaration.

return 0;
}

总结来说, parenthesis 能够在变量或者标识符(函数名) 的两边出现, 而不能在声明调用两边出现.

posted @ 2012-03-12 14:39  walfud  阅读(736)  评论(2编辑  收藏  举报