概述:右值引用、移动语义、完美转发
文章目录
右值引用、移动语义、完美转发
右值引用的定义
左值:有名字的内存中有自己地址的值,可以取地址
右值:可以浅显地理解成等号右边的值,在内存没有确定存储地址、没有变量名,结束就会销毁的值,简单来说右值就是临时对象,生命周期只在当前行有效,不能取地址
右值可分为:纯右值 和 将亡值
常见的右值:
1、除了字符串之外的字面值
2、返回类型为非引用的函数调用
3、算数表达式
举个例子:
int sum(int x, int y){
return x+y;
}
int x = 5;
int y = 7;// 7 字面值
int z = x+y;//算数表达式
int zz = sum(x,y);//sum(x,y) 函数调用,且返回值不是&
C++98/03 标准不支持为右值建立非常量左值引用,但允许使用常量左值引用操作右值,即:
常量左值引用 const + 左值引用
能够同时接受左值和右值
int &a = 10;//报错
const int &a = 10;//不报错,const+左值引用 能够同时接受左值和右值
因此
在函数传参的时候,通常函数的参数值写成 const + 左值引用
void Printname(const std::string& name)
右值引用的声明
右值引用的声明与左值引用一样
右值引用也必须立即进行初始化操作,且只能使用右值进行初始化
右值往往是没有名字的,要使用它只能借助
引用
,实际开发中也需要对右值进行修改,因此C++11 标准新引入了另一种引用方式,称为右值引用,用 “&&” 表示
int num = 10;
//int && a = num; //右值引用不能初始化为左值
int && a = 10;
常量左值引用不能修改值,右值引用可以对右值修改
int && a = 10;
a = 100;
cout << a << endl;//输出100
BTW
C++ 语法上是支持定义常量右值引用的,但实际这没有用
常量右值引用的作用就是引用一个不可修改的右值,完全可以由常量左值完成
const int&& a = 10;//编译器不会报错
其实,C++11 标准中对右值做了更细致的划分,分别称为纯右值(Pure value,简称 pvalue)和将亡值(eXpiring value,简称 xvalue )。其中纯右值就是 C++98/03 标准中的右值(上述中的三种),而将亡值则指的是和右值引用相关的表达式(比如某函数返回的 T && 类型的表达式)。对于纯右值和将亡值,都属于右值,读者知道即可,不必深究。
为什么需要使用右值引用
**右值引用主要用于 |移动语义| 和 |完美转发| **
左值引用,它的本质指针常量
左值引用就是一个不能变的指针,所以说在定义引用的时候就需要给他初始化,因为它之后就不能变了
右值引用的意义:降低内存消耗,可以避免一些复制和删除对象的操作
移动资源
移动语义
移动语义的定义
所谓移动语义,
指的就是以移动而非深拷贝的方式初始化含有指针成员的类对象。
简单的理解,
移动语义指的就是将其他对象(通常是临时对象)拥有的内存资源“移为已用”
需要使用移动的情景:{
需要将函数内传入一个对象 ,这个函数需要获得对象的所有权
此时,需要在堆栈中构造一个一次性对象,然后复制到函数内(这种操作不理想,尤其是对象需要堆分配内存时候)
因此,使用移动
可以避免复制,重新申请内存,删除等操作
}
事实上,对于程序执行过程中产生的临时对象,往往只用于传递数据(没有其它的用处),并且会很快会被销毁。因此在使用临时对象初始化新对象时,我们可以将其包含的指针成员指向的内存资源直接移给新对象所有,无需再新拷贝一份,这大大提高了初始化的执行效率。
移动构造函数
移动构造函数 vs 拷贝构造函数
C++11 标准中借助右值引用可以为指定类添加移动构造函数,这样当使用该类的右值对象(可以理解为临时对象)初始化同类对象时,编译器会优先选择移动构造函数
举个例子:
#include<iostream>
#include<vector>
using namespace std;
class Student{
public:
char*name;
int size;
Student(int size 0):size(size),name(nullptr)
{
if (size>0){
name = new char[size]:
for (int i=0; i < size; i++){
name[i] = 'a';
}
}
}
//注意传引用
//没有引用时传参本身就是拷贝,会不断调用拷贝构造,无限套娃
//深拷贝
//拷贝构造函数,直接复制一模一样的东西
Student(const Student& stu){
size = stu.size;
name =new char[size];
for (int i =0;i<size;i++){
name[i]=stu.name[i];
}
}
//浅拷贝
//两者完全一样,指向同一片内存
//造成指针悬挂问题,在执行析构函数时,会导致同一片内存执行两次delete,报错
//Student(const Student& stu){
//size = stu.size;
//name = stu.name;
//}
//移动构造函数,直接把别人的东西占为己有
//有新旧两个指针指向相同的内存,更改旧指针的指向,析构时候不改变内存的值
Student(Student&& stu)// && 右值引用
{
size =stu.size;
name= stu.name;
stu.name =nullptr;
cout<<"MOVE CONSTRUCTOR" <<endl;
}
~Student(){
delete name;
}
};
Student Create(){
return Student(5);
}
int main(){
vector<Student> school;
Student stu(5);
//school.push_back(stu);//调用拷贝构造
school.push_back(Create());//调用移动构造函数
}
移动构造函数的调用时机是:用同类的右值对象初始化新对象。
当使用当前类的 |左值对象| 初始化同类对象时,
调用 move() 函数,将左值强制转化为右值,即可调用移动构造函数
std::move()函数
std::move()函数,将左值强制转换为右值
使用 std::move()
的例子:
#include<iostream>
#include<vector>
using namespace std;
class Student{
public:
char*name;
int size;
Student(int size 0):size(size),name(nullptr)
{
if (size>0){
name = new char[size]:
for (int i=0; i < size; i++){
name[i] = 'a';
}
}
}
//拷贝构造函数,直接复制一模一样的东西
Student(const Student& stu){
size = stu.size;
name =new char[size];
for (int i =0;i<size;i++){
name[i]=stu.name[i];
}
}
//移动构造函数,直接把别人的东西占为己有
Student(Student&& stu)// && 右值引用
{
size =stu.size;
name= stu.name;
stu.name =nullptr;
cout<<"MOVE CONSTRUCTOR" <<endl;
}
~Student(){
delete name;
}
};
int main(){
vector<Student> school;
Student stu(5);
//std::move 将左值转换为右值,常用于转移资源的时候,节省内存空间
school.push_back(std::move(stu));//调用拷贝构造
}
std::move源码
template<typename T>
//move函数的返回类型typename remove reference<T>::type&& 返回右值引用
//T&& 万能引用,能接受左值 也能接受右值
typename remove_reference<T>::type&& move(T&& t){
return static_case<typename remove_reference<T>::type&&>(t);//类型转换, 转为右值
}
//类型萃取
struct remove_reference{
typedef T type;//定义T的类型别名为type
}
template<typename T>
struct remove_reference<T&>{//左值引用
typedef T type;
}
template<typename T>
struct remove_reference<T&&>{//右值引用
typedef T type;
}
类型成员是什么?
C++的类成员有成员函数、成员变量、静态成员三种类型,
但从C+11之后又增加了一种成员称为类型成员。
类型成员与静态成员一样,它们都属于类而不属于对象,
访问它时也与访问静态成员一样用 :: 访问。
另一个例子
移动构造函数内 参数初始化,将左值参数转化为右值参数
class Entity
{
public:
Entity(const String& name)
:m_Name(name){}
Entity(String&& name)//移动构造函数
:m_Name((String&& )name){} //m_Name(std::move(name))
void PrintName(){ m_Name.Print(); }
private:
String m_Name;
};
具体代码如下:
class String
{
public:
String()=default;
String(const char* string){
printf("Created!\n");
m_size = strlen(string);
m_Data = new char[m_Size+1];
memcpy(m_Data, string, m_Size);
}
String(const String& other){
printf("Copied!\n");
m_Size =other.m_Size;
m_Data = new char[m_Size+1];
memcpy(m_Data,other.m_Data,m_Size);
}
//移动构造函数,构造新对象,把原对象的数据移动到新对象
String(String&& other) noexcept{
printf("Moved!\n");
m_Size = other.m_Size;
m_Data = other.m_Data;
other.m_Size =0;
other.m_Data= nullptr;
}
//移动赋值操作符
//没有构造新对象,将另一个对象移动到自身,需要覆盖当前对象
//需要注意删除旧对象的数据
//需要保证不会赋值给自己
String& operator=(String&& other) noexcept{
printf("Moved!\n");
if(this != &other){
delete[] m_Data;
m_Size =other.m_Size;
m_Data= other.m_Data;
other.m_size =0;
other.m_Data =nullptr;
}
return *this;
}
~String(){
delete[] m_Data;
}
void Print(){
for (uint32_t i=0; i<m_Size; i++)
printf("%c", m_Data[i]);
printf("\n");
}
private:
char* m_Data;
uint32_t m_Size;
};
class Entity
{
public:
Entity(const String& name)
:m_Name(name){}
Entity(String&& name)//右值构造函数
:m_Name((String&& )name){} //m_Name(std::move(name))
void PrintName(){ m_Name.Print(); }
private:
String m_Name;
};
int main()
{
Entity entity(String("Cherno"));
//怎么把hello移动到dest上
String string ="Hello";
//三者均可
String dest((String&&)string);
String dest(std::move(string));
String dest = std::move(string);
//std::move是你想要將一个对象转换为临时对象时要做的
String apple ="Apple";
String dest = std::move(apple);//调用移动构造函数
std::cout <<"Apple:"
apple.Print();
std::cout <<"Dest:"
dest.Print();
dest =std::move(apple);//调用移动赋值运算符
std::cout <<"Apple:"
apple.Print();
std::cout <<"Dest:"
dest.Print();
std::cin.get();
}
C++三法则:
如果需要析构函数
,则一定需要 拷贝构造函数
和 拷贝赋值操作符
。
C++五法则:
为了支持移动语义,又增加了 移动构造函数
和 移动赋值运算符
引用限定符
定义:
所谓引用限定符,就是在成员函数的后面添加 “&” 或者 “&&”,从而限制调用者的类型(左值还是右值)。
作用:
限定成员函数的使用对象
注意,引用限定符不适用于静态成员函数和友元函数
默认情况下,对于类中用 public 修饰的成员函数,既可以被左值对象调用,也可以被右值对象调用
成员函数后 + & 限定只能左值对象使用
成员函数后 + && 限定只能右值对象使用
当const && 修饰类的成员函数时,限定只能是右值对象使用
当 const & 修饰类的成员函数时,不限定使用对象类型
无论是 const && 还是 const & 限定的成员函数,内部都不允许对当前对象做修改操作
#include <iostream>
using namespace std;
class demo {
public:
demo(int num):num(num){}
int get_num(){
return this->num;
}
int get_num1() & { //只能左值对象使用
return this->num;
}
int get_num2() && { //只能由右值对象使用
return this->num;
}
int get_num3() const & { //左值和右值对象都可以调用
return this->num;
}
int get_num4() const && { //仅供右值对象调用
return this->num2;
}
private:
int num;
};
int main() {
demo a(10);
cout << a.get_num() << endl;// 左值对象
cout << move(a).get_num() << endl;// 右值对象
return 0;
}
完美转发
完美转发的定义
指的是函数模板可以将自己的参数“完美”地转发给内部调用的其它函数。所谓完美,即不仅能准确地转发参数的值,还能保证被转发参数的左、右值属性不变
举个反例
template<typename T>
void function(T t) {
otherdef(t);
}
完美转发指的是:如果 function() 函数接收到的参数 t 为左值,那么该函数传递给 otherdef() 的参数 t 也是左值;反之如果 function() 函数接收到的参数 t 为右值,那么传递给 otherdef() 函数的参数 t 也必须为右值
显然上述例子没有实现完美转发
一方面,参数 t 为非引用类型,这意味着在调用 function() 函数时,实参将值传递给形参的过程就需要额外进行一次拷贝操作;
另一方面,无论调用 function() 函数模板时传递给参数 t 的是左值还是右值,对于函数内部的参数 t 来说,它有自己的名称,也可以获取它的存储地址,因此它永远都是左值
是否能实现完美转发,直接决定了该参数传递过程中是使用拷贝语义
还是移动语义
- 拷贝语义:调用拷贝构造函数
- 移动语义:调用移动构造函数
c++98/03 标准下的完美转发:
const 左值引用既可以接收左值,也可以接收右值
使用非 const 引用作为函数模板参数时,只能接收左值
举个例子:
#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(const T& t) {
otherdef(t);
}
//接收左值参数
template <typename T>
void function(T& t) {
otherdef(t);
}
int main()
{
function(5);//5 是右值
int x = 1;
function(x);//x 是左值
return 0;
}
c++11中的完美转发
C++11 标准中实现完美转发,只需要编写如下一个模板函数即可
template <typename T>
void function(T&& t) {//函数模板中 T && 为万能引用
otherdef(t);
}
此模板函数的参数 t 既可以接收左值,也可以接收右值, C++ 可以自行准确地判定出实际传入的实参是左值还是右值。
C++11 标准中规定,
通常情况下右值引用形式的参数只能接收右值,不能接收左值。
但对于函数模板中使用右值引用语法定义的参数来说,
它不再遵守这一规定,既可以接收右值,也可以接收左值(此时的右值引用又被称为“万能引用”)。
引用折叠规则
上述函数模板中,
由 function(num) 实例化的函数底层就变成了 function(int & && t),
由 function(num2) 实例化的函数底层则变成了 function(int && && t)。
C++98/03 标准是不支持这种用法的,
C++ 11标准为了更好地实现完美转发,特意为其指定了新的类型匹配规则,
又称为引用折叠规则(假设用 A 表示实际传递参数的类型):
- 当实参为左值或者左值引用(A&)时,函数模板中 T&& 将转变为 A&(A& && = A&);
- 当实参为右值或者右值引用(A&&)时,函数模板中 T&& 将转变为 A&&(A&& && = A&&)。
完美转发的函数模板
万能引用解决了形参的左右值类型的区分
模板函数 forword<T>()
解决了将形参的左右值属性传递到被调用函数
//实现完美转发的函数模板
template <typename T>
void function(T&& t) {
otherdef(forward<T>(t));
}
forword() 函数模板用于修饰被调用函数中需要维持参数左、右值属性的参数
完美转发的总结
在定义模板函数时,
采用右值引用的语法格式定义参数类型,由此该函数既可以接收外界传入的左值,也可以接收右值;
其次,还需要使用 C++11 标准库提供的 forword() 模板函数修饰被调用函数中需要维持左、右值属性的参数。
由此即可轻松实现函数模板中参数的完美转发
注释:
上述资料为个人学习总结笔记