复制省略和Most vexing parse

今天在和群里的朋友聊到右值和右值引用,发现C++的一些新特性自己掌握的还是不够深,特此记录一下。

起因是,和朋友讨论const引用,会不会调用移动赋值操作,于是便写了下面一段代码进行验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
using namespace std;

class Test
{
  public:
  Test(){}
  Test(const Test &t) { cout <<"copy construct" << endl; }
  Test(const Test &&t) {cout <<"move construct"<< endl;}
};

int main()
{
  Test obj(Test());
  return 0;
}

进行编译,发现输出的结果和自己的预想并不一致,可能有的小伙伴是和我一样的预期,都会认为会调用移动构造函数,然后输出move construct。可事实上啥也不会输出,如果用的是g++编译器,还会报一句warnning:”parentheses were disambiguated as a function declaration “。

Most vexing parse

The most vexing parse is a counterintuitive form of syntactic ambiguity resolution in the C++ language. In certain situations, the C++ grammar cannot distinguish between the creation of an object parameter and specification of a function’s type In those situations, the compiler is required to interpret the line as a function type specification.

详情请参考原文链接

Most vexing parse 翻译为最令人烦恼的解析,这个术语是 Scott Meyers在《Effective STL》中提出的,大意是指在某些情况下C++语法无法区分对象参数的创建和函数类型的声明,在这些情况下,编译器会将这一行解释为为一个函数类型。

也就是说在main函数里面的这一行 Test(Test()); 它会被解释为两种情况:

  1. 声明了类型为Test名称为obj的对象,并且使用类型为Test的临时对象去初始化
  2. 声明了一个返回值为Test类型名为obj的函数,该函数有一个匿名的参数,参数类型是一个指向返回值类型为Test,没有参数的函数指针

这种语句是模棱两可的,C++标准规定把这种情况认定为是函数声明。

所以以上,obj在这里只是一个函数,不是一个对象,根本不会调用移动构造函数。如果确实要这样调用移动构造函数,有两种办法

1. 使用C-style cast。Test obj((Test())); 2. 使用{}统一初始化。Test obj{Test{}};

至此,就避免了Most vexing parse,现在想想,我们使用了{}统一初始化,程序会输出什么?

答案还是,什么也不输出。

Copy Elision

In C++ computer programming, copy elision refers to a compiler optimization technique that eliminates unnecessary copying of objects.

详情参考原文链接

这是因为,编译器为我们提供了优化,包括G++、vs上都有这种优化。

当一个类类型的临时对象用来初始化同类型的对象时,复制构造会被编译器优化为直接构造,例如Test obj{Test{}};首先按照正常流程会先生成一个Test类型的临时对象,在调用移动构造初始化对象obj。编译器优化会优化掉这个移动赋值的过程,因为初始化临时对象和初始化obj的时候过程是相同的。如果面对数据量大的时候,会影响程序性能。

当然对于这种优化,其类的复制初始化或者是移动初始化是要public的,如果将移动构造声明为private,编译器就会报错。某种情况下,如果我们不需要这种优化,可以通过添加编译选项禁止这种优化,在G++中可以通过 -fno-elide-constructors命令选项禁止。

留下评论