来源:《深入应用C++11 代码优化与工程级应用》
右值引用可以避免无谓的复制,提高程序性能。
相应的,C++11的容器中还添加了一些右值版本的插入函数。
1.&&的特性
右值引用就是对一个右值进行引用的类型。因为右值不具名,所以我们只能通过引用的方式找到它。
无论声明左值引用还是右值引用都必须立即进行初始化,因为引用类型本身并不拥有所绑定对象的内存,只是该对象的一个别名。
通过右值引用的声明,该右值又重获新生,其生命周期与右值引用类型变量的生命周期一样,只要该变量还活在,该右值临时量就会一直存活下去。
cpp
#include <iostream>
using namespace std;
int g_constructCount = 0;
int g_copyConstructCount = 0;
int g_destructCount = 0;
struct A
{
A()
{
cout << "construct:" << ++g_constructCount << endl;
}
A(const A& a)
{
cout << "copy construct:" << ++g_copyConstructCount << endl;
}
~A()
{
cout << "destruct:"<<++g_destructCount << endl;
}
};
A GetA()
{
return A();
}
int main()
{
A a = GetA();
cout << "---" << endl;
return 0;
}
在GCC下编译时设置编译选项-fno-elide-constructors来关闭返回值优化效果。
g++ -fno-elide-constructors main.cpp -o demo.exe
在没有返回值优化的情况下,拷贝构造函数调用了两次,一次是GetA() 函数内部创建的对象返回后构造一个临时对象产生的。
在C++中,当函数返回一个对象时,如果这个对象是通过值传递的,则会经历复制构造过程
即:copy construct
当GetA()调用时,它创建了一个局部对象A,调用构造函数,输出construct:1当GetA()返回时,这个局部对象的生命周期即将结束,因此需要创建一个临时对象来保存返回值,此时调用复制构造函数,输出copy construct:1
局部对象在GetA()函数作用域结束时被销毁,调用析构函数,输出destruct:1
使用临时对象对a进行初始化,调用了拷贝构造函数,输出copy construct:2
执行完A a = GetA();然后临时对象被销毁,输出destruct:2
return 0;a被销毁,输出destruct:3
cppA a; //调用构造函数 A b = a; //调用拷贝构造函数 b = a; //调用赋值运算符
拷贝构造函数与赋值运算符的区别:
1.调用时机
拷贝构造函数在创建新对象时调用。
赋值运算符在给已存在对象赋值时调用。
2.资源管理
拷贝构造函数通常需要分配新的资源
赋值运算符通常需要释放旧资源,然后分配新资源
3.返回类型
拷贝构造函数没有返回类型,它创建并返回一个新对象
赋值运算符返回一个指向调用对象的引用。
4.默认行为
如果没有显式定义拷贝构造函数,编译器会生成一个默认的拷贝构造函数,执行成员的逐成员拷贝
如果没有显式定义赋值运算符,编译器会生成一个默认的赋值运算符,执行成员的逐成员赋值。
cppclass MyClass { public: MyClass() { /* ... */ } MyClass(const MyClass& other) { /* 拷贝构造函数 */ } MyClass& operator=(const MyClass& other) { /* 赋值运算符 */ return *this; } // ... };
如果开启返回值优化:输出:
返回值优化会将临时对象优化掉。
这不是C++标准,是各编译器的优化规则。
cpp
#include <iostream>
using namespace std;
int g_constructCount = 0;
int g_copyConstructCount = 0;
int g_destructCount = 0;
struct A
{
A()
{
cout << "construct:" << ++g_constructCount << endl;
}
A(const A& a)
{
cout << "copy construct:" << ++g_copyConstructCount << endl;
}
~A()
{
cout << "destruct:"<<++g_destructCount << endl;
}
};
A GetA()
{
return A();
}
int main()
{
A&& a = GetA();
cout << "---" << endl;
return 0;
}
g++ -std=c++11 -fno-elide-constructors main.cpp -o demo.exe
比之前少了一次拷贝构造和一次析构。GetA()返回的对象没有再用来拷贝构造对象a,而是使用右值引用绑定了右值,让临时右值的生命周期延长了。我们可以利用这个特点做一些性能优化,即避免临时对象的拷贝构造和析构。
事实上,在C++98/03中,通常常量左值引用也经常来做性能优化。将上面的代码改成:
cpp
const A& a = GetA();
输出结果与右值引用一样,因为常量左值引用是一个"万能"的引用类型,可以接收左值,右值,常量左值,常量右值。需要注意的是普通的左值引用不能接受右值:
cpp
A& a = GetA();
编译错误:类型A的右值不能初始化非const类型的引用
实际上T&&并不是一定表示右值,它绑定的类型是未定的,既可能是左值又可能是右值。
cpp
#include <iostream>
using namespace std;
template<typename T>
void f(T&& param)
{
}
int main()
{
f(10); //右值
int x = 10;
f(x); //x是左值
return 0;
}
上面的例子中有&&,这表示param实际上是一个未定的引用类型。该类型被称为universal references(可以认为是一种未定的引用类型)(universal:全体的) ,它必须被初始化,它是左值还是右值引用取决于它的初始化,如果&&被一个左值初始化,它就是一个左值;如果它被一个右值初始化,它就是一个右值。
需要注意的是,只有当发生自动类型推断时,如函数模板的类型自动推导,或auto关键字,&&才是一个universal references。
cpp
template<typename T>
void f(T&& param) //这里的T的类型需要推导,所以&&是一个universal references
{
}
class Test
{
Test(Test&& rhs); //已经定义了一个特定的类型,没有类型推断,&&是一个右值引用
};
void f(Test&& rhs); //已经定义了一个确定的类型,没有类型推断,&&是一个右值引用
更复杂的例子:
cpp
template<typename T>
void f(std::vector<T>&& param);
param是什么类型?
是右值引用类型,因为在调用这个函数之前,这个vector<T>中的推断类型已经确定了,所以到调用的时候就没有类型推断了。
cpp
template<typename T>
void f(const T&& param);
这个param是universal references吗?其实它是一个右值引用类型。universal references仅仅在T&&下发生,任何一点附加条件都会使之失效,而变成一个普通的右值引用。
由于存在T&&这种未定的引用类型,当它作为参数时,有可能被一个左值引用或右值引用的参数初始化,这时经过类型推导的T&&类型,相比右值引用(&&)会发生类型的变化,这种变化被称为引用折叠。C++11中的引用折叠规则如下:1)所有的右值引用叠加到右值引用上仍然还是一个右值引用。
2)所有的其他引用类型之间的叠加都将变成左值引用。
左值或右值是独立于它的类型的,右值引用可能是左值也可能是右值。比如下面的例子:
cpp
int&& var1 = 10; //var1类型:int&&
auto&& var2 = var1; //var2存在类型推导,因此是一个universal reference,这里auto&& 最终被推导为int&
var1既是一个右值引用,var1本身也是一个左值(因为它有名字),此时auto被折叠为int&。
cpp
int w1;
auto&& v1 = w1;
v1是一个universal reference,它被一个左值初始化,所以它最终是一个左值引用。
cpp
int a = 10;
int&& b = a;
使用一个左值初始化右值引用是不合法的,会导致编译错误。
可以使用std::move:
cpp
int a = 10;
int&& b = std::move(a);
std::move可以将一个左值转换为右值。
------
编译器将已命名的右值引用视为左值,而将未命名的右值引用视为右值。
cpp
#include <iostream>
using namespace std;
void PrintValue(int& i)
{
std::cout << "lvalue:" << i <<std::endl;
}
void PrintValue(int&& i)
{
std::cout << "rvalue:" << i <<std::endl;
}
void Forward(int&& i)
{
PrintValue(i);
}
int main()
{
int i = 0;
PrintValue(i);
PrintValue(1);
Forward(2);
return 0;
}
lvalue:0
rvalue:1
lvalue:2
在Forward中调用PrintValue时,右值i变成了一个可命名的对象,编译器会将其当作左值处理。