透彻理解C++11新特性:右值引用、std::move、std::forward

简介:

透彻理解C++11新特性:右值引用、std::move、std::forward

目录
浅拷贝、深拷贝
左值、右值
右值引用类型
强转右值 std::move
重新审视右值引用
右值引用类型和右值的关系
函数参数传递
函数返还值传递
万能引用
引用折叠
完美转发 std::forward
C++11出现的右值相关语法可谓是很多C++程序员难以理解的新特性,不少人知其然而不知其所以然,面试被问到时大概就只知道可以减少开销,但是为什么减少开销、减少了多少开销、什么时候用...这些问题也不一定知道,于是我写下了这篇夹带自己理解的博文,希望它对你有所帮助。

浅拷贝、深拷贝
在介绍右值引用等概念之前,可以先来认识下浅拷贝(shallow copy)和深拷贝(deep copy)。

这里举个例子:

class Vector{

int num;
int* a;

public:

void ShallowCopy(Vector& v);
void DeepCopy(Vector& v);

};
浅拷贝:按位拷贝对象,创建的新对象有着原始对象属性值的一份精确拷贝(但不包括指针指向的内存)。
//浅拷贝
void Vector::ShallowCopy(Vector& v){

this.num = v.num;
this.a = v.a;

}
深拷贝:拷贝所有的属性(包括属性指向的动态分配的内存)。换句话说,当对象和它所引用的对象一起拷贝时即发生深拷贝。
//深拷贝
void Vector::DeepCopy(Vector& v){

this.num = v.num;
this.a = new int[num];
for(int i=0;i<num;++i){a[i]=v.a[i]}

}
可以看到,深拷贝的开销往往比浅拷贝大(除非没有指向动态分配内存的属性),所以我们就倾向尽可能使用浅拷贝。

但是浅拷贝的有一个问题:当有指向动态分配内存的属性时,会造成多个对象共用这块动态分配内存,从而可能导致冲突。一个可行的办法是:每次做浅拷贝后,必须保证原始对象不再访问这块内存(即转移所有权),这样就保证这块内存永远只被一个对象使用。

那有什么对象在被拷贝后可以保证不再访问这块内存呢?相信大家心里都有答案:临时对象。

左值、右值
为了让编译器识别出临时对象,从而好做浅拷贝优化,于是C++引入了左值(lvalue)、右值(rvalue)的概念。

左值:表达式结束后依然存在的持久对象。
右值:表达式结束后就不再存在的临时对象。
之所以取名左值右值,是因为在等式左边的值往往是持久存在的左值类型,在等式右边的表达式值往往是临时对象。

a = ++b;
a = b+c*2;
a = func();
更直观的理解是:有变量名、可以取地址的对象都是左值,没有变量名、不可以取地址的都是右值。(因为有无变量名意味着这个对象是否在下一行代码时依然存在)

右值引用类型
有了左值、右值的概念,我们就很清楚认识到右值都是些短暂存在的临时对象。

于是,C++11 为了匹配这些左右值类型,引入了右值引用类型 && 。
右值引用类型负责匹配右值,左值引用则负责匹配左值。

因此刚刚的浅拷贝、深拷贝例子,我们可以无需显式调用浅拷贝或深拷贝函数,而是调用重载函数:

//左值引用形参=>匹配左值
void Vector::Copy(Vector& v){

this.num = v.num;
this.a = new int[num];
for(int i=0;i<num;++i){a[i]=v.a[i]}

}

//右值引用形参=>匹配右值
void Vector::Copy(Vector&& temp){

this.num = temp.num;
this.a = temp.a;

}
当然,最标准还是编写成各种构造函数(拷贝构造、移动构造、赋值构造、移动赋值构造):

移动的意思是转移所有权。由于右值 大部分 都是临时的值,临时值释放后也就不再持有属性的所有权,因此这相当于转移所有权的行为。

