右值引用、移动语义、完美转发详解

概述:右值引用、移动语义、完美转发

右值引用、移动语义、完美转发

右值引用的定义

左值:有名字的内存中有自己地址的值,可以取地址
右值:可以浅显地理解成等号右边的值,在内存没有确定存储地址、没有变量名,结束就会销毁的值,简单来说右值就是临时对象,生命周期只在当前行有效,不能取地址

右值可分为:纯右值 和 将亡值

常见的右值:

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() 模板函数修饰被调用函数中需要维持左、右值属性的参数。

由此即可轻松实现函数模板中参数的完美转发

注释:

上述资料为个人学习总结笔记

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值