C++11:右值引用与万能引用

引言

右值引用和万能引用都是C++11引入的新概念;由于都与&&有关系,所以这里将他们放到一个专题进行分析论述。

一般情况下,以赋值运算符=为界,=左边称左值lvalue,=右边称右值rvalue。例如:int value = 3;value 就是lvalue, 3就是rvalue。但这只是形式而已,不能完全概括左值和右值,因为在某些情况下,这条规则是不生效的。例如:int aValue = value; value 出现在=右边,但是value却不是右值。

除rvalue和lvalue外,现代C++还存在多个属性分类,他们是将亡值xvalue,纯右值prvalue和泛左值glvalue,彼此之间的关系如图一所示。

ca07a3ac412d4729b597f5e02630d431.png
图一   属性分类图

如图一所示,泛左值glvalue = 左值lvaue + 将亡值xvalue;右值rvalue = 纯右值prvalue + 将亡值xvalue。变化的是C++11之前的左值变为C++11标准中的纯右值;C++11的左值与以前的标准基本一致。

关于左值,C89标准有这样论述:the definition of lvalue as an object locator,也就是说一个可以取地址且不会立刻消亡的对象就是一个左值,所以lvalue中的l可理解为location。

纯右值prvalue则是C89标准的右值,其表示没有明确地址的只读对象,prvalue中的r可理解为read。纯右值要么是字面值常量;要么是求值结果相当于字面值或匿名临时变量。非引用返回的临时变量,表达式产生的临时变量,原始字面值,lambda表达式都是纯右值。

将亡值是C++11引入的与右值引用相关将要销毁的对象,可以被移动的值。左值和右值两种最明显的区别是:左值有持久的状态,而右值要么是纯右值(字面值,表达式求值过程中创建的临时对象)要么是将要消亡可移动的临时对象。

引用由C++98标准引入,由于引用只能绑定左值;所以Morden C++(C++11及以后标准)引入右值引用解决绑定右值的问题,右值引用可以说是对传统引用的扩充。经Morden C++扩充后,传统C++的引用,在Morden C++中变成左值引用;Morden C++引入右值引用,从而引出移动语义;

左值引用

C++98标准引入左值引用,左值引用通过&定义,其绑定规则可总结如下:

  • 一个左值绑定绑定到左值引用;
  • 无法把一个右值绑定到左值引用,除非是const类型的左值引用

具体可参考举例:

int b = 5;
int& a = b;      // 可以正常编译
int& a1 = 500;   // 无法通过编译
const int& a2 = 500; // 可以通过编译

右值引用

所谓右值引用就是必须绑定到右值的引用,我们一般通过&&而不是&来获得右值引用。所以可看出右值引用可绑定到将要销毁的对象(将亡值),把将亡值绑定到右值引用相当于右值引用的资源移动到了一个对象,这样可延长将亡值的生命周期。例如:

struct Circle
{
    Circle(const double& circle) : circle(circle) {}
    double circle;
};

Circle&& newCircle()
{
    return Circle(1.0);
}

除了将亡值以外右值引用还可以绑定纯右值(即字面值常量),例如:

double&& radius = 5.30;

右值引用可以绑定将亡值或纯右值,但是无法绑定左值。例如:

int b = 5;
int&& a = b;      // 无法编译

常量右值引用,可以引用纯右值和将亡值,但是能绑定左值。例如:

int b = 5;
const int&& a = 100;
const int&& width = newCircle().circle;
const int&& c = b; // 无法编译 

万能引用

万能引用的英文是universal reference,除了“万能引用”,还有另外一种翻译“未定义引用”。 万能引用的一般定义方式为T&&,注意此处T为可演绎的模板类型,不是普通意义上的类型。万能引用一般存在于下述两种场景:

  • 传参类型为T&& 的模板函数,且必须存在类型推导
  • auto && 的引用声明,同样必须存在类型推导
void f(int &&value)     // “&&” means rvalue reference

int && var = someValue; // “&&” means rvalue reference

auto&& var2 = var1;     //  “&&” means universal reference

template<typename T>
void f(std::vector<T>&& param);  // “&&” means rvalue reference
 
