C++右值引用与转移和完美转发

C++右值引用与转移和完美转发

1、右值引用

1.1右值

lvalue 是 loactor value 的缩写,rvalue 是 read value 的缩写

左值是指存储在内存中、有明确存储地址(可取地址)的数据;

右值是指可以提供数据值的数据(不可取地址);

通过描述可以看出,区分左值与右值的便捷方法是:可以对表达式取地址(&)就是左值,否则为右值 。所有有名字的变量或对象都是左值,而右值是匿名的。

示例:

double a = 13.14;
double b = 1314;
a += b;

一般情况下,位于 = 前的表达式为左值,位于 = 后边的表达式为右值。也就是说例子中的 a, b 为左值,13.14,1314 为右值。a+=b 是一种特殊情况,在这个表达式中 a, b 都是左值,因为变量 b 是可以被取地址的,不能视为右值。

C++11 中右值可以分为两种:一个是将亡值( xvalue, expiring value),另一个则是纯右值( prvalue, PureRvalue):

纯右值:非引用返回的临时变量、运算表达式产生的临时变量、原始字面量和 lambda 表达式等
将亡值:与右值引用相关的表达式,比如,T&& 类型函数的返回值、 std::move 的返回值等。

int value = 1314;

在上面的语句中,value 是左值,520 是字面量也就是右值。其中 value 可以被引用,但是 520 就不行了,因为字面量都是右值。

1.2右值引用

C++11 增加了一个新的类型,称为右值引用( R-value reference),标记为 &&。右值引用就是对一个右值进行引用的类型。因为右值是匿名的,所以我们只能通过引用的方式找到它。无论声明左值引用还是右值引用都必须立即进行初始化,因为引用类型本身并不拥有所绑定对象的内存,只是该对象的一个别名。通过右值引用的声明,该右值又“重获新生”,其生命周期与右值引用类型变量的生命周期一样,只要该变量还活着,该右值临时量将会一直存活下去。

示例:

#include <iostream>
using namespace std;

int&& value = 520;
class Test
{
public:
    Test()
    {
        cout << "construct: my name is jerry" << endl;
    }
    Test(const Test& a)
    {
        cout << "copy construct: my name is tom" << endl;
    }
};

Test getObj()
{
    return Test();
}

int main()
{
    int a1;
    //int &&a2 = a1;        // error
    //Test& t = getObj();   // error
    Test && t = getObj();
    const Test& t = getObj();
    return 0;
}
在上面的例子中 int&& value = 520; 里面 520 是纯右值,value 是对字面量 520 这个右值的引用。
在 int &&a2 = a1; 中 a1 虽然写在了 = 右边,但是它仍然是一个左值,使用左值初始化一个右值引用类型是不合法的。
在 Test& t = getObj() 这句代码中语法是错误的,右值不能给普通的左值引用赋值。
在 Test && t = getObj(); 中 getObj() 返回的临时对象被称之为将亡值,t 是这个将亡值的右值引用。
const Test& t = getObj() 这句代码的语法是正确的,常量左值引用是一个万能引用类型,它可以接受左值、右值、常量左值和常量右值。

1.3深拷贝时的性能优化

#include <iostream>
using namespace std;

class Test
{
public:
    Test() : m_num(new int(100))
    {
        cout << "construct: my name is jerry" << endl;
    }

    Test(const Test& a) : m_num(new int(*a.m_num))
    {
        cout << "copy construct: my name is tom" << endl;
    }

    // 添加移动构造函数
    Test(Test&& a) : m_num(a.m_num)
    {
        a.m_num = nullptr;
        cout << "move construct: my name is sunny" << endl;
    }

    ~Test()
    {
        delete m_num;
        cout << "destruct Test class ..." << endl;
    }

    int* m_num;
};

Test getObj()
{
    Test t;
    return t;
}

int main()
{
    Test t = getObj();
    cout << "t.m_num: " << *t.m_num << endl;
    return 0;
};

结果如下:

construct: my name is jerry
move construct: my name is sunny
destruct Test class ...
t.m_num: 100
destruct Test class ...

