# 右值引用、引用折叠及完美转发
# 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 等
- 右值 = prvalue + xvalue 即纯右值 加上可以被重复使用的即将销往的值(如fun("123")中的"123")
a++,a--
a+b,a-b
1234 // 常量等
- 左值 = 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条规则做折叠处理,生成只有& 和 && 的引用方式:
- & + & = &
- & + && = &
- && + & = &
- && + && = && 即引用折叠时只要有左值引用,就折叠为左值引用, 全部是右值引用,才折叠为右值引用 。
- 传入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;
}
- 传入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):
* 转载请注明出处