Scott Meyers 在《Effective STL》一书的第六条提出了一个“当心C++最令人烦恼的解析"(the most vexing parse)的问题。“the most vexing parse” 这个术语来形容 C++ 标准对于 declaration 语句的消歧义(ambiguity resolution)约定与常人的认知相悖。

最令人烦恼的解析 (most vexing parse)是C++中的一种反直觉的二义性解析形式。 在一些场景下,编译器无法区分某语句是初始化时某对象的参数,还是声明一个函数时指定参数类型。在这些情况下,编译器将该行解释为函数声明。简言之就是“调用构造函数被误认为是函数声明的问题”,即 形如 Type() 或 Type(name) 的表达在某些情况下具有歧义(syntax ambiguity)。

下面来看那本书中的一个例子:假设有一个存有整数int的文件,需要把这些整数拷贝到一个list中,下面是合理的做法:

std::ifstream file("test.txt");
std::list<int> data(std::istream_iterator<int>(file), std::istream_iterator<int>());

上面这段代码的思路是,把一对istream_iterator对象传入到list的区间构造函数中 (Effective STL的第五条建议:区间成员函数优先于与之对应的单元素成员函数)。

但实际上,使用g++编译上述代码,可以编译成功,但有如下的警告:

D:\Code\Git\cppconcurrent\chapter2\main.cpp: In function 'int main()':
D:\Code\Git\cppconcurrent\chapter2\main.cpp:59:24: warning: parentheses were disambiguated as a function declaration [-Wvexing-parse]
   59 |     std::list<int> data(std::istream_iterator<int>(file), std::istream_iterator<int>());
      |                        ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
D:\Code\Git\cppconcurrent\chapter2\main.cpp:59:24: note: replace parentheses with braces to declare a variable
   59 |     std::list<int> data(std::istream_iterator<int>(file), std::istream_iterator<int>());
      |                        ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
      |                        -
      |                        {                                                              -
      |                                                                                       }

如果你忽略上述警告,直接运行,会发现它什么都不会做。不会从文件中读取任何数据,不会创建list,这是因为第二条语句并没有声明一个list,也没有调用构造函数。实际上上述编译器的警告信息已经说的很清楚了。但为了详细解释,我们还是从最基本的说起。

在C++中,如果要声明一个函数 f ,参数为 double ,返回值为 int ,以下三种方法都是合法的:

int f(double d);
int f(double(d)); // 同上,d两边的括号被忽略
int f(double);    // 同上,参数名被忽略

第2个声明,对行参加上圆括号,编译器会认为是冗余的并忽略。第3个则省略了行参名。

另外再看一个例子,假如我们想申明一个函数 g ,其参数是一个函数指针,下面 3 种写法都是合法的:

int g(double (*pf)()); // g以指向函数的指针为参数
int g(double pf());    // 同上,pf为隐士指针
int g(double());       // 同上,省去参数名pf

上面两个例子可以看出,围绕参数名的括号(比如f的第二个声明中的d)与独立的括号的区别:

  • 围绕参数名的括号会被忽略
  • 独立的括号则表明参数列表的存在,它们说明存在一个函数指针参数。

比如下面这两个语句:

T1 name(T2());
T1 name1(T2(name2));

C++都会将这两条语句视为声明:

  • 对于第一个语句,C++将其视为:返回值类型为 T1 名为 name 的函数(参数类型为指向返回值类型为 T2,参数为空的函数的指针)。
  • 对于第二个语句,C++将其视为:T1 name1(T2 name2); 显然也是一个函数申明。

现在再回到开头:

std::list<int> data(std::istream_iterator<int>(file), std::istream_iterator<int>());

这声明了一个函数  data ,返回值是 std::list<int> ,这个函数有两个参数:

  • 第一个参数名为  file  , 它的类型是  std::istream_iterator<int>  。 file 两边的括号是多余的,可以忽略(见f的第2个声明)。
  • 第二个参数没有名称,它的类型时一个指向不带参数的函数指针,该函数的返回类型是  std::istream_iterator<int> ,函数指针的参数名被省略了(见g的第3个声明)。

g++编译器的错误提醒,也说的很清楚,这是一个“vexing-parse”警告,它会被解析为一个函数声明:

D:\Code\Git\cppconcurrent\chapter2\main.cpp: In function 'int main()':
D:\Code\Git\cppconcurrent\chapter2\main.cpp:59:24: warning: parentheses were disambiguated as a function declaration [-Wvexing-parse]
   59 |     std::list<int> data(std::istream_iterator<int>(file), std::istream_iterator<int>());
      |                        ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
D:\Code\Git\cppconcurrent\chapter2\main.cpp:59:24: note: replace parentheses with braces to declare a variable
   59 |     std::list<int> data(std::istream_iterator<int>(file), std::istream_iterator<int>());
      |                        ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
      |                        -
      |                        {                                                              -
      |                                                                                       }

并且很贴心的给出了解决方法,那就是当初始化时,将小括号改为大括号。

std::ifstream file("test.txt");
std::list<int> data{std::istream_iterator<int>(file), std::istream_iterator<int>()};

针对这一问题的解决方法有多种:

  1. 多增加一对括号来跳过编译器的分析机制!(将形参的声明用括号括起来是非法的,但给函数参数加上括号却是合法的),将第一个参数多增加一个括号。
    std::list<int> data((std::istream_iterator<int>(file)), std::istream_iterator<int>());
  2. 为临时对象取名:
    std::ifstream file("test.txt");
    std::istream_iterator<int> begin=std::istream_iterator<int>(file);
    std::istream_iterator<int> end;
    std::list<int> data(begin, end);
  3. 使用大括号“{}”初始化。如上,可以看到g++编译器在发现"-Wvexing-parse" 警告时,建议的方法就是使用大括号来进行初始化。

在C++ Concurrency in Action中也有一个例子:

#include <iostream>
#include <thread>

class background_task
{
public:
    void operator()() const
    {
        std::cout << "do something!" << std::endl;
        std::cout << "do something else!" << std::endl;
    }
};

int main()
{
    std::thread t(background_task());
    t.join();
    return 1;
}

比如上述代码,在本意为初始化一个thread对象时,却意外定义了一个名为t的函数,返回值为thread,函数参数为一个函数指针,该函数指针参数为空,返回类型为back_ground_task,编译器的报错也很明线,t是一个函数声明,根本没有join方法。

expression must have class type but it has type "std::thread (*)(background_task (*)())"
request for member 'join' in 't', which is of non-class type 'std::thread(background_task (*)())'

解决方法很简单,那就是使用大括号运算符来初始化对象:

std::thread t{background_task()};