自学内容网 自学内容网

【C++11入门】新特性总结之移动语义(右值、右值引用、std::move()...)

C++11出现的右值相关语法可谓是很多C++程序员难以理解的新特性,不少人知其然而不知其所以然,面试被问到时大概就只知道可以减少开销,但是为什么减少开销、减少了多少开销、什么时候用...这些问题也不一定知道。本节我们就探讨一下右值与移动语义的知识。

✨前言:深浅拷贝

为什么我们要再讲深浅拷贝的问题呢?因为右值引用以及移动语义解决的就是拷贝构造的性能问题。我们先定义一个用于本节测试的类:

class Test{
    int *p;
    int n;
public:
    void DeepTest(Test& other){}
    void ShallowTest(Test& other){}
};

在这里 为了给函数起不同的名字分清深和浅界限,我们使用普通成员函数来代替拷贝构造函数。

浅拷贝的实现:直接交你作业

void ShallowTest(Test& other){
    this->n=other.n;
    this->p=other.p;
}

浅拷贝面临的问题就是,拷贝过后,被拷贝的对象的指针赋予了其它对象,多个对象公享一个指针地址,会造成一旦一个对象释放指针,其他对象访问时会造成非法操作的问题,以及一个修改,全部受影响的问题。(我就是拿你作业添上我的名字直接去交,我要是看哪个答案不顺眼,我就改,这个作业纸上写的不止你一个人的名字,纸上有名的任何人都可以进行操作)

深拷贝的实现:我照着抄一份

void DeepTest(Test& other){
    this->n=other.n;
    this->p=new int[n];
    for(int i=0;i<n;i++){
        this->p[i]=other.p[i];
    }
}

 深拷贝是开辟同样大的堆区空间,然后将空间的相对逻辑位置的值设置成拷贝对象的值。(深拷贝就是抄你作业,我自己买纸,在我的合法作业纸上照着你的作业写一份,不影响你的作业。我改自己的不影响其他人的任何作业,这太好了。)

但是此时有一个问题就是我抄作业需要花时间(时间开销)费纸笔(空间开销)。我直接在你作业纸上写我的名字就没有这种问题了,快得很,高效!(所以如果没有成员指针的话,我们更倾向于使用浅拷贝)。一个可行的办法就是,我把你名字擦掉(取消原对象访问权),写上我的名字(转移指针访问权限的所有权)。这样就保证了作业(内存)永远只被一个对象使用了。

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

🦎认识:左值右值

左值(lvalue)和右值(rvalue)是用于描述表达式的值的性质的。

左值(lvalue):指的是有持久地址的表达式,通常表示可以被修改的对象。因为时常出现在赋值运算符左侧,而被称为左值。特点是:可以取地址(&),表示一个存储的对象。

右值(rvalue):指的是没有持久地址的临时值,通常表示无法修改的对象。常出现在赋值运算符的右侧。特点是:不能取地址。通常是字面量,临时对象或表达式的结果。

但是有个特例:字符串字面量,是个左值,存储在静态区,可以去地址,拥有持久地址。而且字符串字面量是唯一的不算右值的字面量。

🦖认识:右值引用T&&

认识右值引用之前,我们先来看一下左值引用是什么?

左值引用:

左值引用是指向左值的引用,使用符号&来声明。

int a = 10;  
int &ref = a;  // ref是a的左值引用  
ref = 20;      // 通过引用修改a,a的值变为20

更多知识请了解: C++入门Day1-CSDN博客

右值引用:

C++11引入了右值引用,可以通过&&符号来声明,它允许你绑定到右值,使得资源移动变得可能,这在性能上非常有优势。

void func(int &&x) {  // x是一个右值引用  
    x += 10;  
}  

func(5); // 5是一个右值,可以绑定到右值引用

移动拷贝构造函数就是以右值引用为参数来实现的。移动的意思是转移所有权。由于右值都是临时的值(严谨来说,右值其实也不一定是临时值),临时值释放后也就不再持有属性的所有权,因此这相当于转移资源所有权的行为。 

