最令人烦恼的解析

最令人烦恼的解析most vexing parse)是C++编程语言中的一种反直觉的二义性解析形式。 在一些场景下,编译器无法区分某语句是初始化时某对象的参数,还是声明一个函数时指定参数类型。在这些情况下,编译器将该行解释为函数声明。

出现

术语"most vexing parse" 最初由Scott Meyers用于2001年的书籍 Effective STL中。[1] 尽管在C语言中不常见,但这种现象在 C++ 中很常见,直到C++11推出了统一初始化才得以解决。[2]

示例

C风格强制类型转换

一个例子如下:

void f(double my_dbl) {
  int i(int(my_dbl));
}

上面的第 2 行是有歧义的。一种可能的解释是声明一个变量 i ,初始值通过转换my_dbl 到一个int而来。但是,C 允许在函数参数声明周围使用多余的括号;因此,声明的i实际上等同于以下代码:

// A function named i takes an integer and returns an integer.
int i(int my_dbl);

未命名的临时对象

一个更可能出现的例子是:

struct Timer {};

struct TimeKeeper {
  explicit TimeKeeper(Timer t);
  int get_time();
};

int main() {
  TimeKeeper time_keeper(Timer());
  return time_keeper.get_time();
}

其中

  TimeKeeper time_keeper(Timer());

是有歧义的,它可以被解释为:

  1. 一个变量:定义为类TimeKeeper的变量time_keeper,用类Timer的匿名实例初始化。
  2. 一个函数声明:声明了一个函数time_keeper,返回一个TimeKeeper,有一个(未命名的)参数。参数的类型是一个(指向)不接受输入并返回Timer对象的函数(的指针)[Note 1]

C ++标准采取第二种解释,这与上面的第9行不一致。例如,Clang++警告第9行存在最令人烦恼的解析,并报错:[3]

$ clang++ time_keeper.cc
timekeeper.cc:9:25: warning: parentheses were disambiguated as a function declaration
      [-Wvexing-parse]
  TimeKeeper time_keeper(Timer());
                        ^~~~~~~~~
timekeeper.cc:9:26: note: add a pair of parentheses to declare a variable
  TimeKeeper time_keeper(Timer());
                         ^
                         (      )
timekeeper.cc:10:21: error: member reference base type 'TimeKeeper (Timer (*)())' is not a
      structure or union
  return time_keeper.get_time();
         ~~~~~~~~~~~^~~~~~~~~

解决方案

这些有歧义的声明往往不会被解析为程序员所期望的语句。[4][5] C++ 中的函数类型通常隐藏在typedef之后,并且通常具有显式引用指针限定符。要强制扭转解析的结果,常见做法是换一种不同的对象创建或转换语法。

在类型转换的示例中,有两种替代语法:“C 风格强制类型转换”

// declares a variable of type int
int i((int)my_dbl);

或一个static_cast转换:

int i(static_cast<int>(my_dbl));

在变量声明的示例中,首选方法(自 C++11 起)是统一(大括号)初始化。[6] 这也允许完全省略类型名称:

//Any of the following work:
TimeKeeper time_keeper(Timer{});
TimeKeeper time_keeper{Timer()};
TimeKeeper time_keeper{Timer{}};
TimeKeeper time_keeper(     {});
TimeKeeper time_keeper{     {}};

在 C++11 之前,强制获得预期解释的常用手段是使用额外的括号或拷贝初始化:[5]

TimeKeeper time_keeper( /*Avoid MVP*/ (Timer()) );
TimeKeeper time_keeper = TimeKeeper(Timer());

后一种写法中,拷贝赋值运算符 有可能被编译器优化[7]C++17开始,这种优化受到保证。[8]

注记

  1. ^ 根据C++类型退化规则,作为参数声明的函数等价于一个指向同类型函数的指针。参见C++函数对象的实例

参考资料

  1. ^ Meyers, Scott. Effective STL: 50 Specific Ways to Improve Your Use of the Standard Template Library. Addison-Wesley. 2001. ISBN 0-201-74962-9. 
  2. ^ Coffin, Jerry. c++ - What is the purpose of the Most Vexing Parse?. Stack Overflow. 29 December 2012 [2021-01-17]. (原始内容存档于2021-11-25). 
  3. ^ Lattner, Chris. Amazing Feats of Clang Error Recovery. LLVM Project Blog. The Most Vexing Parse. 5 April 2010 [2021-01-17]. (原始内容存档于26 September 2020). 
  4. ^ DrPizza; Prototyped; wb; euzeka; Simpson, Homer J. C++'s "most vexing parse". ArsTechnica OpenForum. October 2002 [2021-01-17]. (原始内容存档于2021-11-25). 
  5. ^ 5.0 5.1 Boccara, Jonathan. The Most Vexing Parse: How to Spot It and Fix It Quickly. Fluent C++. 2018-01-30 [2021-01-17]. (原始内容存档于2021-11-25) (美国英语). 
  6. ^ Stroustrup, Bjarne. C++11 FAQ. www.stroustrup.com. Uniform initialization syntax and semantics. 19 August 2016 [2021-01-17]. (原始内容存档于2021-08-20) (英语). 
  7. ^ Myths and urban legends about C++. C++ FAQ. What is copy elision? What is RVO?. [2021-01-17]. (原始内容存档于2021-11-25). 
  8. ^ Devlieghere, Jonas. Guaranteed Copy Elision. Jonas Devlieghere. 2016-11-21 [2021-01-17]. (原始内容存档于2021-11-25) (英语).  Note, however, the caveats covered in Brand, C++. Guaranteed Copy Elision Does Not Elide Copies. Microsoft C++ Team Blog. 2018-12-11 [2021-01-17]. (原始内容存档于2021-11-25) (美国英语). 

外部链接