学习整理一下关于 C++ Most vexing parse 的内容,主要参考wiki

Most vexing parse(最令人烦恼的解析)指的是 C/C++编译器在处理某些特殊语句时,既可以理解为函数声明,也可以理解为对象初始化的情况,这两种情况都满足 C/C++的语法标准,但是编译器无法区分,导致的解析困难。

C++编译器对此的做法是:遇到歧义时倾向于视作函数声明,并且在 C++11 之后为对象初始化提供了新的花括号初始化语法。花括号初始化语法同时又带来了更多的语法困难,进一步加剧了语法的复杂度。

C 语言

这个问题的来源是 C 语言在声明函数时接受比较宽松的语法,而 C++也兼容了这部分语法。

例一

第一个例子,下面的几个函数声明是一样的

1
2
3
4
5
int f(double); // (1.1)

int f(double t); // (1.2)

int f(double (t)); // (1.3)

它们都表示f是一个接收一个double参数,返回值类型为int的函数,这里允许对参数t加上括号。

注意到(1.3)在语法上是有歧义的:

  • 解释一:声明f是一个接收一个double参数,返回值类型为int的函数;
  • 解释二:将变量t转换类型为double,然后用来初始化int类型的变量f

那么含义到底是什么?C 语言的编译器会倾向于视作函数声明。如果希望语句表达第二种初始化的效果,可以改为如下的几种形式

1
2
3
4
5
int f = double(t); // (1.4)

int f((double(t))); // (1.5)

int f((double)t); // (1.6)

(1.4)加了等号显然没有歧义,(1.5)再加一个括号,此时编译器也不会视作函数声明,(1.6)则是使用更明确的类型转换。

代码提示工具对于(1.3)可能会识别出歧义并警告"Parentheses were disambiguated as a function declaration",消歧义建议通常是(1.5)加括号,也可能不会警告。

例二

第二个例子,下面的几个函数声明是一样的

1
2
3
4
5
int g(double(*p)()); // (2.1)

int g(double p()); // (2.2)

int g(double ()); // (2.3)

(2.1)的可读性最好,它表示的是:g是一个接收一个函数指针作为参数,返回值类型为int的函数,作为参数的函数指针p要求类型为:参数列表为空,返回值类型为double

(2.2)是由于退化规则:作为参数声明的函数等价于一个指向同类型函数的指针,因此和(2.1)完全等价。(2.3)和(2.2)一样,只是在声明时缺省了参数的名称。

注意到(2.3)在语法上其实是有歧义的,并且和上一个例子的函数声明也有不同:

  • 解释一:声明g是一个接收一个函数指针为参数,返回值类型为int的函数;(注意和上一个例子的函数声明也有区别)
  • 解释二:初始化一个临时的double变量,然后用来初始化int类型的变量g

C 语言的编译器仍然会倾向于视作函数声明。

C++

这个问题在 C++中更为严重,因为 C++支持面向对象,对于自定义类型的初始化,会产生更多并且更隐蔽的歧义问题。

例三

一个参考 wiki 的完整例子如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>

struct Data {
int cnt = 0;
};

class DataUpdater {
private:
Data m_d;

public:
explicit DataUpdater(Data d) : m_d(d) {}

const Data &update() {
m_d.cnt += 1;
return m_d;
}
};

int main() {
Data d;
DataUpdater p(d);

std::cout << "cnt = " << p.update().cnt << "\n";
std::cout << "cnt = " << p.update().cnt << "\n";
std::cout << "cnt = " << p.update().cnt << "\n";
}

这段代码的意思很明确:定义一个类型为Data的变量d,用于初始化DataUpdater类型的变量p,然后调用p.update()接口进行Data的更新,同时返回Data的 const 引用,用于读取数据。运行结果为

1
2
3
cnt = 1
cnt = 2
cnt = 3

上文的写法没有任何语法问题,没有任何歧义,但是我们注意到Data d这个变量定义似乎是不必要的,它只是用于初始化其它变量,可以改为匿名临时变量的形式,直接在同一行中实现:

1
DataUpdater p(Data()); // (3.1)

然后,我们就再一次地遇见了语法歧义,比 C 语言中出现的更隐蔽。这个语句的歧义如下:

  • 解释一:声明p是一个接收一个函数指针为参数,返回值类型为DataUpdater的函数,作为参数的函数指针要求参数列表为空,返回值类型为Data
  • 解释二:初始化一个临时的Data变量,然后用来初始化DataUpdater类型的变量g

C++的编译器仍然会倾向于视作函数声明,因此这里无法通过编译。

改法有很多,包括和 C 语言一样的加括号或者引入等号

1
DataUpdater p((Data())); // (3.2)

也可以使用 C++11 引入的花括号初始化语法(这里的选择有很多,只要出现花括号就不可能识别为函数声明了)

1
2
3
4
5
6
7
8
9
DataUpdater data_updater(Data{}); // (3.3)

DataUpdater data_updater{Data()}; // (3.4)

DataUpdater data_updater{Data{}}; // (3.5)

DataUpdater data_updater({}); // (3.6)

DataUpdater data_updater{{}}; // (3.7)

modern C++的建议:尽可能使用花括号来初始化变量,含有花括号的表达式不可能被视作函数声明;尽可能使用 C++风格的类型转换(例如 static_cast),而不是 C 风格的类型转换,后者非常隐蔽难以检查,容易导致错误。