//拷贝构造函数:这意味着深拷贝
Vector::Vector(Vector& v){

this.num = v.num;
this.a = new int[num];
for(int i=0;i<num;++i){a[i]=v.a[i]}

}
//移动构造函数:这意味着浅拷贝
Vector::Vector(Vector&& temp){

this.num = temp.num;
this.a = temp.a;
temp.a = nullptr;    //实际上Vector一般都会在析构函数来释放指向的内存,所以需赋值空地址避免释放

}
虽然从优雅的实现深、浅拷贝这个目的开始出发,C++11的移动语义可以不止用于浅拷贝,得益于转移所有权的特性,我们还可以做其它事情,例如在右值所占有的空间临时存放一些东西。

强转右值 std::move
除了上面说的临时值,有些左值其实也很适合转移所有权:

void func(){

Vector result;
//...DoSomehing with ans
if(xxx){ans = result;}  //现在我希望把结果提取到外部的变量a上。
return;

}
可以看到result赋值给ans后就不再被使用,我们期望它调用的是移动赋值构造函数。
但是result是一个有变量名的左值类型,因此ans = result 调用的是赋值构造函数而非移动赋值构造函数。

为了将某些左值当成右值使用,C++11 提供了 std::move 函数以用于将某些左值转成右值,以匹配右值引用类型。

这也是移动语义的由来:无论是临时值还是被强转的左值,只要遵守转移所有权的保证,都可以使用移动语义。

void func(){

Vector result;
//...DoSomehing with ans
if(xxx){ans = std::move(result);}   //调用的是移动赋值构造函数
return;

}
重新审视右值引用
右值引用类型和右值的关系
有了上面的知识后,我们来重新审视一下右值引用类型。

先看看如下代码:

void test(Vector& o) {std::cout << "为左值。" << std::endl;}
void test(Vector&& temp) {std::cout << "为右值。" << std::endl;}

int main(){

Vector a;
Vector&& b = Vector();
    //请分别回答:a、std::move(a)、b 分别是左值还是右值?
test(a);
test(std::move(a));
test(b);

}
答:a是左值,std::move(a)是右值,但b却是左值。

在这里b虽然是 Vector&& 类型,但却因为有变量名(即可持久存在),被编译器认为是左值。

//即使函数返还值是临时值,但返还类型是左值引用类型,因此被认为是持久存在的左值。
Vector& func1();
//函数返还值为右值引用类型=>是短暂存在的右值。
Vector&& func2();
//函数返还值为正常类型=>是短暂存在的右值。
Vector func3();
结论:右值引用类型只是用于匹配右值,而并非表示一个右值。因此,尽量不要声明右值引用类型的变量,而只在函数形参使用它以匹配右值。

实际上C++ std::move的实现原理就是的强转右值引用类型并返还之,由于函数返还值类型是临时值,且返还的还是右值引用类型(非左值引用类型),因此该返还值会被判断为右值。

函数参数传递
void func1(Vector v) {return;}
void func2(Vector && v) {return;}

int main() {

Vector a;
Vector &b = a;
Vector c;
Vector d;

//请回答:不开优化的版本下,调用以下函数分别有多少Copy Consturct、Move Construct的开销?
func1(a);
func1(b);
func1(std::move(c));
func2(std::move(d));

}
实际上在不开优化的版本下,如果实参为右值,调用func1的开销只比func2多了一次移动构造函数和析构函数。

实参传递给形参,即形参会根据实参来构造。其结果是调用了移动构造函数;函数结束时则释放形参。

倘若说对象的移动构造函数开销较低(例如内部仅一个指针属性),那么使用无引用类型的形参函数是更优雅的选择,而且还能接受左值引用类型或无引用的实参(尽管这两种实参都会导致一次Copy Consturct)。

那我们在写一般函数形参的时候,有必要每个函数都提供关于&&形参的重载版本吗?

回答:一般来说是没必要的。对象的移动构造(赋值)函数开销不大时,我们可以只提供非引用类型和左值引用类型(避免Copy Construct)的重载版本,而不必编写右值引用类型的重载版本。

函数返还值传递
Vector func1() {

Vector a;
return a;

}

Vector func2() {

Vector a;
return std::move(a);

}

Vector&& func3() {

Vector a;
return std::move(a);

}

int main() {

//请回答:不开优化的版本下,执行以下3行代码分别有多少Copy Consturct、Move Construct的开销?
Vector test1 = func1();
Vector test2 = func2();
Vector test3 = func3();

}
同样的道理,执行这3行代码实际上都没有任何Copy Construct的开销(这其中也有NRV技术的功劳),都是只有一次Move Construct的开销。

