C++: most vexing parse(语法解析)

(令人头痛的语法解析)

(这样的问题感觉很难发现,尤其在项目相对复杂的时候。。平时应该如何避免呢)

在写C++的时候遇到一个问题,初始化一个对象的时候构造函数没有被调用。类似的代码如下:

#include <iostream>
#include <string>
using std::cout;
using std::endl;

class A {  
public:
    A(int num){
    	m_num=num;
        cout << "构造函数执行"<< endl;
    }
	A(){
		m_num=0;
		cout<<"默认构造函数执行"<<endl;
	}
	A(std::string s){
		num=s.size();
		cout<<"string形参构造函数"<<endl;
	}
	
private:
	int m_num;
};
	
int main()  {
    A a();//被当成函数声明
    
    int num=99;
    A b(int (num));//被当成函数声明
    
    char stemp[]="hello";
    A c(std::string(stemp));//被当成函数声明
    
    return 0;
}

这段代码中,(最后一个例子)意图是构造一个std::string型匿名对象,然后传递给类A的构造函数,构造函数输出这个字符串。但是这段代码什么都没输出,说明构造函数没有被执行。这个奇怪的结果让我很好奇。
第一个和第二个例子也没有执行相应的构造函数,why?

经过一番资料查阅,原来A a(std::string(strmp));这段代码被解析成了一个函数名为a的有一个std::string参数stemp并返回A类型的函数声明。这在Scott Meyers的《Effective STL》有做解释,并把这个问题称为C++'s most vexing parse,本文也用了这个标题,翻译为C++最令人费解的解析。

在C++中,以下三种写法都声明了同一个函数

int f(double d)//声明接受一个double参数d,返回值为int类型的函数  
int f(double (d))//效果一样,参数名外的括号会被忽略  
int f(double)//直接省略参数名  

类似的,以下三种写法都声明的函数也相同

int g(double (*pf)())//声明接受一个无参数返回类型为double的函数指针pf参数,
//返回值为int类型的函数  
int g(double pf())//效果一样,pf是隐式函数指针  
int g(double ())//直接省略参数名  

前面代码中的A a(std::string(stemp));其实就跟函数f的第二种声明方式,stemp两边的括号被忽略。然后被解析成一个函数声明。还有一种情况,如果这段这段代码改成A a();,也不会调用A的默认构造函数,同样会被解析成函数声明。

(另外,关于函数声明的位置)

·······································································
1.在调用的函数前定义函数,此时可以不需要声明
2.在调用的函数前声明
3.在调用的函数里面也可以声明
4.在其他文件的头文件*.h文件里面声明,然后*.c文件直接调用头文件也可以。
以上,static函数慎用(它只在定义声明 它的文件中可见,而普通函数默认是extern的)
··································································
————————————————
1.函数在使用之前要声明
当函数定义放在main函数之后时(或者其他.cpp文件都可以)(关于编译的规则),函数声明可以在main函数之前,也可以在main函数里面(只要在(首次)调用此函数的语句之前的任意位置处声明都可以,一般都在main函数开头处声明)
2.当函数定义在main函数之前时,main函数里面就不用再次声明了,直接调用即可。
3.当函数定义的函数体比较长的时候,一般把定义写在main函数之后,声明写在main函数里面或者前面。
————————————————
所以。函数声明可以在main里面,也可以在main外面。

可以查看我另一篇有相关内容的文章:
https://blog.csdn.net/qq_26189301/article/details/102699627

大佬的解释

问了下爱吃烤鱼和炸鸡的大佬,大佬说:

1.为了兼容C语言,C++里规定,一切可以被当成函数声明的都会被当成函数声明。

2.如果要使用一个类的话。如果这个类的构造函数里没有参数,就初始化时就把括号去掉,比如这个例子中,把A a();改成A a;就不会被当成函数声明而是当成默认构造函数了;

如果这个类的构造函数里带了参数,那么就要把这些参数加进去。

像这里这样的情况,参数是用的该参数类型的构造函数创建的匿名对象传进去的,但是这样就会把std::string(stemp)解析成 std::string stemp,即函数名为a的带有一个形参且该形参类型为std::string的一个stemp形参的函数声明。
(直接传递进stemp(用隐式转换)就不会有歧义,就会调用构造函数)

解决方案

这是一个违反直觉的解析方式,所以在C++11中,针对这种情况,有提出解决方案。

1.Scott在书中有提到一种解决方法,就是把整个匿名对象用括号括起来,就像这样A a((std::string(stemp)));

2.更好地做法还是避免写这样的代码,而是先在外面初始化一个std::string类型的变量,然后再传给构造函数。或者直接通过隐式转换A a(stemp);来创建对象。

在C++11中,使用Uniform initialization可以处理这种歧义,

Uniform initialization syntax Using the new uniform initialization
syntax introduced in C++11 solves this issue.

The problematic code is then unambiguous when braces are used:

TimeKeeper time_keeper{Timer{}};

3.使用新的语法可以这样写A a{std::string(stemp)};

在这里插入图片描述
在这里插入图片描述

创建线程时的问题

定义一个函数对象(仿函数)(重载了()运算符的类):

class background_task{
public:
  void operator()() const
  {
    do_something();
    do_something_else();
  }
};

background_task f;
std::thread my_thread(f);

当把函数对象传入到线程构造函数中时,需要避免“最令人头痛的语法解析”(C++’s most vexing parse)。如果你传递了一个临时变量(匿名变量),而不是一个命名的变量;C++编译器会将其解析为函数声明,而不是类型对象的定义

例如:

std::thread my_thread(background_task());

这里相当与声明了一个名为my_thread的函数,这个函数带有一个参数(函数指针指向没有参数并返回background_task对象的函数),返回一个std::thread对象的函数,而非启动了一个线程。

。。。。。。。。。。。。。。。。。。。。。。。。。。。
另外。。下面这一段为什么没有被当成函数声明呢??(哦不对,。。还是当成声明了,没啥毛病)

class background_task{
public:
  void operator()() const
  {
    do_something();
    do_something_else();
  }
};

std::thread my_thread(back_ground);

在这里插入图片描述
在这里插入图片描述

。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。
也就是上述提到的下图中的第三种省略参数名的函数声明方式:
在这里插入图片描述
使用在前面命名函数对象的方式,或使用多组括号①,或使用新统一的初始化语法②,可以避免这个问题。

如下所示:

std::thread my_thread((background_task()));  // 1
std::thread my_thread{background_task()};    // 2

使用lambda表达式也能避免这个问题。lambda表达式是C++11的一个新特性,它允许使用一个可以捕获局部变量的局部函数(可以避免传递参数)。之前的例子可以改写为lambda表达式的类型:

std::thread my_thread([]{
  do_something();
  do_something_else();
});
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值