Cpp Most vexing parse
学习整理一下关于 C++ Most vexing parse 的内容,主要参考wiki。
Most vexing parse(最令人烦恼的解析)指的是 C/C++编译器在处理某些特殊语句时,既可以理解为函数声明,也可以理解为对象初始化的情况,这两种情况都满足 C/C++的语法标准,但是编译器无法区分,导致的解析困难。
C++编译器对此的做法是:遇到歧义时倾向于视作函数声明,并且在 C++11 之后为对象初始化提供了新的花括号初始化语法。花括号初始化语法同时又带来了更多的语法困难,进一步加剧了语法的复杂度。
C 语言
这个问题的来源是 C 语言在声明函数时接受比较宽松的语法,而 C++也兼容了这部分语法。
例一
第一个例子,下面的几个函数声明是一样的
1 | int f(double); // (1.1) |
它们都表示f
是一个接收一个double
参数,返回值类型为int
的函数,这里允许对参数t
加上括号。
注意到(1.3)在语法上是有歧义的:
- 解释一:声明
f
是一个接收一个double
参数,返回值类型为int
的函数; - 解释二:将变量
t
转换类型为double
,然后用来初始化int
类型的变量f
。
那么含义到底是什么?C 语言的编译器会倾向于视作函数声明。如果希望语句表达第二种初始化的效果,可以改为如下的几种形式
1 | int f = double(t); // (1.4) |
(1.4)加了等号显然没有歧义,(1.5)再加一个括号,此时编译器也不会视作函数声明,(1.6)则是使用更明确的类型转换。
代码提示工具对于(1.3)可能会识别出歧义并警告"Parentheses were disambiguated as a function declaration",消歧义建议通常是(1.5)加括号,也可能不会警告。
例二
第二个例子,下面的几个函数声明是一样的
1 | int g(double(*p)()); // (2.1) |
(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 |
|
这段代码的意思很明确:定义一个类型为Data
的变量d
,用于初始化DataUpdater
类型的变量p
,然后调用p.update()
接口进行Data
的更新,同时返回Data
的
const 引用,用于读取数据。运行结果为
1 | cnt = 1 |
上文的写法没有任何语法问题,没有任何歧义,但是我们注意到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 | DataUpdater data_updater(Data{}); // (3.3) |
modern C++的建议:尽可能使用花括号来初始化变量,含有花括号的表达式不可能被视作函数声明;尽可能使用 C++风格的类型转换(例如 static_cast),而不是 C 风格的类型转换,后者非常隐蔽难以检查,容易导致错误。