带你一起探索 c++11 中右值引用、移动构造、&&、move、forward
共 7922字,需浏览 16分钟
·
2021-04-02 15:13
本文将介绍带你一步步的了解 c++11 中:
右值、右值引用
移动构造函数
&& 解密
move 移动语义
forward 完美转发
产生原由
class Object
{
public:
//无参构造函数
Object() : m_num(new int(10))
{
std::cout << "contr function..." << std::endl;
printf("m_num 地址:%p\n", m_num);
}
//拷贝构造函数
Object(const Object& o) : m_num(new int(*o.m_num))
{
std::cout << "copy contr function..." << std::endl;
}
private:
int* m_num;
};
getObj
函数初始化一个对象 oo1
, 分析其执行过程:getObj
函数中初始化一个临时对象 temp
, 调用构造函数;oo1
, 调用拷贝构造函数;class Object
{
...
}
Object getObj() {
//1、初始化一个临时对象 temp, 调用构造函数;
Object temp;
return temp;
}
int main() {
//2、将临时对象赋值给 `oo1`, 调用拷贝构造函数;
Object oo1 = getObj();
return 0;
};
contr function...
m_num 地址:00E7F6D8
copy contr function...
getObj
时,创建的临时对象 temp
在构造过程中如果要进行大量的初始化工作(特别耗时),并且其用完后将被释放;第二步中,将临时对象拷贝给 oo1
时,也需要进行大量的拷贝工作。oo1
在生命周期结束后也将释放。temp
只起到赋值作用,极大的耗费性能。有什么方式可以避免产生这个中间的临时变量呢?怎么去优化它呢?c++ 11
的右值引用。右 值
int x = 1000;
int y = 2000;
x = y;
右值引用
int&& data = 1000; //必须进行初始化
class Object
{
public:
Object()
{
std::cout << "contr function..." << std::endl;
}
Object(const Test& a)
{
std::cout << "copy contr function..." << std::endl;
}
};
Object getObj()
{
return Object();
}
int main()
{
int a1;
int &&a2 = a1; // error
Object& t = getObj(); // error
Object && t = getObj();
const Object& t = getObj();
return 0;
}
int &&a2 = a1;
a1 具有名字,其为左值,左值赋值给右值引用 错误
Object& t = getObj();
getObj 函数返回一个没有名字的右值,将一个右值赋值给左值引用 错误
Object && t = getObj();
右值赋值给右值引用 正确
const Object& t = getObj();
常量左值引用被成为万能引用,既可以引用左值也可以引用右值 正确
性能优化
中间产生了临时对象
temp只起到赋值作用,极大的耗费性能。有什么方式可以避免产生这个中间的临时变量呢?怎么去优化它呢?
class Object
{
...
}
Object getObj() {
//1、初始化一个临时对象 temp, 调用构造函数;
Object temp;
return temp;
}
int main() {
//2、将临时对象赋值给 `oo1`, 调用拷贝构造函数;
Object oo1 = getObj();
return 0;
};
getObj
函数中创建的临时对象(堆上)构建完成后,还没有使用,就释放掉了,那么如果可以复用这个临时对象,将会对性能有很大帮助。class Object
{
...
//移动构造函数
Object(Object&& o) {
m_num = o.m_num;
o.m_num = nullptr;
std::cout << "move contr function..." << std::endl;
}
private:
int* m_num;
}
Object getObj() {
//1、初始化一个临时对象 temp, 调用构造函数;
Object temp;
return temp;
}
int main() {
//2、 调用移动构造函数
Object oo1 = getObj();
return 0;
};
contr function...
m_num 地址:00CCF5B0
move contr function...
//移动构造函数
Object(Object&& o) {
m_num = o.m_num;
o.m_num = nullptr;
std::cout << "move contr function..." << std::endl;
}
...
Object oo1 = getObj();
Object oo1 = getObj();
这条语句是,调用移动构造函数将临时对象 temp
所指的内存直接赋值给 oo1
对象的指针(m_num = o.m_num;
); 然后避免 temp 出了作用域销毁内存,则将 temp 指向的内存置空(o.m_num = nullptr;
)。oo1
对象直接拥有了 构造 temp
时分配的内存。右值符号 && 解密
在很多代码模板函数中经常会出现诸如以下的代码:
1、 static_cast<typename remove_reference<_Ty>::type&&>(_Arg)
2、 typename<class T> void function(T&& t)
代码中的 && 会不会让你晕头转向?如果是,那么和我一起解密吧!
c++ 中有一种叫做未定义的引用类型,通常有以下两种方式:
自动类型推导的 auto&&
模板类型推导的 T&&
有一种特列 const T&&
, 表示右值引用,不属于未定义类型引用。
那么接下来记住两个规则即可,不对,是一个规则(引用折叠):
使用右值推导 T&& 和 auto&& 得到的是一个右值引用类型;其他的都是左值引用类型。
int a = 10;
int b = 250;
auto&& x = a; //a 是一个左值, auto&& 表示左值引用
auto&& y = 100; //100 是右值, auto&& 表示右值引用
int&& a1 = 5;
auto&& b1 = a1; //a1是右值引用,不是右值,所以b1 是左值引用
int a2 = 10;
int& a3 = a2;//a2是左值,a3为左值引用
auto&& c1 = a3;// a3是左值引用, c1 即是左值引用类型
auto&& c2 = a2; // a2是左值, c2 即是左值引用类型
const int& d1 =3;
const int&& d2 = 4;
auto&& e1 = d1; //d1是常量左值引用, e1 即为常量左值引用
auto&& e2 = d2; //d2是常量右值引用, e2 即为常量左值引用
void printX(int &x)
{
std::cout << "l-value: " << x << std::endl;
}
void printX(int &&x)
{
cout << "r-value: " << x << endl;
}
void forward(int &&x)
{
printX(x);
}
int main()
{
int a = 100;
printX(a);
printX(20);
forward(500);
return 0;
system("pause");
};
上面定义了两个重载函数 printX
, 先对上面的输出结果进行推导:
1、 printX(a)
; 其中 a
是一个左值,那么调用第一个 printX
函数,输出应该是左值;
2、 printX(20)
; 其中 20 是右值, 那么调用第二个printX
函数, 输出应该是右值;
3、 forward(500)
, 其中 500 是右值,forward
形参x
是右值引用类型,继续调用printX
,由于此时的右值具备的名字,所以将退化成一个左值,所以将调用第一个 printX
函数, 输出应该是左值;
对于最后以重情况可能稍微难以理解,只需要记住,右值引用在传递的过程中将会退化成左值引用。这也是 std::forward
为了防止退化成左值引用,所以才出现的,被誉为完美转发 ,即不做任何变动的转发。将上述 forward
函数中的 printX(x)
改成 printX(std::forward<int>(x))
将会调用第二个函数,输出应该是右值。
输出结果:
l-value: 100
r-value: 20
l-value: 500
结果完全正确。
std::move
在上述的例子中,有一种情况是不能进行赋值的,即用一个左值初始化一个右值引用;
int a = 10;
int&& b = a; //error 无法将右值引用绑定到左值
std::move
函数应运而生, move
通常被理解为“移动”, 但在本文中被译为“转移”更加合适,即转移所有权,将你的房产名字转给你的老婆,房子本身不变,只是所有者发生了变化。再来看 std::move 的源代码:
template<class _Ty> inline
_CONST_FUN typename remove_reference<_Ty>::type&&
move(_Ty&& _Arg) _NOEXCEPT
{// forward _Arg as movable
return (static_cast<typename remove_reference<_Ty>::type&&>(_Arg));
}
使用 std::move 后,上述代码可以更改为:
int a = 10;
int&& b = std::move(a); //ok
对于这种将左值转化为右值的方法有什么用处呢?
vector<string> vec;
vec.push_back("wang");
vec.push_back("zhuo");
.....
//插入一百万条数据
.....
vector<string> vec1 = vec;
vector<string> vec2 = std::move(vec);
如果用 vec
这个左值直接初始化 vec1
,将会发生大量的内存拷贝。
如果用 vec2 = std::move(vec),
直接将 vec
的所有权转移给 vec2
即可。
用处?在对于拥有大量的堆内存或者动态数组时候,使用 std::move 可以有效的节省时间效率。
如果将 std::move 和 移动构造函数结合起来,尽可能重复利用资源, 移动构造函数接收的是一个右值引用类型。
std::forward
上文也提及到了完美转发 forward,即在右值引用传递的过程中,为了防止被编译器当作左值处理,使其以原由类型进行转发,引入了 forward。
原型如下:
std::forward<T>(x);
当forward 的模板类型参数 T 为左值引用类型时,x将被转换成 T类型的左值,否则将被转换成右值。
template<typename T>
void printX(T& t)
{
std::cout << "left value"<< std::endl;
}
template<typename T>
void printX(T&& t)
{
std::cout << "rifht value " <<std::endl;
}
template<typename T>
void test(T && v)
{
printX(v);
printX(move(v));
printX(forward<T>(v));
}
int main()
{
test(100);
int num = 10;
test(num);
test(forward<int>(num));
test(forward<int&>(num));
test(forward<int&&>(num));
return 0;
}
test(100)
, 100 是右值,test
形参是未定义引用类型,即根据上文提到的 ”使用右值推导 T&&
和 auto&&
得到的是一个右值引用类型“ 形参 v 是一个具有名字的右值引用,但编译器将其视为左值。传递给第一个函数
printX
变为左值,调用第一个,输出左值。使用
move(v)
之后,左值 v 被 move 成右值,输出右值。printX(forward<T>(v))
, T为右值引用,因此最终将会成为右值, 输出右值
2、test(forward<int&>(num))
; 模板参数为int&
, 根据 ”当forward
的模板类型参数 T 为左值引用类型时,x将被转换成 T类型的左值“ , 将会得到一个左值,test
形参为未定义类型 T&&
, 根据 ”使用右值推导 T&&
和 auto&&
得到的是一个右值引用类型,反之为左值引用“,即test 形参为左值引用类型。
printX(v);
v 是左值, 输出左值printX(move(v));
左值经过move
后成为右值,输出右值printX(forward<T>(v));
T类型为int&
,v将被转换成左值, 输出左值。
你学会了吗?其他的几种留给你自己分析。