此外一提,func3是危险的。因为局部变量释放后,函数返还值仍持有它的右值引用。

因此,这里也不建议函数返还右值引用类型,同前面传递参数类似的,移动构造开销不大的时候,直接返还非引用类型就足够了(在某些特殊场合有特别作用,例如std::move的实现)。

结论:我们应该把编写右值引用类型相关的任务放在对象的构造、赋值函数上,而非一般函数。从源头上出发,你就会发现在编写其它代码时就会自然而然享受到了移动构造、移动赋值的优化效果。

万能引用
接下来的内容都是属于模板的部分了:万能引用、引用折叠、完美转发。这部分更加难以理解,不编写模板代码的话可以绕道了。

万能引用(Universal Reference):

发生类型推导(例如模板、auto)的时候,使用T&&类型表示为万能引用,否则表示右值引用。
万能引用类型的形参既能匹配任意引用类型的左值、右值。
也就是说编写模板函数时,只提供万能引用形参一个版本就可以匹配左值、右值,不必编写多个重载版本。

template
void func(T&& t){

return;

}

int main() {

Vector a,b;
func(a);                //OK
func(std::move(b));     //OK

}
此外需要注意的是,使用万能引用参数的函数是最贪婪的函数,容易让需要隐式转换的实参匹配到不希望的转发引用函数。例如下面代码:

template
void f(T&& value);

void f(int a);
//当调用f(long类型的参数)或者f(short类型的参数),则不会匹配int版本而是匹配到万能引用的版本
引用折叠
使用万能引用遇到的第一个问题是推导类型会出现不正确的引用类型:例如当模板参数T为Vector&或Vector&&,模板函数形参为T&&时,展开后变成Vector& &&或者Vector&& &&。

template
void func(T&& t){

return;

}

int main(){

func(Vector()); //模板参数T被推导为Vector&&

}
但显然C++中是不允许对引用再进行引用的,于是为了让模板参数正确传递引用性质,C++定义了一套用于推导类型的引用折叠(Reference Collapse)规则:
所有的折叠引用最终都代表一个引用,要么是左值引用,要么是右值引用。

引用折叠 & &&
& & &
&& & &&
Example1:

func(Vector());
模板函数func的T被推导为Vector&&,形参object为T&&即展开后为Vector&& &&。由于折叠规则的存在,形参object最终被折叠推导为Vector&&类型。

Example2:

func(a);
模板函数func的T在这里被推导为Vector&,形参object为T&&即展开后为Vector& &&。由于折叠规则的存在,形参object最终被推导为Vector&类型。

完美转发 std::forward
当我们使用了万能引用时,即使可以同时匹配左值、右值,但需要转发参数给其他函数时,会丢失引用性质(形参是个左值,从而无法判断到底匹配的是个左值还是右值)。

//当然我们也可以写成如下重载代码,但是这已经违背了使用万能引用的初衷(仅编写一个模板函数就可以匹配左值、右值)
template
void func(T& t){

doSomething(t);

}

template
void func(T&& t){

doSomething(std::move(t));

}
完美转发(Perfect Forwarding):C++11提供了完美转发函数 std:forward 。它可以在模板函数内给另一个函数传递参数时,将参数类型保持原本状态传入(如果形参推导出是右值引用则作为右值传入,如果是左值引用则作为左值传入)。

于是现在我们可以这样做了:

template
void func(T&& object){

doSomething(std::forward<T>(object));

}
不借助std::forward间接传入参数的话,无论object是左值引用类型,还是右值引用类型,都会被视为左值。

std::forward()的实现主要就一句return static_cast(形参),实际上也是利用了折叠规则。从而接受右值引用类型时,将右值引用类型的值返还(返还值为右值)。接受左值引用类型时,将左值引用类型的值返还(返还值为左值)。

而std::move()的实现还需要先移除形参的所有引用性质得到无引用性质的类型(假设为T2),然后再return static_cast(形参),从而保证不会发生引用折叠,而是直接作为右值引用类型的值返还(返还值为右值)。

作者:KillerAery 出处:http://www.cnblogs.com/KillerAery/

