# 右值引用、引用折叠及完美转发

# c++中类型的分类

Each C++ expression (an operator with its operands, a literal, a variable name, etc.) is characterized by two independent properties: a type and a value category. Each expression has some non-reference type, and each expression belongs to exactly one of the three primary value categories: prvalue, xvalue, and lvalue.

在c++中,每个表达式(带有操作数的运算符、文字、变量名等),都有两个独立属性:类型和值类别。每个都属于prvalue、xvalue和xvalue中的一种

详情可以翻阅:https://en.cppreference.com/w/cpp/language/value_category

  • prvalue: 纯右值 ,表达式产生的中间值,不能取地址。
  • glvalue: 广义上的左值
  • xvalue: 一个即将被销毁的值,但是可以被重复使用,如static_cast<char&&>(x)、std::move 等
  1. 右值 = prvalue + xvalue 即纯右值 加上可以被重复使用的即将销往的值(如fun("123")中的"123")
a++,a--
a+b,a-b
1234 // 常量等
  1. 左值 = glvalue + !xvalue 可以简单立即为不是右值的值都是左值
a[n],
a.m

总结:

c++标准把值类型分为三种,但是实际使用时,大概只分为了2种:左值和右值。

右值简单说就是不能获取地址的值,包括表达式产生的中间值和可以被重复使用的临时值; 除了右值以外的其他值就是左值

# 什么是引用?左值引用和右值引用

A reference variable is a "reference" to an existing variable, and it is created with the & operator.

reference是用&创建的一个已经存在变量的引用,也称之为变量的别名,本身不占任何存储空间。 针对左值的引用类型称之为左值引用,根据c++11之前的叫法,也常简称为引用;c++11之后提出的右值的概念,所以将针对右值的引用称之为右值引用,以下是一些基本示例:

struct Data {
    Data(int a_): a(a_){}
    int a;
};

Data a{1};              // 左值
Data& b = a;            // 左值引用
const Data& c = {2};    // 左值引用,右值转为左值时需要const修饰,否则编译报错
Data(3);                // 右值
Data&& d = {4};         // 右值引用,但d作为形参或者表达式时是左值
Data e = {5};           // 右值赋值左值
// Data&& f = a;        // 右值引用指向左值,编译错误

std::cout << std::boolalpha << std::endl;
std::cout << std::is_reference<decltype(a)>::value << std::endl;            // false: 左值不是引用
std::cout << std::is_lvalue_reference<decltype(b)>::value << std::endl;     // true:  左值引用
std::cout << std::is_lvalue_reference<decltype(c)>::value << std::endl;     // true:  const引用也是左值引用
std::cout << std::is_rvalue_reference<decltype(d)>::value << std::endl;     // true:  右值引用
std::cout << std::is_lvalue_reference<decltype(e)>::value << std::endl;     // false: 左值不是引用
std::cout << std::is_reference<decltype(Data(1))>::value << std::endl;      // false: 右值
std::cout << std::is_rvalue_reference<decltype(std::move(e))>::value << std::endl;  // true: std::move返回右值引用

既然有左值和右值、左值引用和右值引用的区别,那么在函数定义上可能也就有了不同的定义方法: 下面提供了三个PrintData函数的重载方法,最大的差异是右值引用形参和左值引用形参的差别,右值引用会匹配右值引用形参如:最后的三个例子 左值会匹配左值引用形参的重载(其他除了const的所有示例)

void PrintData(const Data& data) {
    std::cout << "const left reference:" << data.a << std::endl;
}
void PrintData(Data&& data){
    std::cout << "right reference:" << data.a << std::endl;
}
void PrintData(Data& data){
    std::cout << "left reference:" << data.a << std::endl;
}

// 函数重载
PrintData(a);                       // left reference
PrintData(b);                       // left reference
PrintData(c);                       // const left reference
PrintData(d);                       // left reference       右值引用在作为形参或表达式时是左值,左值引用重载
PrintData(e);                       // left reference
PrintData(Data(5));                 // right reference      右值引用重载
PrintData(std::move(d));            // right reference      右值引用重载
PrintData(static_cast<Data&&>(e));  // right reference      右值引用重载

这里值得注意的一点是: 虽然d声明为一个右值引用,但在表达式中使用时,其会被解释称左值!如果觉得不可理解:那么试想以下,如果d只是一个引用的别名,那么其真名是什么?又或者在控制台打印d的地址,看下是否会包异常,以及输出正确?

# 引用折叠及完美转发

