C++编译优化之RVO(Return ValueOptimization)
C++直接初始化与赋值初始化的区别
只要编程,问题就层出不穷,解决了一个,下回又会出新问题,好在有谷姐,度娘帮忙。这不今天编程时就遇到一个奇怪的问题,整整困扰了我一整天。闲话少说,让我们先来回顾一下。
首先让我们看一段代码,起初以为是二义性引起的(结果肯定不是啰):
classB{ public: B(){std::cout<<"B()."<<std::endl;} explicit B(int*l){std::cout<<"B(int*l)."<<std::endl;}//explicit B&operator=(const B&) { std::cout<<"B&operator=(const B&)."<<std::endl; } B& operator=(B&) { std::cout<<"B&operator=(B&)."<<std::endl; } // B(constB&){std::cout<<"B(const B&)."<<std::endl;} B(B&){std::cout<<"B(B&)."<<std::endl;} }; int main() { int *j; B w = j; B q = B(j); B l = w; return 0; }
输出: run.cpp: In function 'int main()': run.cpp:46: error: no matching function for call to 'B::B(B)' run.cpp:20: note: candidates are: B::B(B&) run.cpp:15: note: B::B() run.cpp:48: error: no matching function for call to 'B::B(B)' run.cpp:20: note: candidates are: B::B(B&) run.cpp:15: note: B::B()
本以为是B(int*l), B(B&)造成了二义性,结果发现去掉B(const B&)注释就一切OK,煞是费解啊。
1)直接调用构造函数B(int*l)初始化w;
2)j先调用B(int*l)生成临时对象,然后调用拷贝构造函数初始化w.
那么只有1中2)的假设是对的,即不是二义性引起的。那么就是拷贝构造函数的问题,这里需要声明B(j)这种调用方式产生的临时变量作为引用参数时都是常引用,即我们实际缺少的是带常引用的拷贝构造函数,那么当我们加入拷贝构造函数时,执行时
却没有输出“B(const B&).”这是为何?这不禁让我有些怀疑自己的推断,难道没有调用拷贝构造函数,很是怪啊。至此有些束手无策,还好在CSDN上有好心人帮忙,大神提示说是实际需要调用拷贝构造函数的。只是编译器优化掉了。(我的理解是语法上是需要的,但编译期在优化期将其剪掉了,实际并未执行)。
了解到这个后,找到了去除编译器优化的命令-O0,结果运行:
g++ -O0 run.cpp
结果仍然不如所愿,没看到拷贝构造函数的调用,伤心啊,继续查找让我看到了RVO(Return Value Optimization)-------编译器会优化多次传递临时变量的情形。然后搜索关掉RVO功能的办法:
g++ --no-elide-constructors run.cpp
结果输出如下:
run.cpp:52: error: conversion from 'int*' to non-scalar type 'B' requested
这个很容易解释:B w = j;需要调用B(int*l)进行一次隐士类型转化,而B(int*l)的类型前加了关键字explicit是禁止构造函数作为隐式转化使用。也即B(int*)必须显示调用。如B w = B(j)也是隐式调用拷贝构造函数,如果改为explicit B& B(const B&)调用也会失败。
原例中:去掉Bw = j,关掉RVO则输出:
B(int*l). B(const B&)
从这里我们可以看到赋值初始化是需要拷贝构造函数的。
在C++中有一类构造函数除了能显示调用以外,还可以被编译器隐式调用:带一个参数的构造函数------------默认隐式转换器。
1.上例中如果去掉explicit关键字,那么B w = j;就会隐式调用B(int*).
2. 同样用“=“初始化时,是隐式调用拷贝构造函数。如果拷贝构造函数加上explicit,赋值初始化将被禁用。
3. 赋值操作时,调用的是“operator=“。
严格意义上来讲,带一个参数的构造函数是构造器&&隐式转换器,而构造函数是构造器&&拷贝器&&默认拷贝器,而“operator=“是拷贝器。
默认拷贝器一般发生在:(1)复制初始化;(2)传递对象参数等。
隐式转换器一般发生在:(1)需要类型转换的复制初始化;(2)需要类型转换的复制操作。(3)需要类型转换的对象参数传递。
以上名词仅仅为便于理解,非官方,理解的越字面越好。
想必大家都了解C++中的初始化有两种:(1)直接初始化,(2)赋值初始化。但是或许有许多人没有真正明白这两者的差异。
对于类来讲直接初始化就是调用构造函数(包括拷贝构造函数)。所以只需要一步操作,那么就没有赋值初始化那么多的限制。
class B{ public: B(){std::cout<<"B()."<<std::endl;} B(int*l){std::cout<<"B(int*l)."<<std::endl;}//explicit //B(const B&){std::cout<<"B(const B&)."<<std::endl;} B(B&){std::cout<<"B(B&)."<<std::endl;} private: int *p; }; int main() { int *j; B a(j); B b(a); return 0; }
输出: B(int*l). B(B&). 若加一句:B c((B()));报错。 原因是B()这个临时值作为参数传递时是常引用类型。故加入B(const B&){std::cout<<"B(const B&)."<<std::endl;}就Ok了。
赋值初始化分两种:
1. 如例一的B w = j;需要两步,首先调用B(j)生成临时对象,然后隐式调用拷贝构造函数构造w。(B w = B()也是一样的)
2. 如上例一B l = w;直接隐式调用拷贝构造函数来构造l。
由上述讨论,我们可以得出以下几点:
(1) 在C++中由形如A(arglist)(arglist可以为空)产生的临时变量作为参数时是const A&类型。
(2) 赋值初始化隐式调用拷贝构造函数,可以通过加上explicit关键字禁掉赋值初始化功能。
(3) 若类需要赋值初始化功能我们应当将拷贝构造函数的参数类型设置为如const A&的常引用类型。事实上,任何时候都需 要这样。
(4) 由于隐式转换往往带来难以察觉的错误,所以当我们想要明确拒绝隐式转换时,对只含一个参数的构造函数加上explicit关键字,但是拷贝构造函数如非特殊要求千万别加。
返回值优化(Return Value Optimization,简称RVO),是这么一种优化机制:当函数需要返回一个对象的时候,如果自己创建一个临时对象用户返回,那么这个临时对象会消 耗一个构造函数(Constructor)的调用、一个复制构造函数的调用(Copy Constructor)以及一个析构函数(Destructor)的调用的代价。而如果稍微做一点优化,就可以将成本降低到一个构造函数的代价,也就是将内容直接构造到左值中,中间不生成临时变量。
我们来看一个例子:
#include<iostream> using namespace std; class car{ public: car(){cout<<"car()."<<endl;} car(const car&){cout<<"car(const car&)."<<endl;} ~car(){cout<<"~car()."<<endl;} }; car callDirectOne() { return car(); } car callDirectTwo() { return callDirectOne(); } car callElenOne() { car f; return f; } car callElenTwo() { return callElenOne(); } int main() { cout<<"*****************car a*********************"<<endl; car a; cout<<"*****************car b = a*********************"<<endl; car b = a; cout<<"*****************car c = car()*********************"<<endl; car c = car(); cout<<"*****************car fucA = callDirectOne()*********************"<<endl; car fucA = callDirectOne(); cout<<"*****************car fucB = callDirectTwo()*********************"<<endl; car fucB = callDirectTwo(); cout<<"*****************car fucC = callElenOne()*********************"<<endl; car fucC = callElenOne(); cout<<"*****************car fucD = callElenTwo()*********************"<<endl; car fucD = callElenTwo(); cout<<"**********************Delete Work******************************"<<endl; }
编译:g++ --no-elide-constructors init.cpp 执行:./a.out 输出: *****************car a********************* car(). *****************car b = a********************* car(const car&). *****************car c = car()********************* car(). car(const car&). ~car(). *****************car fucA = callDirectOne()********************* car(). car(const car&). ~car(). car(const car&). ~car(). *****************car fucB = callDirectTwo()********************* car(). car(const car&). ~car(). car(const car&). ~car(). car(const car&). ~car(). *****************car fucC = callElenOne()********************* car(). car(const car&). ~car(). car(const car&). ~car(). *****************car fucD = callElenTwo()********************* car(). car(const car&). ~car(). car(const car&). ~car(). car(const car&). ~car(). **********************Delete Work****************************** ~car(). ~car(). ~car(). ~car(). ~car(). ~car(). ~car().
如果打开RVO 的话输出结果如下:
*****************car a********************* car(). *****************car b = a********************* car(const car&). *****************car c = car()********************* car(). *****************car fucA = callDirectOne()********************* car(). *****************car fucB = callDirectTwo()********************* car(). *****************car fucC = callElenOne()********************* car(). *****************car fucD = callElenTwo()********************* car(). **********************Delete Work****************************** ~car(). ~car(). ~car(). ~car(). ~car(). ~car(). ~car().
中间的不断生成临时对象和销毁临时对象的过程全部省略,直接向左值中构造。当然如果不是返回值,本身传递的就是一个对象的话,那直接调用拷贝构造函数,否则中间过程+拷贝构造函数全部跳过,直接向左值中构造。