【C++进阶笔记】右值引用和完美转发

本文介绍了C++中的右值引用,包括lvalue、rvalue和xvalue的概念,以及如何使用std::move转换。接着,文章讨论了右值引用带来的性能优化,如在处理函数参数时避免不必要的拷贝。为了解决多参数传递时的问题,C++11引入了完美转发,通过std::forward保持参数的原始引用类型。文章还提醒了在使用完美转发时需要注意的潜在问题,例如当同一对象被多次转发时可能引发的错误。
摘要由CSDN通过智能技术生成

右值引用

C++中的值类别(Value Category)

一般而言,C++中将值分为两种类别:左值(lvalue)和右值(rvalue)。有一种细的分类方式,将其分成了三类:lvalue,xvalue,prvalue。这里的xvalue指的是,lvalue转的rvalue和rvalue转的lvalue,prvalue是指纯右值,逻辑上存在,可能会被编译器优化的rvalue,这个后面再详细说。先重点说lvalue和rvalue:

C++中的部分概念很难定义,手册中也都是以示例的方式给出。下面的定义,也是只是我自己的理解。

  • lvalue:有读写权限的内存地址
  • rvalue:只有可读权限的内存地址,或者说真实存在,但我们不可见的内存地址

lvalue的例子

std::string str("123");  // 可读可写
int i = 5;

rvalue的例子

字面值:42truenullptr// 只能读
函数返回值:std::string fun();   // 如果不写成这样,std::string str = fun(); 我们都看不见这个内存地址

但是如果我们想操作rvalue怎么办,C++11新增的std::move可以解决这个问题。首先为什么我们非得操作rvalue,因为我们想获取更多的性能上的好处。比如:

void fun(const std::string& str) {
    // something
}

std::string long_string();    // 返回一个很长的std::string

fun(long_string());

将long_string()的返回值传递给fun的时候,会调用一次std::string的拷贝构造函数,由于这个string很长,性能开销很大。所以C++11就通过右值引用的方式来解决这个问题。

右值引用的例子

修改fun来支持右值引用

class Widget {
public:
void fun(std::string&& str) {
    p_str_.swap(str);
}
private:
    std::string p_str_;
}

std::string long_string();    // 返回一个很长的std::string

Widget widget;
widget.fun(long_string());

此时在fun中我们就可以直接操作long_string()返回的rvalue了,比如swap操作,效率就会高很多。

xvalue

lvalue和rvalue都可以转成xvalue,xvalue是可以随意破坏内存地址的一种特殊lvalue。lvalue可以通过std::move,表明这个内存地址可以随意操作。将rvalue传递给右值引用方法之后,他就会变成xvalue,因为只有这样我们才可以操作这个内存。

int i = 10;
std::move(i);   // rvalue

void fun(std::string&& str) {
    // 可以调用str.swap方法操作str
}

右值引用的问题

假如fun函数接收两个string类型的参数,如下:

void fun(const std::string& str1, const std::string& str2);
void fun(const std::string& str1, std::string&& str2);
void fun(std::string&& str1, const std::string& str2);
void fun(std::string&& str1, std::string&& str2);

这样就可以支持各种情况的右值引用了,但是如果是3个参数,4个呢?排列组合,难道要写8个方法,16个方法?C++11引入了完美转发,来解决这个问题。

完美转发

完美转发的标准写法:

std::string str;
foo(str);                     // fun收到string类型,lvalue

template <typename T>
void foo(T&& value) {
    fun(std::forward<T>(value));
}

C++ 11标准为了更好地实现完美转发,特意为类型推到指定了新的类型匹配规则,又称为引用折叠规则(假设用 A 表示实际传递参数的类型):

  • 当实参为左值或者左值引用(A&)时,函数模板中 T&& 将转变为 A&(A& && = A&)
  • 当实参为右值或者右值引用(A&&)时,函数模板中 T&& 将转变为 A&&(A&& && = A&&)

这样,fun和foo收到参数类别就一致了。

解决右值引用的问题

void fun1(const std::string& str);
void fun1(std::string&& str);
void fun2(const std::string& str);
void fun2(std::string&& str);

template <typename T1, typename T2>
void foo(T1&& v1, T2&& v2) {
    fun1(std::forward<T1>(v1));
    fun2(std::forward<T2>(v2));
}

上面代码,有一个风险,就是如果传递给foo的是同一个string,可能就会有问题。如下测试代码:

#include <iostream>

void fun1(std::string& str) {
  std::cout << "fun1 &: " << str << std::endl;
  str = "fun1";
}

void fun1(std::string&& str) {
  std::cout << "fun1 &&: " << str << std::endl;
  std::string tmp;
  tmp.swap(str);
  str = "fun1 &&";
}

void fun2(std::string& str) {
  std::cout << "fun2 &: " << str << std::endl;
}

void fun2(std::string&& str) {
  std::cout << "fun2 &&: " << str << std::endl;
}

template <typename T1, typename T2>
void foo(T1&& v1, T2&& v2) {
    fun1(std::forward<T1>(v1));
    fun2(std::forward<T2>(v2));
}

int main() {
  std::string str = "123";
  foo(std::move(str), std::move(str));
  return 0;
}

输出结果

fun1 &&: 123
fun2 &&: fun1 &&

参考

C++11完美转发及实现方法详解

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值