通过修改,在上面的代码给 Test 类添加了移动构造函数(参数为右值引用类型),这样在进行 Test t = getObj(); 操作的时候并没有调用拷贝构造函数进行深拷贝,而是调用了移动构造函数,在这个函数中只是进行了浅拷贝,没有对临时对象进行深拷贝,提高了性能。

如果不使用移动构造,在执行 Test t = getObj() 的时候也是进行了浅拷贝,但是当临时对象被析构的时候,类成员指针 int* m_num; 指向的内存也就被析构了,对象 t 也就无法访问这块内存地址了。

在测试程序中 getObj() 的返回值就是一个将亡值,也就是说是一个右值,在进行赋值操作的时候如果 = 右边是一个右值,那么移动构造函数就会被调用。移动构造中使用了右值引用,会将临时对象中的堆内存地址的所有权转移给对象t,这块内存被成功续命,因此在t对象中还可以继续使用这块内存。

对于需要动态申请大量资源的类,应该设计移动构造函数,以提高程序效率。需要注意的是,我们一般在提供移动构造函数的同时,也会提供常量左值引用的拷贝构造函数,以保证移动不成还可以使用拷贝构造函数。

1.4&&什么时候是右值引用

template<typename T>
void f(T&& param);
void f1(const T&& param);
f(10); 	   //1
int x = 10;//2
f(x); 	   //3
f1(x);	   //4
第 1 行中,对于 f(10) 来说传入的实参 10 是右值,因此 T&& 表示右值引用
第 3 行中,对于 f(x) 来说传入的实参是 x 是左值,因此 T&& 表示左值引用
第 4 行中,f1(x) 的参数是 const T&& 不是未定引用类型,不需要推导,本身就表示一个右值引用
int main()
{
    int x = 520, y = 1314;
    auto&& v1 = x;          //1
    auto&& v2 = 250;        //2
    decltype(x)&& v3 = y;   //3 error
    cout << "v1: " << v1 << ", v2: " << v2 << endl;
    return 0;
};

第 1 行中 auto&& 表示一个整形的左值引用
第 2 行中 auto&& 表示一个整形的右值引用
第 3 行中 decltype(x)&& 等价于 int&& 是一个右值引用不是未定引用类型,y 是一个左值,不能使用左值初始化一个右值引用类型。

由于上述代码中存在 T&& 或者 auto&& 这种未定引用类型,当它作为参数时,有可能被一个右值引用初始化,也有可能被一个左值引用初始化,在进行类型推导时右值引用类型(&&)会发生变化,这种变化被称为引用折叠。在 C++11 中引用折叠的规则如下:

通过右值推导 T&& 或者 auto&& 得到的是一个右值引用类型

通过非右值(右值引用、左值、左值引用、常量右值引用、常量左值引用)推导 T&& 或者 auto&& 得到的是一个左值引用类型

int&& a1 = 5;   //1
auto&& bb = a1; //2
auto&& bb1 = 5; //3

int a2 = 5;     //4
int &a3 = a2;   //5
auto&& cc = a3; //6
auto&& cc1 = a2;//7

const int& s1 = 100;//8
const int&& s2 = 100;//9
auto&& dd = s1; //10
auto&& ee = s2; //11

const auto&& x = 5; //12

第 2 行:a1 为右值引用,推导出的 bb 为左值引用类型
第 3 行:5 为右值,推导出的 bb1 为右值引用类型
第 6 行:a3 为左值引用,推导出的 cc 为左值引用类型
第 7 行:a2 为左值,推导出的 cc1 为左值引用类型
第 10 行:s1 为常量左值引用,推导出的 dd 为常量左值引用类型
第 11 行:s2 为常量右值引用,推导出的 ee 为常量左值引用类型
第 12 行:x 为右值引用,不需要推导,只能通过右值初始化

#include <iostream>
using namespace std;

void printValue(int &i)
{
    cout << "l-value: " << i << endl;
}

void printValue(int &&i)
{
    cout << "r-value: " << i << endl;
}

void forward(int &&k)
{
    printValue(k);
}

int main()
{
    int i = 520;
    printValue(i);
    printValue(1314);
    forward(250);

    return 0;
};