template<typename T>
void f(T&& param);               // “&&” mean universal reference

对于万能引用,需要注意的有两点:

  • 声明形式必须为T && 或 auto &&
  • 只有推导发生,&&才称为万能引用,否则算作右值引用

为了更好的说明上述两个注意事项,我们举例分析:

例一:T与容器结合

template<typename T>
void fun(std::vector<T>&& arg)   // “&&” means rvalue reference

此处,&&为右值引用,因为入参的声明类型为std::vector<T>&&,而不是T&&

例二:const T&&

template<typename T>
void fun(const T&& arg);               // “&&” means rvalue reference

由于存在const修饰T&&,导致此处&&也是右值引用,而非万能引用。

例三:成员函数T&&

template <class T, class Allocator = allocator<T>>
class vector 
{
public:
    ...
    void push_back(T&& x);       // fully specified parameter type ⇒ no type deduction;
    ...                          // &&  rvalue reference
};

虽然此处声明为T&&,但是在模板实例化时T类型就已经确定了,因此T不需要编译是类型推导。所以push_back声明中的&&为右值引用而非万能引用。

例四:变长参数模板Args&&... args

template <class T, class Allocator = allocator<T> >
class vector 
{
public:
    ...
    template <class... Args>
    void emplace_back(Args&&... args); // deduced parameter types ⇒ type deduction;
    ...                                // && ≡ universal references
};

此举例中,虽然模板实例化时T的类型可确定,但是每个args的类型也还是需要逐个推导。因此此处&&为万能引用而非右值引用。

引用折叠

左值和右值在模板推导时是存在差异的,对于类型T的lvalue,模板会推导为T&类型;但是对于类型T的右值,模板会推导为T。

template<typename T>
void func(T&& arg);
 
...
 
int x;
 
...
 
func(10);                           // invoke func on rvalue
func(x);                            // invoke func on lvalue

对于函数调用func(10),func<T>中的T会推导为int,所以func的形式类似于

void func(int&& arg);             // 右值变量func的实例化

但是对于func(x),func<T>中的T会推导为int&,所以func的形式类似于

void func(int& && arg);           // 左值变量func的实例化

但是func(int& && arg)这是一个不合法的函数声明。为了解决这个问题C++11引入了“引用折叠”。

由于存在左值引用和右值,所以就会存在4形式的引用组合,他们分别是:lvalue reference to lvalue reference, lvalue reference to rvalue reference, rvalue reference to lvalue reference, and rvalue reference to rvalue reference。这四种形式的引用折叠规则:

  • rvalue reference to rvalue reference引用折叠后变成 rvalue reference
  • 其他三种组合方式,引用折叠后均变成rvalue reference。

表一:引用折叠规则
模板入参声明入参类型引用折叠的类型
lvalue referencelvalue referencelvalue reference
lvalue referencervalue referencelvalue reference
rvalue referencelvalue referencelvalue reference
rvalue referencervalue referencervalue reference

同左值引用和右值引用一样,万能引用同样需要初始化。基于引用折叠规则,万能引用的初始化规则可总结如下:

  • 如果万能引用由左值表达式初始化,那么万能引用将变成一个左值引用,左值初始表达式可是可取址的表达式,左值引用表达式例如T&, const T&。
  • 如果万能引用由右值初始化,那么万能引用将变成一个右值引用,右值的初始表达式可是纯右值或将亡值。
int&& var1 = 100;   // var1 is a rvalue reference, but var1 is a lvalue
auto&& var2 = var1; // var2 is an lvalue reference, since we can take the address of var1

总结

综述所述,假设T为一个具体类型,那么关于引用可总结如下:

  • 左值引用,使用T&,只能绑定左值;
  • 右值引用,使用T&&,只能绑定右值;
  • 常量左值引用,使用const T&,既可以绑定左值,也可以绑定右值;
  • 已命名的右值引用,编译器会认为是一个左值;
  • 常量右值引用,可以引用常量右值和非常量右值。

同样对应万能引用和引入折叠,C++11折叠规则更简单:

  • 引用折叠时只要有左值引用出现,折叠后的类型就是左值引用,否则为右值引用。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值