左值引用和右值引用可以结合在模板中使用,以实现完美转发(perfect forwarding)。通过std::forward,可以在模板函数中正确地转发参数的值类别。这个我们后面说,先给个代码看着。

#include <iostream>  
#include <utility>  

template<typename T>  
void forwardingFunc(T&& arg) { // arg可以是左值或右值  
    process(std::forward<T>(arg)); // 保持arg的值类别  
}  

int main() {  
    int x = 10;  
    forwardingFunc(x); // 传递左值  
    forwardingFunc(20); // 传递右值  
}

🐳认识:移动语义std::move()

除了大多数临时值充当的右值,一部分左值也很适合转移所有权:

void func(){
    Test ts;
    if(/*****/){
        ans=ts;//最终语句了,原来的左值对象没啥用了,我想将对象的一切权力交给ans上
    }
    return ;
}

在这里,以后不再使用的变量,也被期望能够移交权限,我也想他也能充当“右值”的角色 。如果我直接像上面那样写,肯定是赋值构造函数,不是移动赋值构造。那我想让他移交权限怎么办呢?别着急,std::move()来帮你:

void func(){
    Test ts;
    if(/*****/){
        ans=std::move(ts);//移动赋值
    }
    return ;
}

🦕普通进阶:深入探究右值与右值引用

右值与右值引用的关系:

右值引用只是表示接受一个右值,右值引用本身可能是一个拥有持久地址的左值。

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

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

答案:a是一个左值,std::move(a)是一个右值,b是一个左值。

你唯一不理解的就是b为什么是一个左值,我们要知道,b有没有地址,是不是用完就找不到了,答案是显然的,有地址,用完依旧存在并且可以根据变量名寻址。现在的b就是一个右值引用类型的左值。只不过由于引用类型的实质const属性它不能再出现在赋值运算符的左边而已。 

结论:右值引用类型只是用于匹配右值,而并非表示一个右值。因此,尽量不要声明右值引用类型的变量,而只在函数形参使用它以匹配右值。

左值、右值、纯右值、将亡值

前面对移动语义的认识我们都是基于C++98时左值、右值概念,而C++11对左值、右值类别被重新进行了定义,因此现在我们重新认识一下新的类别。

C++11使用下面两种独立的性质来区别类别:

  1. 拥有身份:指代某个非临时对象。
  2. 可被移动:可被右值引用类型匹配。

每个C++表达式只属于三种基本值类别中的一种:左值 (lvalue)、纯右值 (prvalue)、将亡值 (xvalue)

  • 拥有身份且不可被移动的表达式被称作 左值 (lvalue) 表达式,指持久存在的对象或类型为左值引用类型的返还值。
  • 拥有身份且可被移动的表达式被称作 将亡值 (xvalue) 表达式,一般是指类型为右值引用类型的返还值。
  • 不拥有身份且可被移动的表达式被称作 纯右值 (prvalue) 表达式,也就是指纯粹的临时值(即使指代的对象是持久存在的)。
  • 不拥有身份且不可被移动的表达式无法使用。

如此分类是因为移动语义的出现,需要对类别重新规范说明。例如不能简单定义说右值就是临时值(因为也可能是std::move过的对象,该代指对象并不一定是临时值)。

//1.左值:可以取地址的变量(如a)。
左值是指能够取得地址的值,可以出现在赋值语句的左侧。它通常表示持久的对象,比如变量和数组的元素。
例如:int a = 10; 这里的a是一个左值,因为你可以取它的地址(&a)。

//2.右值:不能取地址的临时值(如20)。
右值是指不能取得地址的值,通常表示一个临时的对象,或者是字面量(如常量)。它通常出现在赋值语句的右侧。
例如:int b = 20; 中的20是一个右值,它没有持久的存储。