l-value: 520
r-value: 1314
l-value: 250

根据测试代码可以得知,编译器会根据传入的参数的类型(左值还是右值)调用对应的重置函数(printValue),函数 forward () 接收的是一个右值,但是在这个函数中调用函数 printValue () 时,参数 k 变成了一个命名对象,编译器会将其当做左值来处理。

左值和右值是独立于他们的类型的,右值引用类型可能是左值也可能是右值。
编译器会将已命名的右值引用视为左值,将未命名的右值引用视为右值。
auto&&或者函数参数类型自动推导的T&&是一个未定的引用类型,它可能是左值引用也可能是右值引用类型,这取决于初始化的值类型(上面有例子)。
通过右值推导 T&& 或者 auto&& 得到的是一个右值引用类型,其余都是左值引用类型。

2、转移和完美转发

2.1move

在 C++11 添加了右值引用,并且不能使用左值初始化右值引用,如果想要使用左值初始化一个右值引用需要借助 std::move () 函数,使用std::move方法可以将左值转换为右值。使用这个函数并不能移动任何东西,而是和移动构造函数一样都具有移动语义,将对象的状态或者所有权从一个对象转移到另一个对象,只是转移,没有内存拷贝。

从实现上讲,std::move 基本等同于一个类型转换:static_cast<T&&>(lvalue);,函数原型如下:

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));
}
class Test
{
publicTest(){}
    ......
}
int main()
{
    Test t;
    //decltype(x) && v1 = t;          // error
    decltype(x) && v2 = move(t);    // ok
    return 0;
}

在第 4 行中,使用左值初始化右值引用,因此语法是错误的
在第 5 行中,使用 move() 函数将左值转换为了右值,这样就可以初始化右值引用了。

#include <iostream>
#include <vector>
#include <string>
using namespace std;


int main(int argc,char *argv[])
{
    vector<string> dd={
        "dddddd",
        "dddddd",
        "dddddd",
        "dddddd",
    };
    
    vector<string> cc=move(dd);

    decltype(dd) && bb=move(cc);

    cout<<"cc.size: "<<cc.size()<<"  dd.size: "<<dd.size()<<endl;

    cout<<"cc.size: "<<cc.size()<<endl;
    dd=move(cc);
    cout<<"cc.size: "<<cc.size()<<"  dd.size: "<<dd.size()<<endl;

    
    
    return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qCbi7uDt-1620299826745)(C:\Users\wei\AppData\Roaming\Typora\typora-user-images\image-20210506185804586.png)]

如果不使用 std::move,拷贝的代价很大,性能较低。使用 move 几乎没有任何代价,只是转换了资源的所有权。如果一个对象内部有较大的堆内存或者动态数组时,使用 move () 就可以非常方便的进行数据所有权的转移。另外,我们也可以给类编写相应的移动构造函数(T::T(T&& another))和和具有移动语义的赋值函数(T&& T::operator=(T&& rhs)),在构造对象和赋值的时候尽可能的进行资源的重复利用,因为它们都是接收一个右值引用参数。

注意:

使用move后,被move的类的数据转移到了新类上,被move的类的大小变为0。而decltype(dd) && bb=move(cc);右值引用则不会是原来的类清空。

2.forward

右值引用类型是独立于值的,一个右值引用作为函数参数的形参时,在函数内部转发该参数给内部其他函数时,它就变成一个左值,并不是原来的类型了。如果需要按照参数原来的类型转发到另一个函数,可以使用 C++11 提供的 std::forward () 函数,该函数实现的功能称之为完美转发。

// 函数原型
template <class T> T&& forward (typename remove_reference<T>::type& t) noexcept;
template <class T> T&& forward (typename remove_reference<T>::type&& t) noexcept;

// 精简之后的样子
std::forward<T>(t);
#include <iostream>
using namespace std;

template<typename T>
void printValue(T& t)
{
    cout << "l-value: " << t << endl;
}

template<typename T>
void printValue(T&& t)
{
    cout << "r-value: " << t << endl;
}

template<typename T>
void testForward(T && v)
{
    printValue(v);
    printValue(move(v));
    printValue(forward<T>(v));
    cout << endl;
}

