-
注:当我们尝试用不具名对象构建新对象的时候编译器会用NRVO,RVO优化
c++返回值优化的手段:
1、RVO和NRVO
RVO =Return value optimization
中文意思是不具名返回值优化
NRVO = Named Return value optimization
中文意思是具名返回值优化
这两种都是编译器在汇编级别上对代码段的优化,对于c++程序员来说却是透明的。
多说无益,直接上一个例子,这是从网上摘下来的一段代码:
#include<iostream>
using namespace std;
class RVO
{
friend RVO operator +(RVO & a, RVO& b);
public:
RVO(){printf("Iam in constructor\n");}
RVO(const RVO& c_RVO) {printf ("I am in copy constructor\n");}
~RVO(){printf("I am in destructor\n");}
intmem_var;
};
RVO operator +(RVO& a, RVO & b)
{
std::cout<<" +\n";
RVO c;
c.mem_var = a.mem_var + b.mem_var;
return c;
}
RVO foo()
{
RVOrvo;
returnrvo;
}
RVO foo1()
{
return RVO();
}
int main()
{
RVOrvo= RVO();
return0;
}
我想对于大多数学过c++的人也知道,常理来说的打印结果是这样的
于是我开启了编译器的返回值优化
优化结果就是少了一次拷贝。
对于RVO rvo= RVO();
编译器可能做出这样的优化
rvo.RVO::RVO();
于是乎就相当于直接构造了rvo
改一下主函数:
int main()
{
RVOrvo= foo();
return0;
}
关闭优化后,查看输出:
打开优化:
直接少了两次拷贝
一般对于上述情况的优化可能是这样的
RVOfoo(RVO &_hiddenArg,RVO &var)
{
_hiddenArg.RVO::RVO();
Return;
}
对于上述式子,没错,依旧可以被优化
+(+(RVO(0), RVO(1)), RVO(2))
没优化之前:
参数从左到右入栈,于是:
优化后:
一般返回不具名变量,编译器都会主动去优化。
2、右值引用
右值,也可以说是一个不具名对象。我们可以用一个const引用引用右值,但是右值引用却不能引用左值。
还有一点值得注意的是,不是我们引用了右值,右值的生命就会变得持久,一个语句结束后,右值会被马上释放。我们唯一能做的就是看能否在马上消失的右值那里窃取一些好处
用《深入理解C++11:C++11新特性解析与应用》里面的一段话
“
还有一个与移动语义看似无关,但偏偏有些关联的话题是,编译器中被称为RVO/NRVO的优化(RVO, Return Value Optimization,返回值优化,或者NRVO,Named Return Value optimization)。事实上,在本节中大量的代码都使用了-fno-elide-constructors选项在g++/clang++中关闭这个优化,这样可以使读者在代码中较为容易地利用函数返回的临时量右值。
但若在编译的时候不使用该选项的话,读者会发现很多构造和移动都被省略了。对于下面这样的代码,一旦打开g++/clang++的RVO/NRVO,从ReturnValue函数中a变量拷贝/移动构造临时变量,以及从临时变量拷贝/移动构造b的二重奏就通通没有了。
- A ReturnRvalue() { A a(); return a; }
- A b = ReturnRvalue();
b变量实际就使用了ReturnRvalue函数中a的地址,任何的拷贝和移动都没有了。通俗地说,就是b变量直接“霸占”了a变量。这是编译器中一个效果非常好的一个优化。不过RVO/NRVO并不是对任何情况都有效。比如有些情况下,一些构造是无法省略的。还有一些情况,即使RVO/NRVO完成了,也不能达到最好的效果。但结论是明显的,移动语义可以解决编译器无法解决的优化问题,因而总是有用的。
”
确实,本来我想用一个右值去匹配拷贝构造函数,但是编译器的优化让我们直接去调用了构造函数,拷贝构造函数被搁置在了一边。确定一个右值的重载函数有没有被调用,关键在于看编译器是否有优化:
#include <iostream>
using namespace std;
class RVO
{
public:
RVO(){printf("I am in constructor\n");}
RVO (const RVO& c_RVO) {printf ("I am in copy constructor\n");}
~RVO(){printf ("I am in destructor\n");}
int mem_var;
};
void MyMethod (RVO& rvo)
{
std::cout<<" Mythod copy\n";
}
void MyMethod (RVO&& rvo)
{
std::cout<<" Mythod rv\n";
}
int main()
{
RVO rvo;
MyMethod(rvo);
MyMethod(RVO());
return 0;
}
很明显,这只是简单的左值和右值引用,并没有产生新的对象,于是这里没有优化
判断是否会使用优化,我们应该看他是否构成了拷贝。
:
于是乎,我们是不是可以在传入右值的时候做点优化
#include <iostream>
using namespace std;
class RVO
{
public:
RVO(){mem_var = new int(0);printf("I am in constructor\n");}
RVO (const RVO& c_RVO) {
mem_var = new int(*c_RVO.mem_var);
printf ("I am in copy constructor\n");
}
RVO (RVO&& c_RVO) {
mem_var = c_RVO.mem_var;
c_RVO.mem_var = nullptr ;
printf ("I am in copy constructor\n");}
~RVO(){
delete mem_var;
mem_var = nullptr;
printf ("I am in destructor\n");
}
int *mem_var;
};
void MyMethod (RVO& rvo)
{
std::cout<<" Mythod copy\n";
}
void MyMethod (RVO&& rvo)
{
std::cout<<" Mythod rv\n";
}
int main()
{
RVO rvo;
MyMethod(rvo);
MyMethod(RVO());
return 0;
}
既然右值可以构成重载,那么就让编译器去判断左右值吧,我们只需要去关心拿到右值后我们该做些什么,我只知道右值生命不长久,那么在他结束生命前我是否从他那里得到一些好处,来减小自己的开销。注意,如果我们觉得一个具名对象我们不再需要他了,我们就用std::move()告诉编译器,我觉得把这个变量作为右值。但是在使用这个方法前,你必须保证你已经清楚自己在右值重载的函数里对右值做了些什么,而且被转换的变量应该在之后都不会去使用它。
接下来看看几种情况会使RVO和NRVO失效
1、不同的返回路径上返回不同名的对象(比如if XXX 的时候返回x,else的时候返回y)
2、引入 EH 状态的多个返回路径(就算所有的路径上返回的都是同一个具名对象)
3、在内联asm语句中引用了返回的对象名。
4、在函数内,用用std::move()包围返回值
#include <iostream>
using namespace std;
class RVO
{
public:
RVO(){mem_var = new int(0);printf("I am in constructor\n");}
RVO (const RVO& c_RVO) {
mem_var = new int(*c_RVO.mem_var);
printf ("I am in copy constructor\n");
}
RVO (RVO&& c_RVO) {
mem_var = c_RVO.mem_var;
c_RVO.mem_var = nullptr ;
printf ("I am in copy constructor\n");}
~RVO(){
delete mem_var;
mem_var = nullptr;
printf ("I am in destructor\n");
}
int *mem_var;
};
void MyMethod (RVO& rvo)
{
std::cout<<" Mythod copy\n";
}
void MyMethod (RVO&& rvo)
{
std::cout<<" Mythod rv\n";
}
RVO foo()
{
RVO rvo;
return std::move(rvo);
}
int main()
{
RVO r = foo();
return 0;
}
就是说上面的 return std::move(rvo);不仅不会提高效率,而是会造成一次不必要的拷贝,因为他使得NRVO失效了
#include <iostream>
using namespace std;
class RVO
{
public:
RVO(){mem_var = new int(0);printf("I am in constructor\n");}
RVO (const RVO& c_RVO) {
mem_var = new int(*c_RVO.mem_var);
printf ("I am in copy constructor\n");
}
RVO (RVO&& c_RVO) {
mem_var = c_RVO.mem_var;
c_RVO.mem_var = nullptr ;
printf ("I am in copy constructor\n");}
~RVO(){
delete mem_var;
mem_var = nullptr;
printf ("I am in destructor\n");
}
int *mem_var;
};
void MyMethod (RVO& rvo)
{
std::cout<<" Mythod copy\n";
}
void MyMethod (RVO&& rvo)
{
std::cout<<" Mythod rv\n";
}
RVO foo()
{
return std::move(RVO());
}
int main()
{
RVO rvo;
MyMethod(std::move(rvo));
return 0;
}
不过在上面的这种情况下,并没有任何的值拷贝,所以不会有RVO(仅仅是对右值和局部变量有优化),我们可以用move语句去告诉编译器我以后再也不会去用rvo这个变量了。
3、模板推导右值引用
当你传入一个右值的时候,如int(),那么推导出的是,int
但是,如果你传入一个左值,但是他的确不会报错,这是很让人吃惊的事情,其实对于左值,他推倒出的T却是一个int&
上面的例子,我在外部传入一个左值,并在内部定义了一个T类型的变量,并将其赋值与t,之后改变其值,那么不出所料,那么外部的
c++ primer 学习笔记之右值引用
最新推荐文章于 2024-04-08 17:12:06 发布