//3.纯右值:更为临时的右值(如5 + 3)。
纯右值(prvalue)是右值的一种,指那些不具备持久性和不能被引用的值。它们通常是字面量、临时对象等。
例如:5 + 3的结果是一个纯右值,因为它是一个临时计算结果,没有名字,也不能被取地址。

//4.将亡值:资源即将被转移的对象(如std::move(myObject))。
将亡值(xvalue)是另一种右值,它表示一个将要被移动的对象,通常用于资源管理(尤其是移动语义)中。这样的对象在某些情况下仍然有可用的状态,但即将被转移其所有权。
例如,调用std::move(myObject)时,返回的是一个xvalue,表示myObject的资源即将被转移。

右值包括:纯右值、将亡值 。而且实际上C++ std::move函数的实现原理就是的强转成右值引用类型并返还之,因此该返还值会被判断为将亡值,更宽泛的说是被判定为右值。

 建议:

1. 我们应该首先把编写右值引用类型相关的任务重点放在对象的构造、赋值函数上。从源头上出发,在编写其它代码时会自然而然享受到了移动构造、移动赋值的优化效果。

2. 形参:从优化的角度上看,若参数有支持移动构造(或移动赋值)的类型,应提供左值引用和右值引用的重载版本。移动开销很低时,只提供一个非引用类型的版本也是可以接受的。

3. 返还值:不要且没必要编写返还右值引用类型的函数,除非有特殊用途。

🐉模板进阶:万能引用

模板的:万能引用:

模板中的万能引用又称为转发引用。

万能引用一般用于模板编程方面,但也有能用于普通编程的“特殊的万能引用”中。

什么是万能引用呢?万能引用就是在函数传参时,可以接收右值类型也可以接收左值类型的一个引用类型。万能引用的好处在于,不需要对接收左值的函数和和接收右值的函数分别写一个函数进行函数重载了。没必要提供多个重载版本,缩减了代码量。

template<typename T>  
void func(T&& arg) {  
    // ...  
}

在这个例子中,T&&被称为万能引用或转发引用。这里的关键是:

  • 当传入左值时,T会被推导为左值引用(T&),所以arg最终的类型是左值引用(T& &&简化为T&)。
  • 当传入右值时,T会被推导为非引用类型,arg的类型为右值引用(T&&)。

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

template<class T>
void func(T&& val);

void func(int a);

以上两个函数构成重载关系,当调用func函数时,传入long类型的数据,不会匹配int 版本而是匹配到万能引用的版本。因为模板本身就能够提供更多的匹配,也就是所谓的贪婪,只要你不定义单独的类型参数的函数,那么你就都来进入我万能引用的模板函数里面吧(全都到碗里来~)。

万能引用通常用于完美转发(perfect forwarding),这允许我们将参数的值类别(左值或右值)完美地传递给另一个函数。例如,结合std::forward可以实现这个目的:

#include <utility> // 包含 std::forward  

template<typename T>  
void wrapper(T&& arg) {  
    // 完美转发 arg 到另一个函数  
    anotherFunction(std::forward<T>(arg));  
}  

void anotherFunction(int& x) {  
    // 处理左值  
}  

void anotherFunction(int&& x) {  
    // 处理右值  
}

在这个例子中,wrapper函数通过完美转发,将arg的值类别转发给anotherFunction,从而能够正确处理左值和右值。 

特殊的"万能引用"

只有在模板的上下文中,T&&才能被视为万能引用;在非模板上下文中,T&&始终是右值引用。那么普通函数有能兼容接收左值和右值的参数类型嘛,有的:

void test(const T& variable);

在这里,参数就既可以接收左值,又可以接收右值了。 

🐦‍🔥模板进阶:引用折叠

引用折叠(Reference collapsing)是C++11引入的一种特性,它用于处理多个引用类型结合在一起时的类型推导。引用折叠的规则是:当我们用引用类型(尤其是右值引用)作为模板参数时,C++会按照特定的规则来简化这些嵌套的引用类型。