我们都知道右值及右值引用是c++11中引入的一个非常重要的概念,而且由其扩展得到的一系列新特性,比如完美转发,完美转发的意思是可以将输入参数的值和类型原封不动的进行传递(注意是值和类型),c++标准库是怎样实现的完美转发,下面一起看看: 完美转发要保证参数类型不会发生变化,首先介绍std::forward中使用的万能引用和引用折叠后,再给出std::forward的源码,可能会更好理解一些。

# 引用折叠和万能引用

万能引用的意思是指只有一个函数,既要能接受左值引用类型的传参,还要接受右值引用类型的传参。如下先直接给出一个简单的实现,然后来验证下这个是否是真的万能引用:

// 万能引用
template<typename  T>
void Forward_All(T&& t) {
    std::cout << "-----Forward_ALL" << std::endl;
    std::cout << std::is_rvalue_reference<decltype(t)>::value << std::endl;
    std::cout << std::is_lvalue_reference<decltype(t)>::value << std::endl;
}

Forward_All(a);             // a是左值,左值引用          false, true
Forward_All(d);             // 函数形参变为左值,左值引用   false, true
Forward_All(std::move(e));  // move 返回右值引用          true, false
Forward_All(Data(5));       // 右值                     true,false

示例的Forward_All的形参为右值引用形式,但它声明为模版函数,模版函数可以接受左值引用类型和右值引用类型(在c++11之前函数参数全部为左值引用类型):

这里先抛出一个新的概念 引用折叠 ,因为T的不同会产生不同的模版函数特化版本,如参数会变为T& && 和 T&& &&这两种形式,c++11支持的编译器会对这种情况用4条规则做折叠处理,生成只有& 和 && 的引用方式:

  • & + & = &
  • & + && = &
  • && + & = &
  • && + && = && 即引用折叠时只要有左值引用,就折叠为左值引用, 全部是右值引用,才折叠为右值引用
  1. 传入T& ,特化后为:
void Forward_All(T& && t) {
    std::cout << "-----Forward_ALL" << std::endl;
    std::cout << std::is_rvalue_reference<decltype(t)>::value << std::endl;
    std::cout << std::is_lvalue_reference<decltype(t)>::value << std::endl;
}

则根据上面的折叠规则,特化模版变成了左值引用:

void Forward_All(T& t) {
    std::cout << "-----Forward_ALL" << std::endl;
    std::cout << std::is_rvalue_reference<decltype(t)>::value << std::endl;
    std::cout << std::is_lvalue_reference<decltype(t)>::value << std::endl;
}
  1. 传入T&&, 特化后为:
void Forward_All(T&& && t) {
    std::cout << "-----Forward_ALL" << std::endl;
    std::cout << std::is_rvalue_reference<decltype(t)>::value << std::endl;
    std::cout << std::is_lvalue_reference<decltype(t)>::value << std::endl;
}

则根据上面的折叠规则,特化模版变成了右值引用:

void Forward_All(T&& t) {
    std::cout << "-----Forward_ALL" << std::endl;
    std::cout << std::is_rvalue_reference<decltype(t)>::value << std::endl;
    std::cout << std::is_lvalue_reference<decltype(t)>::value << std::endl;
}

总结: 引用折叠是c++编译器内部实现的机制,在c++语法层面是不支持的,各位大侠请不要轻易尝试

# 完美转发

完美转发则主要基于万能引用和引用折叠来实现的,下面是标准库中std::forward的实现机制,在万能引用和引用折叠的基础上,应该比较容易看懂了

// 去除引用类型,返回T原始类型
template< class T > 
struct remove_reference {
  typedef T type;
};
template< class T > 
struct remove_reference<T&> {
  typedef T type;
};
template< class T > 
struct remove_reference<T&&> {
  typedef T type;
};

// 通过引用折叠返回右值引用类型
template<typename T>
T&& Forward(typename std::remove_reference<T>::type&& t){
    return static_cast<T&&>(t);
}
// 通过引用折叠将t左值引用强制转化为右值引用类型
template<typename T>
T&& Forward(typename std::remove_reference<T>::type& t){
    return static_cast<T&&>(t);
}
// 完美转发
std::cout << std::is_lvalue_reference<decltype(Forward<decltype(b)>(b))>::value << std::endl;       // true
std::cout << std::is_rvalue_reference<decltype(Forward<Data&&>(std::move(b)))>::value << std::endl; // true

# 完美转发的应用

  • stl容器的emplace_back等
  • 多模版参数的使用等

# 总结

// TODO(ranky):

* 转载请注明出处