C++11中的右值引用
作者:网络转载 发布时间:[ 2015/8/24 10:45:59 ] 推荐标签:测试开发技术 .NET
右值引用和右值的关系
这个问题有点绕了,需要开动思考一下右值引用和右值是啥含义了。读者会凭空的认为右值引用肯定是右值,其实不然。我们在之前的例子中添加如下代码,并将main函数进行修改如下:
void test_rvalue_rref(MyString &&str)
{
cout << "tmp object construct start" << endl;
MyString tmp = str;
cout << "tmp object construct finish" << endl;
}
int main() {
test_rvalue_rref(foo());
return 1;
}
// 输出结果
Constructor is called! this->_data: 28913680
Move Constructor is called! src: 28913680
DeConstructor is called!
tmp object construct start
Copy Constructor is called! src: 28913680 dst: 28913712
// 可以看到这里调用的是复制构造函数而不是移动构造函数
tmp object construct finish
DeConstructor is called! this->_data: 28913712
DeConstructor is called! this->_data: 28913680
我想程序运行的结果肯定跟大多数人想到的不一样,“Are you kidding me?不是应该调用移动构造函数吗?为什么调用了复制构造函数?”。关于右值引用和左右值之间的规则是:
如果右值引用有名字则为左值,如果右值引用没有名字则为右值。
通过规则我们可以发现,在我们的例子中右值引用str是有名字的,因此为左值,tmp的构造会调用复制构造函数。之所以会这样,是因为如果tmp构造的时候调用了移动构造函数,则调用完成后str的申请的内存自己已经不可用了,如果在该函数中该语句的后面在调用str变量会出现我们意想不到的问题。鉴于此,我们也能够理解为什么有名字的右值引用是左值了。如果已经确定在tmp构造语句的后面不需要使用str变量了,可以使用std::move()函数将str变量从左值转换为右值,这样tmp变量的构造可以使用移动构造函数了。
而如果我们调用的是MyString b = foo()语句,由于foo()函数返回的是临时对象没有名字属于右值,因此b的构造会调用移动构造函数。
该规则非常的重要,要想能够正确使用右值引用,该规则必须要掌握,否则写出来的代码会有一个大坑。
完美转发
前面已经介绍了本文的两大主题之一的移动语义,还剩下完美转发机制。完美转发机制通常用于库函数中,至少在我的工作中还是很少使用的。如果实在不想理解该问题,可以不用向下看了。在泛型编程中,经常会遇到的一个问题是怎样将一组参数原封不动的转发给另外一个函数。这里的原封不动是指,如果函数是左值,那么转发给的那个函数也要接收一个左值;如果参数是右值,那么转发给的函数也要接收一个右值;如果参数是const的,转发给的函数也要接收一个const参数;如果参数是非const的,转发给的函数也要接收一个非const值。
该问题看上去非常简单,其实不然。看一个例子:
#include <iostream>
using namespace std;
void fun(int &) { cout << "lvalue ref" << endl; }
void fun(int &&) { cout << "rvalue ref" << endl; }
void fun(const int &) { cout << "const lvalue ref" << endl; }
void fun(const int &&) { cout << "const rvalue ref" << endl; }
template<typename T>
void PerfectForward(T t) { fun(t); }
int main()
{
PerfectForward(10);
// rvalue ref
int a;
PerfectForward(a);
// lvalue ref
PerfectForward(std::move(a));
// rvalue ref
const int b = 8;
PerfectForward(b);
// const lvalue ref
PerfectForward(std::move(b));
// const rvalue ref
return 0;
}
在上述例子中,我们想达到的目的是PerfectForward模板函数能够完美转发参数t到fun函数中。上述例子中的PerfectForward函数必然不能够达到此目的,因为PerfectForward函数的参数为左值类型,调用的fun函数也必然为void fun(int &)。且调用PerfectForward之前产生了一次参数的复制操作,因此这样的转发只能称之为正确转发,而不是完美转发。要想达到完美转发,需要做到像转发函数不存在一样的效率。
因此,我们考虑将PerfectForward函数的参数更改为引用类型,因为引用类型不会有额外的开销。另外,还需要考虑转发函数PerfectForward是否可以接收引用类型。如果转发函数PerfectForward仅能接收左值引用或右值引用的一种,那么也无法实现完美转发。
我们考虑使用const T &t类型的参数,因为我们在前文中提到过,const左值引用类型可以绑定到任何类型。但是这样目标函数不一定能接收const左值引用类型的参数了。const左值引用属于左值,非const左值引用和非const右值引用是无法绑定到const左值的。
如果将参数t更改为非const右值引用、const右值也是不可以实现完美转发的。
在C++11中为了能够解决完美转发问题,引入了更为复杂的规则:引用折叠规则和特殊模板参数推导规则。
引用折叠推导规则
为了能够理解清楚引用折叠规则,还是通过以下例子来学习。
typedef int& TR;
int main()
{
int a = 1;
int &b = a;
int & &c = a;
// 编译器报错,不可以对引用再显示添加引用
TR &d = a;
// 通过typedef定义的类型隐式添加引用是可以的
return 1;
}
在C++中,不可以在程序中对引用再显示添加引用类型,对于int & &c的声明变量方式,编译器会提示错误。但是如果在上下文中(包括使用模板实例化、typedef、auto类型推断等)出现了对引用类型再添加引用的情况,编译器是可以编译通过的。具体的引用折叠规则如下,可以看出一旦引用中定义了左值类型,折叠规则总是将其折叠为左值引用。这是引用折叠规则的全部内容了。另外折叠规则跟变量的const特性是没有关系的。
A& & => A&
A& && => A&
A&& & => A&
A&& && => A&&
特殊模板参数推导规则
下面我们再来学习特殊模板参数推导规则,考虑下面的模板函数,模板函数接收一个右值引用作为模板参数。
template<typename T>
void foo(T&&);
说白点,特殊模板参数推导规则其实是引用折叠规则在模板参数为右值引用时模板情况下的应用,是引用折叠规则的一种情况。我们结合上文中的引用折叠规则,
如果foo的实参是上文中的A类型的左值时,T的类型为A&。根据引用折叠规则,后foo的参数类型为A&。
如果foo的实参是上文中的A类型的右值时,T的类型为A&&。根据引用折叠规则,后foo的参数类型为A&&。
解决完美转发问题
我们已经学习了模板参数为右值引用时的特殊模板参数推导规则,那么我们利用刚学习的知识来解决本文中待解决的完美转发的例子。
#include <iostream>
using namespace std;
void fun(int &) { cout << "lvalue ref" << endl; }
void fun(int &&) { cout << "rvalue ref" << endl; }
void fun(const int &) { cout << "const lvalue ref" << endl; }
void fun(const int &&) { cout << "const rvalue ref" << endl; }
//template<typename T>
//void PerfectForward(T t) { fun(t); }
// 利用引用折叠规则代替了原有的不完美转发机制
template<typename T>
void PerfectForward(T &&t) { fun(static_cast<T &&>(t)); }
int main()
{
PerfectForward(10);
// rvalue ref,折叠后t类型仍然为T &&
int a;
PerfectForward(a);
// lvalue ref,折叠后t类型为T &
PerfectForward(std::move(a));
// rvalue ref,折叠后t类型为T &&
const int b = 8;
PerfectForward(b);
// const lvalue ref,折叠后t类型为const T &
PerfectForward(std::move(b));
// const rvalue ref,折叠后t类型为const T &&
return 0;
}
例子中已经对完美转发的各种情况进行了说明,这里需要对PerfectForward模板函数中的static_cast进行说明。static_cast仅是对传递右值时起作用。我们看一下当参数为右值时的情况,这里的右值包括了const右值和非const右值。
// 参数为右值,引用折叠规则引用前
template<int && &&T>
void PerfectForward(int && &&t) { fun(static_cast<int && &&>(t)); }
// 引用折叠规则应用后
template<int &&T>
void PerfectForward(int &&t) { fun(static_cast<int &&>(t)); }
可能读者仍然没有发现上述例子中的问题,“不用static_cast进行强制类型转换不是也可以吗?”。别忘记前文中仍然提到一个右值引用和右值之间关系的规则,如果右值引用有名字则为左值,如果右值引用没有名字则为右值。。这里的变量t虽然为右值引用,但是是左值。如果我们想继续向fun函数中传递右值,需要使用static_cast进行强制类型转换了。
其实在C++11中已经为我们封装了std::forward函数来替代我们上文中使用的static_cast类型转换,该例子中使用std::forward函数的版本变为了:
template<typename T>
void PerfectForward(T &&t) { fun(std::forward<T>(t)); }
对于上文中std::move函数的实现也是使用了引用折叠规则,实现方式跟std::forward一致。
本文内容不用于商业目的,如涉及知识产权问题,请权利人联系SPASVO小编(021-61079698-8054),我们将立即处理,马上删除。
相关推荐
更新发布
功能测试和接口测试的区别
2023/3/23 14:23:39如何写好测试用例文档
2023/3/22 16:17:39常用的选择回归测试的方式有哪些?
2022/6/14 16:14:27测试流程中需要重点把关几个过程?
2021/10/18 15:37:44性能测试的七种方法
2021/9/17 15:19:29全链路压测优化思路
2021/9/14 15:42:25性能测试流程浅谈
2021/5/28 17:25:47常见的APP性能测试指标
2021/5/8 17:01:11热门文章
常见的移动App Bug??崩溃的测试用例设计如何用Jmeter做压力测试QC使用说明APP压力测试入门教程移动app测试中的主要问题jenkins+testng+ant+webdriver持续集成测试使用JMeter进行HTTP负载测试Selenium 2.0 WebDriver 使用指南