当两个引用类型结合时,引用折叠遵循以下规则:

  1. 如果两个引用都是左值引用,那么它们折叠为一个左值引用:

    T& & → T&
  2. 如果一个引用是左值引用,另一个是右值引用,那么它们折叠为一个左值引用:

    T& && → T&
  3. 如果一个引用是右值引用,另一个是左值引用,那么它们折叠为一个左值引用:

    T&& & → T&
  4. 如果两个引用都是右值引用,那么它们折叠为一个右值引用:

    T&& && → T&&
  5. 总结:但凡出现左值引用,全被折叠为左值引用,只有引用全为右值引用时才会折叠为一个右值引用。
#include <iostream>  
#include <type_traits>  

template<typename T>  
void referenceExample(T&& arg) {  
    // 打印 arg 的类型  
    std::cout << "Type of arg: " << typeid(arg).name() << std::endl;  
}  

int main() {  
    int x = 42;  

    referenceExample(x);            // T推导为 int&,arg的类型为 int&  
    referenceExample(10);           // T推导为 int,arg的类型为 int&&  
    referenceExample(std::move(x)); // T推导为 int,arg的类型为 int&&  

    return 0;  
}

我们上文的模板的万能引用的原理就是T对传入参数的推导可以推导出引用类型,从而可以同时接收左值和右值。 在这个代码中,referenceExample 函数能够接受左值和右值,具体如何折叠取决于传入的参数类型。在使用模板时,参考折叠的特性尤为重要,它有助于简化和规范化参数传递的行为。

引用折叠常与完美转发(perfect forwarding)一起使用,以确保在函数模板中能够正确保持参数的值类别(左值或右值),这通常会伴随着std::forward的使用。这对于编写高效和灵活的C++代码至关重要。

🐧模板进阶:完美转发std::forward<T>

我们上文所有的模板进阶模块都在提到完美转发,那么完美转发到底是个什么东西呢?

背景-概念:

背景:在函数模板中,传入的参数可能是左值或右值。如果在转发参数时不保留其原有的值类别,可能会导致不必要的对象复制或移动,从而影响性能。完美转发的目的是在转发参数时,避免这种不必要的开销。

概念:完美转发(Perfect Forwarding)是C++11引入的一个重要概念,主要用于在函数模板中保持参数的值类别(left value or right value),确保传递给其他函数或对象的参数与原始参数的值类别完全一致。这是通过使用万能引用(Universal References)和std::forward来实现的。

实现-解析:

完美转发的关键是使用模板参数和std::forward。下面是一个实现完美转发的示例:

#include <iostream>  
#include <utility> // 包含 std::forward  

// 示例函数,处理左值  
void process(int& x) {  
    std::cout << "Processing lvalue: " << x << "\n";  
}  

// 示例函数,处理右值  
void process(int&& x) {  
    std::cout << "Processing rvalue: " << x << "\n";  
}  

// 包装函数,使用完美转发  
template<typename T>  
void wrapper(T&& arg) {  
    // 使用 std::forward 完美转发  
    process(std::forward<T>(arg));  
}  

int main() {  
    int x = 5;  

    wrapper(x);           // 传入左值  
    wrapper(10);         // 传入右值  
    wrapper(std::move(x)); // 传入右值(通过 std::move)  

    return 0;  
}
  1. 万能引用:在模板函数wrapper中,T&& arg被称为万能引用(或转发引用),它根据参数arg的值类别推导出T的类型。若传入左值,T推导为int&,即arg为左值引用;若传入右值,T推导为int,即arg为右值引用。

  2. std::forward:通过std::forward<T>(arg),我们能够将arg以其原本的值类别转发给process函数。若arg是左值,std::forward<T>(arg)将返回一个左值引用;若arg是右值,它将返回一个右值引用。

注意事项:

  • 完美转发通常使用在需要转发构造函数参数或在泛型代码中传递参数时。
  • std::forward只在模板函数中与模板类型T结合使用时有效。它不能用于非模板函数。

感谢大家观看


原文地址:https://blog.csdn.net/U2396573637/article/details/142891599

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!