int main()
{
    testForward(520);
    int num = 1314;
    testForward(num);
    testForward(forward<int>(num));
    testForward(forward<int&>(num));
    testForward(forward<int&&>(num));

    return 0;
}

testForward(520); 函数的形参为未定引用类型 T&&,实参为右值,初始化后被推导为一个右值引用
printValue(v); 已命名的右值 v,编译器会视为左值处理,实参为左值
printValue(move(v)); 已命名的右值编译器会视为左值处理,通过 move 又将其转换为右值,实参为右值
printValue(forward<T>(v));forward 的模板参数为右值引用,最终得到一个右值,实参为 右值
testForward(num); 函数的形参为未定引用类型 T&&,实参为左值,初始化后被推导为一个左值引用
printValue(v); 实参为左值
printValue(move(v)); 通过 move 将左值转换为右值,实参为右值
printValue(forward<T>(v));forward 的模板参数为左值引用,最终得到一个左值引用,实参为左值
testForward(forward<int>(num));forward 的模板类型为 int,最终会得到一个右值,函数的形参为未定引用类型 T&& 被右值初始化后得到一个右值引用类型
printValue(v); 已命名的右值 v,编译器会视为左值处理,实参为左值
printValue(move(v)); 已命名的右值编译器会视为左值处理,通过 move 又将其转换为右值,实参为右值
printValue(forward<T>(v));forward 的模板参数为右值引用,最终得到一个右值,实参为右值
testForward(forward<int&>(num));forward 的模板类型为 int&,最终会得到一个左值,函数的形参为未定引用类型 T&& 被左值初始化后得到一个左值引用类型
printValue(v); 实参为左值
printValue(move(v)); 通过 move 将左值转换为右值,实参为右值
printValue(forward<T>(v));forward 的模板参数为左值引用,最终得到一个左值,实参为左值
testForward(forward<int&&>(num));forward 的模板类型为 int&&,最终会得到一个右值,函数的形参为未定引用类型 T&& 被右值初始化后得到一个右值引用类型
printValue(v); 已命名的右值 v,编译器会视为左值处理,实参为左值
printValue(move(v)); 已命名的右值编译器会视为左值处理,通过 move 又将其转换为右值,实参为右值
printValue(forward<T>(v));forward 的模板参数为右值引用,最终得到一个右值,实参为右值

参考:爱编程的大丙

  • 7
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
转发引用(forwarding reference)和右值引用(rvalue reference)是C++中两个相关但不完全相同的概念。 转发引用(forwarding reference)是C++11引入的一种特殊的引用类型,使用模板类型推导结合右值引用语法(&&)表示。它的主要特点是它的类型是根据实参的值类别(左值还是右值)来推导得出的。当使用转发引用声明函数模板参数时,可以接受左值和右值,并将其原始类型和值类别保持不变。转发引用常常与完美转发(perfect forwarding)一起使用,用于在函数模板中传递参数。 右值引用(rvalue reference)是C++11引入的另一种引用类型,使用&&表示,主要用于绑定到临时对象、右值表达式和可以被移动的对象。右值引用允许对右值进行特殊操作,例如移动语义和完美转发。通过右值引用,可以将资源所有权从一个对象转移到另一个对象,提高代码的效率。 区别: - 转发引用是一种特殊的引用类型,在函数模板中使用,根据实参的值类别来推导其类型。而右值引用则是一种普通的引用类型,用于绑定到临时对象、右值表达式和可以被移动的对象。 - 转发引用常常用于实现完美转发,保持原始参数的类型和值类别。而右值引用主要用于实现移动语义和转移资源所有权。 - 转发引用在函数模板中使用,可以接受左值和右值。右值引用可以绑定到右值,但不能绑定到左值。 - 转发引用使用模板类型推导来推导其类型,可以根据实参的值类别来确定是左值引用还是右值引用右值引用是通过使用&&来声明的,表示特定的引用类型。 需要注意的是,转发引用和右值引用可以在某些情况下产生相似的效果,但它们的语义和使用场景有所不同。 希望能解答你的问题!如果还有其他问题,请随时提问。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值