右值引用、移动语义、完美转发
-
左值、右值:在c++ 中,所有的值不是左值,就是右值。有名字的对象都是左值,右值没有名字。还有一个可以区分左值和右值的方法:看能不能对表达式取地址,如果能,则为左值,否则为右值。
-
左值:即表达式结束后依然存在的持久化对象。
-
右值:即表达式结束后就不再存在的临时对象。
-
C++ 11扩展了右值的概念,将右值分为了纯右值和将亡值。
-
纯右值:
- 非引用返回的临时变量
- 运算表达式产生的结果
- 字面常亮(C风格字符串除外,它是地址)
-
**将亡值:**与右值引用相关的表达式(C++ 11 为优化性能,减少空间的分配,将要释放的资源,延长其生命周期,而定义出来的新值)。列如:将要被移动的对象、T&&函数返回的值、std::move() 的返回值、转换成T&&的类型的转换函数的返回值。
class Person{ int m_nNum; } Person getTemp(){ return Person(); } int main(){ int iTemp0=3;/*iTemp0 为左值,有地址。3为右值*/ int iTemp1=iTemp0+8; //iTemp1 为左值,iTemp0+8 为右值,表达式返回值为临时对象 Person person=getTemp(); //person 为左值,getTemp()的返回对象为右值 临时对象。 }
-
-
-
-
**左值引用、右值引用:**C++ 98 中的引用很常见,就是给变量取个别名,在C++ 11 中,因为增加了右值引用的概念,所以C++ 98中的引用都称为左值引用。右值引用就是给右值取个别名,使用的符号是&&。
-
语法: 数据类型&& 变量名=右值
class Person { public: int m_Num = 0; }; Person getPerson() { return Person(); } int main(){ std::cout<<"hello World"<<endl; int&& A = 3; //3是右值 A是左值 延长右值即临时对象生命周期 int b = 8; //b是左值 int&& C = b + 5; //b+5 表达式返回临时对象,右值。C 是一个变量,是左值 延长右值即临时对象生命周期 Person&& person = getPerson(); //getPerson()的返回值是右值(临时对象)延长右值即临时对象生命周期 cout << "A=" << A << endl; cout << "c=" << C << endl; cout << "Person.m_Num:"<<person.m_Num << endl; return 0; }
-
引入右值引用的主要目的是为了实现移动语义。
-
左值引用只能绑定左值,右值引用只能绑定右值,如果绑定的不对,编译器就会失败。但是,常量左值引用却是一个奇葩,它可以算是一个万能的引用类型,它可以绑定非常量左值、常量左值、右值,而且在绑定右值的时候,常量左值引用还可以像右值的生命周期延长。
int &&A=3;//编译器通过 int&A=3; //编译器不通过 “初始化”: 无法从“int”转换为“int &” const int &A=3; //编译器通过 int a=1; const int &ra=a;//编译器通过 const int b=1; //编译器通过 const int &rb=b;//编译器通过
注意:上述代码中,常量左值引用用在函数参数中,可以接受字面值的原因,可能会问,为什么常量左值引用可以这样?答案很简单,编译器做了特殊处理,就行右值引用一样。都是做了特殊处理。
-
-
-
**移动语义:**如果一个对象中有堆区资源,需要编写拷贝构造函数和赋值函数,实现深拷贝。这里深拷贝与浅拷贝区别,就不一一介绍。感兴趣的同学可以自行百度一下。回到题点,如果被拷贝对象是临时对象,拷贝完了就没什么用,这样会造成没有意义的资源申请和释放操作,如果拷贝的对象数据量比较大,这种资源的操作会极大的降低我们程序的性能。为了节省资源的申请和释放的时间,C++ 11 新增了移动语义。
-
实现移动语义的主要两个函数:移动构造函数、移动赋值函数
-
**移动构造函数语法:**ClassName(ClassName&& other){}
-
**移动赋值函数语法:**ClassName &operator=(类名&&other){}
class Person { public: int* m_data = nullptr; Person() = default; void alloc() { m_data = new int; memset(m_data, 0, sizeof(int)); } Person(const Person& other) { cout << "调用了深拷贝构造函数。 \n"; if (m_data==nullptr) { alloc(); } memcpy(m_data, other.m_data, sizeof(int)); } Person(Person&& other) { cout << "调用了移动构造函数。 \n"; if (m_data != nullptr) { delete m_data; } m_data = other.m_data; other.m_data = nullptr; } Person& operator=(const Person& other) { cout << "调用了赋值函数。 \n"; if (this == &other) { return *this; } if (m_data == nullptr) { alloc(); } memcpy(m_data, other.m_data, sizeof(int)); } Person& operator=(Person&& other) { cout << "调用了移动赋值函数。 \n"; if (this == &other) { return *this; } if (m_data != nullptr) { delete m_data; } m_data = other.m_data; other.m_data = nullptr; } ~Person() { if (m_data != nullptr) { delete m_data; m_data = nullptr; } } }; Person getPerson() { return Person(); } int main(){ Person person1; person1.alloc(); *person1.m_data = 3; cout << "person.m_data=" << *person1.m_data << endl; Person person2 = person1;//调用拷贝构造函数 cout << "person2.m_data=" << *person2.m_data << endl; Person person3; person3= person1;//调用赋值函数 cout << "person3.m_data=" << *person3.m_data << endl; return 0; }
输出结果:
person.m_data=3
调用了深拷贝构造函数。
person2.m_data=3
调用了赋值函数。
person3.m_data=3 -
下面我们修改下main 函数让其调其移动构造函数、以及移动赋值函数
int main(){ Person person1; person1.alloc(); *person1.m_data = 3; cout << "person.m_data=" << *person1.m_data << endl; Person person2 = person1;//调用拷贝构造函数 cout << "person2.m_data=" << *person2.m_data << endl; Person person3; person3= person1;//调用赋值函数 cout << "person3.m_data=" << *person3.m_data << endl; Person person4= std::move(person3); //调用移动构造函数 cout << "person4.m_data=" << *person4.m_data << endl; Person person5; person5 = std::move(person3); //调用移动赋值函数 cout << "person5.m_data=" << *person5.m_data << endl; return 0; }
person.m_data=3
调用了深拷贝构造函数。
person2.m_data=3
调用了赋值函数。
person3.m_data=3
调用了移动构造函数。
person4.m_data=3
调用了移动赋值函数。
person5.m_data=程序崩溃
为什么奔溃?这里很多同学应该能猜到了吧!这是因为person3 被移动了两次,第一次移动,没有任何问题,但在第二次移动的时候,由于person3 的资源已经被移动了,所以在下面的调用访问,访问到空指针。所以导致崩溃。
-
对于一个左值,会调用拷贝构造函数但是有些左值是局部变量,生命周期也很短,能不能也移动而不是拷贝呢?C++ 11 就提出std::move()方法来将左值转义为右值。从而方便了移动语义。它其实就是告诉编译器,进行特殊处理。左值对象被转移资源后,不会立刻析构,只有在离开作用域后才会析构,如果此时继续使用左值的资源,可能会发生意想不到的错误。
-
如果没有提供移动构造函数、移动赋值函数,只提供了拷贝构造函数、赋值函数,编译器找不到移动构造函数和移动赋值函数,就去找拷贝构造函数和赋值函数。
-
C++ 11 中所有容器都实现了移动语义,避免了对含有资源的对象发生无畏的拷贝。
-
移动语义对于拥有资源(内存、文件句柄等)的对象有效,如果是基本类型,使用移动语义没有任何意义。
-
其实说白了点就是std::move() 其实就是调用了C++ 的指针强制转换。下面就是std::move的源码
template <class _Ty> _NODISCARD constexpr remove_reference_t<_Ty>&& move(_Ty&& _Arg) noexcept { // forward _Arg as movable return static_cast<remove_reference_t<_Ty>&&>(_Arg);
-
-
-
**完美转发:**在函数模板中,可以将参数“完美”的转发给其他函数,所谓完美转发,即不仅能准确的转发参数的值,还能转发参数的左值和右值的属性。
-
如果模板中函数的参数写成T&& 形式,那么函数既可以接收左值又可以接收右值。
-
提供了模板函数std::forward(),用于转发参数,保持其值的属性。
template<typename TT> void func(TT&&i){ func1(std::forward<TT>(i)); }
-