目录
左值引用与右值引用
1、左值与右值2、纯右值、将亡值3、左值引用与右值引用4、右值引用和 std::move 使用场景
引用限定符const 和引用限定符挪动语义—std::move()完美转发emplace_back 减少内存拷贝和挪动总结
左值引用与右值引用
1、左值与右值
概念1:
左值:可以放到等号左边的东西叫左值。右值:不可以放到等号左边的东西就叫右值。
概念2
左值:可以取地址并且有名字的东西就是左值。右值:不能取地址的没有名字的东西就是右值。
概念3
左值是指那些在表达式执行完毕后仍然存在的数据,也就是耐久性的数据。右值是指那些在表达式执行完毕后不再存在的数据,也就是临时性的数据。
有一种很简单的方法来区分左值和右值:对表达式取地址,假设编译器不报错就为左值,否则为右值。例如:int a = b + c;,a 是左值,有变量名,可以取地址,也可以放到等号左边,表达式 b+c 的返回值是右值,没有名字且不能取地址,&(b+c) 不能通过编译,而且也不能放到等号左边。
左值一般有:
变量名和函数名(注意:是函数名不是函数调用)返回左值引用的函数调用前置自增自减表达式++i、–i由赋值表达式或赋值运算符连接的表达式(a=b, a += b等)解引用表达式 *p字符串字面值 “abcd”
2、纯右值、将亡值
纯右值和将亡值都属于右值。
纯右值:运算表达式产生的临时变量、不和对象关联的原始字面量、非引用返回的临时变量、lambda 表达式等都是纯右值。举例:
除字符串字面值外的字面值返回非引用类型的函数调用后置自增自减表达式 i++、i–算术表达式 a+b,a*b,a&&b,a==b 等取地址表达式等,&a
将亡值:
将亡值是指 c++11 新增的和右值引用相关的表达式,通常指将要被挪动的对象、T&& 函数的返回值、std::move函数的返回值、转换为 T&& 类型转换函数的返回值,将亡值可以理解为即将要销毁的值,通过“盗取”其它变量内存空间方式获取的值,在确保其它变量不再被使用或者即将被销毁时,可以防止内存空间的释放和分配,延长变量值的生命周期,常用来完成挪动构造或者挪动赋值的特殊任务。举例:- class A {
- xxx;
- };
- A a;
- auto c = std::move(a); // c是将亡值
- auto d = static_cast<A&&>(a); // d是将亡值
复制代码 3、左值引用与右值引用
左值引用就是对左值停止引用的类型,右值引用就是对右值停止引用的类型,他们都是引用,都是对象的一个别名,并不拥有所绑定对象的堆存,所以都必需立即初始化。引用可以通过引用修改变量的值,传参时传引用可以防止拷贝。- type &name = exp; // 左值引用
- type &&name = exp; // 右值引用
复制代码 左值引用
左值引用:能指向左值,不能指向右值的就是左值引用:- int a = 5;
- int& b = a; // b是左值引用
- b = 4;
- int& c = 10; // error,10无法取地址,无法停止引用
- const int& d = 10; // ok,因为是常引用,引用常量数字,这个常量数字会存储在内存中,可以取地址。
复制代码 引用是变量的别名,由于右值没有地址,没法被修改,所以左值引用无法指向右值,等号右边的值必需可以取地址,假设不能取地址,则会编译失败。
但是,const 左值引用(常量引用)是可以指向右值的:const 左值引用不会修改指向值,因而可以指向右值,这也是为什么要使用 const & 作为函数参数的原因之一。
右值引用
c++11 规范新引入了另一种引用方式,称为右值引用,用 “&&” 表示。假设使用右值引用,那表达式等号右边的值需要是右值(不能是左值),可以使用 std::move 函数强迫把左值转换为右值。- int a = 4;
- int&& b = a; // error, a 是左值
- int&& c = std::move(a); // ok
- int num = 10;
- int && a = num; //error, 右值引用不能初始化为左值
- int && a = 10; // ok
复制代码 【注意】和声明左值引用一样,右值引用也必需立即停止初始化操作。
左值引用与右值引用实质
(1)右值引用指向左值- int a = 5; // a是个左值
- int &ref_a_left = a; // 左值引用指向左值
- int &&ref_a_right = std::move(a); // 通过std::move将左值转化为右值,可以被右值引用指向
- cout << a; // 打印结果:5
复制代码 前面讲过可以使用 std::move 函数强迫把左值转换为右值,实现右值引用指向左值。std::move 是一个非常有迷惑性的函数:
不理解左右值概念的人们往往以为它能把一个变量里的内容挪动到另一个变量,比如在上边的代码里,看上去是左值 a 通过 std::move 挪动到了右值 ref_a_right 中,那是不是a里边就没有值了?并不是,打印出a的值仍然是5。事实上 std::move 挪动不了什么,唯一的功能是把左值强迫转化为右值,让右值引用可以指向左值。其实现等同于一个类型转换: static_cast<T&&>(lvalue)。 所以,单纯的 std::move(xxx) 不会有性能提升。
同样的,右值引用能指向右值,实质上也是把右值提升为一个左值,并定义一个右值引用通过 std::move:- int &&ref_a = 5;
- ref_a = 6;
- // 等同于以下代码:
- int temp = 5;
- int &&ref_a = std::move(temp);
- ref_a = 6;
复制代码 (2)左值引用、右值引用自身是左值还是右值?
被声明出来的左、右值引用都是左值。 因为被声明出的左右值引用是有地址的,也位于等号左边。认真看下边代码:- // 形参是个右值引用
- void change(int &&right_value) { right_value = 8; }
- int main() {
- int a = 5; // a是个左值
- int &ref_a_left = a; // ref_a_left是个左值引用
- int &&ref_a_right = std::move(a); // ref_a_right是个右值引用
- change(a); // 编译不过,a是左值,change参数要求右值
- change(ref_a_left); // 编译不过,左值引用ref_a_left自身也是个左值
- change(ref_a_right); // 编译不过,右值引用ref_a_right自身也是个左值
- change(std::move(a)); // 编译通过
- change(std::move(ref_a_right)); // 编译通过
- change(std::move(ref_a_left)); // 编译通过
- change(5); // 当然可以直接接右值,编译通过
- cout << &a << ' ';
- cout << &ref_a_left << ' ';
- cout << &ref_a_right;
- // 打印这三个左值的地址,都是一样的
- }
复制代码 看完后你可能有个问题,std::move 会返回一个右值引用 int && ,它是左值还是右值呢? 从表达式 int &&ref = std::move(a) 来看,右值引用 ref 指向的必需是右值,所以move返回的 int && 是个右值。所以右值引用既可能是左值,又可能是右值吗? 确实如此:右值引用既可以是左值也可以是右值,假设有名称则为左值,否则是右值。
或者说:作为函数返回值的 && 是右值,直接声明出来的 && 是左值。 这同样也符合前面章节对左值,右值的断定方式:其实引用和普通变量是一样的, int &&ref = std::move(a) 和 int a = 5 没有什么区别,等号左边就是左值,右边就是右值。
(3)无论是左值引用还是右值引用都是引用- int temp = 5;
- int &ref_t = temp;
- int &&ref_a = std::move(temp);
- ref_a = 6;
- cout << &temp << "," << &ref_t << "," << &ref_a<<endl;
- cout << "temp:" <<temp <<endl;
- // 输出结果
- // 0x61fe84 0x61fe84 0x61fe84
- // temp:6
复制代码 最后,从上述分析中我们得到如下结论:
从性能上讲,左右值引用没有区别,传参使用左右值引用都可以防止拷贝。右值引用可以直接指向右值,也可以通过 std::move 指向左值;而左值引用只能指向左值(const左值引用也能指向右值)。作为函数形参时,右值引用更灵敏。虽然 const 左值引用也可以做到左右值都接受,但它无法修改,有一定局限性。
- void f(const int& n) {
- n += 1; // 编译失败,const左值引用不能修改指向变量
- }
- void f2(int&& n) {
- n += 1; // ok
- }
- int main() {
- f(5);
- f2(5);
- }
复制代码 4、右值引用和 std::move 使用场景
std::move 只是类型转换工具,不会对性能有好处;右值引用在作为函数形参时更具灵敏性。他们有什么实际应用场景吗?
1、右值引用优化性能,防止深拷贝
(1)浅拷贝反复释放:对于含有堆内存的类,我们需要提供深拷贝的拷贝构造函数,假设使用默认构造函数,会导致堆内存的反复删除,比如下面的代码:- class A {
- public:
- A(int size) : size_(size) { data_ = new int[size]; }
- A() {}
- A(const A& a) {
- size_ = a.size_;
- data_ = a.data_;
- cout << "copy " << endl;
- }
- ~A() { delete[] data_; }
- int* data_;
- int size_;
- };
- int main() {
- A a(10);
- A b = a;
- cout << "b " << b.data_ << endl;
- cout << "a " << a.data_ << endl;
- return 0;
- }
复制代码 上面代码中,两个输出的是相同的地址,a 和 b 的 data_ 指针指向了同一块内存,这就是浅拷贝,只是数据的简单赋值,那再析构时 data_ 内存会被释放两次,导致程序出问题,这里正常会呈现 double free 导致程序解体的。
(2)深拷贝构造函数
在上面的代码中,默认构造函数是浅拷贝,在析构的时候会导致反复删除指针。正确的做法是提供深拷贝的拷贝构造函数,比如下面的代码:- #include <iostream>
- using namespace std;
- class A {
- public:
- A() : m_ptr(new int(0)) { cout << "constructor A" << endl; }
- A(const A& a) : m_ptr(new int(*a.m_ptr)) {
- cout << "copy constructor A" << endl;
- }
- ~A() {
- cout << "destructor A, m_ptr:" << m_ptr << endl;
- delete m_ptr;
- m_ptr = nullptr;
- }
- private:
- int* m_ptr;
- };
- // 为了防止返回值优化,此函数故意这样写
- A Get(bool flag) {
- A a;
- A b;
- cout << "ready return" << endl;
- if (flag)
- return a;
- else
- return b;
- }
- int main() {
- {
- A a = Get(false); // 正确运行
- }
- cout << "main finish" << endl;
- return 0;
- }
复制代码 深拷贝就是在拷贝对象时,假设被拷贝对象内部还有指针引用指向其它资源,自己需要重新开拓一块新内存存储资源,而不是简单的赋值。虽然深拷贝可以处置浅拷贝的问题,但是存在效率问题。
(3)挪动构造函数
深拷贝构造函数可以保证拷贝构造时的安全性,但有时这种拷贝构造存在效率问题,比如上面代码中的拷贝构培养是不用要的。上面代码中的 Get 函数会返回临时变量,然后通过这个临时变量拷贝构造了一个新的对象 b,临时变量在拷贝构造完成之后就销毁了,假设堆内存很大,那么,这个拷贝构造的代价会很大,带来了额外的性能损耗。
有没有办法防止临时对象的拷贝构造呢?答案是肯定的。看下面的代码:- #include <iostream>
- using namespace std;
- class A {
- public:
- A() : m_ptr(new int(0)) { cout << "constructor A" << endl; }
- A(const A& a) : m_ptr(new int(*a.m_ptr)) {
- cout << "copy constructor A" << endl;
- }
- // 挪动构造函数,可以浅拷贝
- A(A&& a) : m_ptr(a.m_ptr) {
- a.m_ptr = nullptr; // 为防止a析构时delete data,提早置空其m_ptr
- cout << "move constructor A" << endl;
- }
- ~A() {
- cout << "destructor A, m_ptr:" << m_ptr << endl;
- if (m_ptr) delete m_ptr;
- }
- private:
- int* m_ptr;
- };
- // 为了防止返回值优化,此函数故意这样写
- A Get(bool flag) {
- A a;
- A b;
- cout << "ready return" << endl;
- if (flag)
- return a;
- else
- return b;
- }
- int main() {
- {
- A a = Get(false); // 返回右值,调用挪动构造函数
- }
- cout << "main finish" << endl;
- return 0;
- }
复制代码 上面的代码中实现了挪动构造( Move Construct)。从挪动构造函数的实现中可以看到,它的参数是一个右值引用类型的参数 A&&,这里没有深拷贝,只要浅拷贝,这样就防止了对临时对象的深拷贝,进步了性能。
在实际开发中,通常在类中自定义挪动构造函数的同时,会再为其自定义一个适当的拷贝构造函数,由此当用户利用右值初始化类对象时,会调用挪动构造函数;使用左值(非右值)初始化类对象时,会调用拷贝构造函数。
这里的 A&& 用来根据参数是左值还是右值来建立分支,假设是临时值,则会选择挪动构造函数。
挪动构造函数只是将临时对象的资源做了浅拷贝,不需要对其停止深拷贝,从而防止了额外的拷贝,进步性能。这也就是所谓的挪动语义( move 语义),右值引用的一个重要目的是用来支持挪动语义的(挪动语义的分析详细见下文)。
引用限定符
将左值的类对象称为左值对象,将右值的类对象称为右值对象。默认情况下,对于类中用 public 修饰的成员函数,既可以被左值对象调用,也可以被右值对象调用,举个例子:- #include <iostream>
- using namespace std;
- class demo {
- public:
- demo(int num) : num(num) {}
- int get_num() { return this->num; }
- private:
- int num;
- };
- int main() {
- demo a(10);
- cout << a.get_num() << endl;
- cout << move(a).get_num() << endl;
- return 0;
- }
复制代码 可以看到,demo 类中的 get_num() 成员函数既可以被 a 左值对象调用,也可以被 move(a) 生成的右值 demo 对象调用,运行程序会输出两个 10。
某些场景中,我们可能需要限制调用成员函数的对象的类型(左值还是右值),为此 c++11 新添加了引用限定符。所谓引用限定符,就是在成员函数的后面添加 “&” 或者 “&&”,从而限制调用者的类型(左值还是右值)。【注意】引用限定符不适用于静态成员函数和友元函数。- // 代码修改
- class demo {
- public:
- demo(int num) : num(num) {}
- int get_num() & { return this->num; } // 添加了 "&",限定调用该函数的对象必需是左值对象
- private:
- int num;
- };
- int main() {
- demo a(10);
- cout << a.get_num() << endl; // 正确
- // cout << move(a).get_num() << endl; // 错误
- return 0;
- }
复制代码- // 代码修改
- class demo {
- public:
- demo(int num) : num(num) {}
- int get_num() && { return this->num; } // 添加了 "&&",限定调用该函数的对象必需是右值对象
- private:
- int num;
- };
- int main() {
- demo a(10);
- //cout << a.get_num() << endl; // 错误
- cout << move(a).get_num() << endl; // 正确
- return 0;
- }
复制代码 const 和引用限定符
const 也可以用于修饰类的成员函数,习惯称为常成员函数。
const 和引用限定符修饰类的成员函数时,都位于函数的末尾。C++11 规范规定,当引用限定符和 const 修饰同一个类的成员函数时,const 必需位于引用限定符前面。如下:- #include <iostream>
- using namespace std;
- class demo {
- public:
- demo(int num, int num2) : num(num), num2(num2) {}
- //左值和右值对象都可以调用
- int get_num() const& { return this->num; }
- //仅供右值对象调用
- int get_num2() const&& { return this->num2; }
- private:
- int num;
- int num2;
- };
复制代码 【注意】当 const && 修饰类的成员函数时,调用它的对象只能是右值对象;当 const & 修饰类的成员函数时,调用它的对象既可以是左值对象,也可以是右值对象。无论是 const && 还是 const & 限定的成员函数,内部都不允许对当前对象做修改操作。
挪动语义—std::move()
所谓挪动语义,指的就是以挪动而非深拷贝的方式初始化含有指针成员的类对象:之前的拷贝是对于他人的资源,自己重新分配一块内存存储复制过来的资源,而对于挪动语义,类似于转让或者资源窃取的意思,对于那块资源,转为自己所拥有,他人不再拥有也不会再使用,通过 c++11 新增的挪动语义可以省去很多拷贝负担,怎么利用挪动语义呢,是通过挪动构造函数。
挪动语义可以将资源(堆、系统对象等)通过浅拷贝方式从一个对象转移到另一个对象,这样可以减少不用要的临时对象的创建、拷贝以及销毁,可以大幅度进步 c++ 应用程序的性能,消除临时对象的维护(创建和销毁)对性能的影响。- class A {
- public:
- A(int size) : size_(size) { data_ = new int[size]; }
- A() {}
- A(const A& a) {
- size_ = a.size_;
- data_ = new int[size_];
- cout << "copy " << endl;
- }
- A(A&& a) { // 挪动构造函数
- this->data_ = a.data_;
- a.data_ = nullptr;
- cout << "move " << endl;
- }
- ~A() {
- if (data_ != nullptr) {
- delete[] data_;
- }
- }
- int* data_;
- int size_;
- };
- int main() {
- A a(10);
- A b = a;
- A c = std::move(a); // 返回右值,调用挪动构造函数
- return 0;
- }
复制代码 假设不使用 std::move(),会有很大的拷贝代价,使用挪动语义可以防止很多无用的拷贝,提供程序性能,c++ 所有的 STL 都实现了挪动语义,方便我们使用。
【注意1】挪动语义仅针对于那些实现了挪动构造函数的类的对象,对于那种根本类型 int、float 等没有任何优化作用,还是会拷贝,因为它们实现没有对应的挪动构造函数。
【注意2】在实际开发中,通常在类中自定义挪动构造函数的同时,会再为其自定义一个适当的拷贝构造函数,由此当用户利用右值初始化类对象时,会调用挪动构造函数;使用左值(非右值)初始化类对象时,会调用拷贝构造函数。
完美转发
首先,解释一下什么是完美转发,它指的是函数模板可以将自己的参数“完美”地转发给内部调用的其它函数。所谓完美,即不只能准确地转发参数的值,还能保证被转发参数的左、右值属性不变。例如:- template <typename T>
- void function(T t) {
- otherdef(t);
- }
复制代码 如上所示,function() 函数模板中调用了 otherdef() 函数。在此根底上,完美转发指的是:假设 function() 函数接收到的参数 t 为左值,那么该函数传送给 otherdef() 的参数 t 也是左值;反之假设 function() 函数接收到的参数 t 为右值,那么传送给 otherdef() 函数的参数 t 也必需为右值。
显然, function() 函数模板并没有实现完美转发。一方面,参数 t 为非引用类型,这意味着在调用 function() 函数时,实参将值传送给形参的过程就需要额外停止一次拷贝操作;另一方面,无论调用 function() 函数模板时传送给参数 t 的是左值还是右值,对于函数内部的参数 t 来说,它有自己的名称,也可以获取它的存储地址,因而它永远都是左值,也就是说,传送给 otherdef() 函数的参数 t 永远都是左值。总之,无论从那个角度看, function() 函数的定义都不“完美”。
接下来,那如何实现完美转发呢,答案是使用 std::forward():
首先在定义模板函数时,采用右值引用的语法格式定义参数类型,由此该函数既可以接收外界传入的左值,也可以接收右值;其次,还需要使用 c++11 规范库提供的 std::forword() 模板函数修饰被调用函数中需要维持左、右值属性的参数。
由此即可轻松实现函数模板中参数的完美转发,如下所示:- void PrintV(int& t) {
- cout << "lvalue" << endl;
- }
- void PrintV(int&& t) {
- cout << "rvalue" << endl;
- }
- template <typename T>
- void Test(T&& t) { // 1、采用右值引用的语法格式定义参数类型
- PrintV(t);
- PrintV(std::forward<T>(t));
- PrintV(std::move(t));
- }
- int main() {
- Test(1); // lvalue rvalue rvalue
- int a = 1;
- Test(a); // lvalue lvalue rvalue
- // 2、使用 std::forword() 模板函数修饰被调用函数
- Test(std::forward<int>(a)); // lvalue rvalue rvalue
- Test(std::forward<int&>(a)); // lvalue lvalue rvalue
- Test(std::forward<int&&>(a)); // lvalue rvalue rvalue
- return 0;
- }
复制代码Test(1):1是右值,模板中 T &&t 这种为万能引用,右值 1 传到 Test 函数中变成了右值引用,但是调用 PrintV() 时候,t 变成了左值,因为它变成了一个拥有名字的变量,所以打印 lvalue,而 PrintV(std::forward<T>(t)) 时候,会停止完美转发,依照原来的类型转发,所以打印 rvalue,PrintV(std::move(t)) 毫无疑问会打印 rvalue。Test(a):a 是左值,模板中 T && 这种为万能引用,左值 a 传到 Test 函数中变成了左值引用,所以有代码中打印。Test(std::forward<T>(a)):转发为左值还是右值,依赖于 T,T 是左值那就转发为左值,T 是右值那就转发为右值。
- #include <iostream>
- using namespace std;
- //重载被调用函数,查看完美转发的效果
- void otherdef(int & t) {
- cout << "lvalue\n";
- }
- void otherdef(const int & t) {
- cout << "rvalue\n";
- }
- //实现完美转发的函数模板
- template <typename T>
- void function(T&& t) {
- otherdef(forward<T>(t));
- }
- int main()
- {
- function(5); // rvalue
- int x = 1;
- function(x); // lvalue
- return 0;
- }
- // 打印结果
- // rvalue
- // lvalue
复制代码 emplace_back 减少内存拷贝和挪动
对于STL容器,c++11 后引入了 emplace_back 接口。emplace_back 是就地构造,不用构造后再次复制到容器中,因而效率更高。考虑这样的语句:- vector<string> testVec;
- testVec.push_back(string(16, 'a'));
复制代码 上述语句足够简单易懂,将一个 string 对象添加到 testVec 中。底层实现:
首先,string(16, ‘a’) 会创建一个 strin g类型的临时对象,这涉及到一次string 构造过程。其次,vector 内会创建一个新的 string 对象,这是第二次构造。最后在 push_back 完毕时,最开端的临时对象会被析构。加在一起,这两行代码会涉及到两次 string 构造和一次析构。
c++11 可以用 emplace_back 替代 push_back,emplace_back 可以直接在vector中构建一个对象,而非创建一个临时对象,再放进vector,再销毁。emplace_back可以省略一次构建和一次析构,从而到达优化的目的。
emplace_back 内部没有使用拷贝构造函数,也没有使用挪动构造函数,而是直接调用构造函数,因而更加高效。
总结
c++11 在性能上做了很大的改进,最大水平减少了内存挪动和复制,通过右值引用、 forward、emplace 和一些无序容器我们可以大幅度改进程序性能。
右值引用仅仅是通过改变资源的所有者(剪切方式,而不是拷贝方式)来防止内存的拷贝,能大幅度进步性能。forward 能根据参数的实际类型转发给正确的函数(参数用 &&的方式)。emplace 系列函数通过直接构造对象的方式防止了内存的拷贝和挪动。
以上就是C++11学习之右值引用和挪动语义详解的详细内容,更多关于C++11右值引用 挪动语义的资料请关注网站其它相关文章! |