相关文章
|
9月前
|
编译器 C++ 容器
【c++11】c++11新特性(上)(列表初始化、右值引用和移动语义、类的新默认成员函数、lambda表达式)
C++11为C++带来了革命性变化,引入了列表初始化、右值引用、移动语义、类的新默认成员函数和lambda表达式等特性。列表初始化统一了对象初始化方式,initializer_list简化了容器多元素初始化;右值引用和移动语义优化了资源管理,减少拷贝开销;类新增移动构造和移动赋值函数提升性能;lambda表达式提供匿名函数对象,增强代码简洁性和灵活性。这些特性共同推动了现代C++编程的发展,提升了开发效率与程序性能。
383 12
|
编译器 C++ 开发者
C++一分钟之-C++20新特性:模块化编程
【6月更文挑战第27天】C++20引入模块化编程,缓解`#include`带来的编译时间长和头文件管理难题。模块由接口(`.cppm`)和实现(`.cpp`)组成,使用`import`导入。常见问题包括兼容性、设计不当、暴露私有细节和编译器支持。避免这些问题需分阶段迁移、合理设计、明确接口和关注编译器更新。示例展示了模块定义和使用,提升代码组织和维护性。随着编译器支持加强,模块化将成为C++标准的关键特性。
1195 3
|
编译器 C语言 C++
C++一分钟之-C++11新特性:初始化列表
【6月更文挑战第21天】C++11的初始化列表增强语言表现力,简化对象构造,特别是在处理容器和数组时。它允许直接初始化成员变量,提升代码清晰度和性能。使用时要注意无默认构造函数可能导致编译错误,成员初始化顺序应与声明顺序一致,且在重载构造函数时避免歧义。利用编译器警告能帮助避免陷阱。初始化列表是高效编程的关键,但需谨慎使用。
294 2
|
存储 安全 C++
【C++11】右值引用
C++11引入的右值引用(rvalue references)是现代C++的重要特性,允许更高效地处理临时对象,避免不必要的拷贝,提升性能。右值引用与移动语义(move semantics)和完美转发(perfect forwarding)紧密相关,通过移动构造函数和移动赋值运算符,实现了资源的直接转移,提高了大对象和动态资源管理的效率。同时,完美转发技术通过模板参数完美地转发函数参数,保持参数的原始类型,进一步优化了代码性能。
217 2
|
编译器 C++
C++ 11新特性之右值引用
C++ 11新特性之右值引用
183 1
|
编译器 C++ 开发者
C++一分钟之-返回值优化与Move Semantics
【6月更文挑战第19天】C++的RVO与移动语义提升效率,减少对象复制。RVO是编译器优化,避免临时对象的创建。移动语义通过右值引用和`std::move`转移资源所有权。注意RVO不是总有效,不应过度依赖。使用移动语义时,避免误用`std::move`导致对象无效。示例展示了RVO和移动构造函数的应用。理解并恰当使用这些机制能写出更高效代码。
238 3
|
编译器 C语言 C++
从C语言到C++_33(C++11_上)initializer_list+右值引用+完美转发+移动构造/赋值(下)
从C语言到C++_33(C++11_上)initializer_list+右值引用+完美转发+移动构造/赋值
168 1
|
编译器 C++ 开发者
C++一分钟之-右值引用与完美转发
【6月更文挑战第25天】C++11引入的右值引用和完美转发增强了资源管理和模板灵活性。右值引用(`&&`)用于绑定临时对象,支持移动语义,减少拷贝。移动构造和赋值允许有效“窃取”资源。完美转发通过`std::forward`保持参数原样传递,适用于通用模板。常见问题包括误解右值引用只能绑定临时对象,误用`std::forward`,忽视`noexcept`和过度使用`std::move`。高效技巧涉及利用右值引用优化容器操作,使用完美转发构造函数和创建通用工厂函数。掌握这些特性能提升代码效率和泛型编程能力。
196 0
|
11月前
|
编译器 C++ 开发者
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
|
7月前
|
人工智能 机器人 编译器
c++模板初阶----函数模板与类模板
class 类模板名private://类内成员声明class Apublic:A(T val):a(val){}private:T a;return 0;运行结果:注意:类模板中的成员函数若是放在类外定义时,需要加模板参数列表。return 0;
206 0