前几天看了一篇文章《4行代码看看右值引用》 觉得写得不错,但是觉得右值引用的内容还有很多可以去挖掘学习,所以总结了一下,希望能对右值引用有一个更加深层次的认识
一、几个基本概念
1.1左值和右值
左值和右值的区分标准在于能否获取地址。
最早的c++中,左值的定义表示的是可以获取地址的表达式,它能出现在赋值语句的左边,对该表达式进行赋值。但是修饰符const的出现使得可以声明如下的标识符,它可以取得地址,但是没办法对其进行赋值:
const int& i = ;
右值表示无法获取地址的对象,有常量值、函数返回值、Lambda表达式等。无法获取地址,但不表示其不可改变,当定义了右值的右值引用时就可以更改右值。
1.2 左值引用和右值引用
传统的c++引用被称为左值引用,用法如下:
int i = ;
int & ii = I;
C++ Primer Plus 第6版18.1.9中说到,c++11中增加了右值引用,右值引用关联到右值时,右值被存储到特定位置,右值引用指向该特定位置,也就是说,右值虽然无法获取地址,但是右值引用是可以获取地址的,该地址表示临时对象的存储位置。语法如下:
int && iii = ;
1.3 左值引用和右值引用的汇编代码
以下汇编都是x86汇编
写一段简单的语句,看其汇编
int i = ;
int & ii = i;
0x080483f3 movl $0x1,-0x10(%ebp)
0x080483fa lea -0x10(%ebp),%eax
0x080483fd mov %eax,-0x8(%ebp)
第一句是将1赋值给i,第二句将i的地址放入eax中,第三句将eax中的值传给ii。可见引用就是从一个变量处取得变量的地址,然后赋值给引用变量。
再看一句右值引用的汇编
int && iii = ;
0x08048400 mov $0xa,%eax
0x08048405 mov %eax,-0xc(%ebp)
0x08048408 lea -0xc(%ebp),%eax
0x0804840b mov %eax,-0x4(%ebp)
第一句将10赋值给eax,第二句将eax放入-0xc(%ebp)处,前面说到“临时变量会引用关联到右值时,右值被存储到特定位置”,在这段程序中,-0xc(%ebp)便是该临时变量的地址,后两句通过eax将该地址存到iii处。
通过上述代码,我们还可以发现,在上述的程序中-0x4(%ebp)存放着右值引用iii,-0x8(%ebp)存放着左值引用,-0xc(%ebp)存放着10,而-0x10(%ebp)存放着1,左值引用和右值引用同int一样是四个字节(因为都是地址)
同时,我们可以深入理解下临时变量,在本程序中,有名字的1(名字为i)和没有名字的10(临时变量)的值实际是按同一方式处理的,也就是说,临时变量根本上来说就是一个没有名字的变量而已。它的生命周期和函数栈帧是一致的。也可以说临时变量和它的引用具有相同的生命周期。
1.4 const左值引用
如果写如下代码,定义一个左值引用,将其值置为一个常量值,则会报错:
int & i = ;
原因很明显,左边是一个左值引用,而右边是一个右值,无法将左值引用绑定到一个右值上。
但是如果是一个const的左值引用,是可以绑定到右值上的。即如下写法是符合语法规范的:
const int & i = ;
这段程序的汇编代码如下:
0x08048583 mov $0xa,%eax
0x08048588 mov %eax,-0x8(%ebp)
0x0804858b lea -0x8(%ebp),%eax
0x0804858e mov %eax,-0x4(%ebp)
易知-0x4(%ebp)处存放着i,-0x8(%ebp)处则存放着临时对象10,程序将10的地址存放到了i处。看到这里会发现const引用在绑定右值时和右值引用并没有什么区别。
1.5 左值引用和右值引用的相互赋值
能将右值引用赋值给左值引用,该左值引用绑定到右值引用指向的对象,在早期的c++中,引用没有左右之分,引入了右值引用之后才被称为左值引用,所以说左值引用其实可以绑定任何对象。这样也就能理解为什么const左值引用能赋予常量值。
int&& iii = ;
int& ii = iii; //ii等于10,对ii的改变同样会作用到iii
二、右值引用和移动语义
在旧的c++中,出现了很多的不必须要的拷贝,因为在某些情况下,对象拷贝完之后就下来就销毁了。新标准引入了移动操作,减少了很多的复制操作,而右值引用正式为了支持移动操作而引入的新的引用类型。
2.1 标准库move函数
根据右值引用的语法规则可知,不能将右值引用绑定到一个左值上,c++11引入右值引用,并且提供了move函数,用来获得绑定到左值上的右值引用,此函数定义在头文件utility中。
Int &&iii = move(ii)
调用move之后,必须保证除了对ii复制或销毁它外,我们将不再使用它,在调用move之后,我们不能对移动源后对象做任何假设。
2.2 模板实参推断和引用
为了理解move函数的实现,首先需要理解模板实参推断和引用。
当左值引用作为参数时,看几个例子:
template<class T> void f1(T&) {}
f1(i) //i是一个int,模板参数类型T是int
f1(ci) //ci是一个const int,模板参数T是const int
fl() //错误:传递给一个&参数的实参必须是一个左值
如果函数的参数是const的引用时:
template<class T> void f2(const T&) {}
f2(i) //i是一个int,模板参数类型T是int,因为非const可以转化为const
f2(ci) //ci是一个const int,模板参数T是int
f2() //看前面,const的引用可以绑定右值,T是int
当参数是右值引用时,
template<class T> void f3(T&&) {}
f3() // T是int
2.3 引用折叠和右值引用参数
按照道理来说,f3(i)是应该不正确的,因为无法将右值引用绑定到一个左值上,但是,c++中有两个正常绑定规则的例外,允许这种绑定。这两个例外规则是move正确工作的基础
例外1:右值引用的类型推断。当我们将一个左值传递给函数的右值引用作为参数时(函数参数为T&&),编译器推断模板类型参数为实参的左值引用类型,,因此,调用f3(i)时,T被推断为int&,而不是int。并且,模板函数中对参数的改变会反映到调用时传入的实参。
通常,我们不能直接定义一个引用的引用,但是同过类型别名(使用typedef)和模板间接定义是可以的。
例外2:引用折叠。当定义了引用的引用时,则这些引用形成了“折叠”,所有的情况下(除了一个例外),引用会折叠成一个普通的左值引用类型。这个例外就是右值引用的右值引用:
l X& &&、X& &&、X&& &都折叠成X&
l 类型X&& &&折叠成X&&
2.4 理解右值引用折叠和右值引用类型推断
对于函数f3而言,根据右值引用类型推断规则可以知道如下结果:
f3(i) //实参是左值,模板参数T是int&
f3(ci) //实参是左值,模板参数T是一个const int&
但是当T被推断为int&时,函数f3会实例化成如下的样子:
void f3<int&>(int& &&)
然后根据右值引用折叠规则可以知道,上述实例化方式应该被折叠成如下样子:
void f3<int&>(int&)
这两个规则导致了两个重要的结果:
l 如果一个函数参数是一个指向模板类型参数的右值引用,如T&&,则它能被绑定到一个左值,且
l 如果实参是一个左值,则推断出的模板实参类型将时一个左值引用,且函数参数被实例化为一个普通左值引用参数(T&)
值得注意,参数为T&&类型的函数可以接受所有类型的参数,左值右值均可。在前面,同样介绍过,const的左值引用做参数的函数同样也可以接受所有类型的参数。
2.5 当右值引用作为函数模板参数时
通过前面,我们了解到当右值引用作为函数模板参数时,类型T会被推断为一个引用类型。这一特性会影响模板函数内部的代码,看下面一段代码:
template<class T>
void f3(T&& val)
{
T t = val;
t = fcn(t);
if(val == t){…}
}
假如以左值i来调用该函数,那么T被推断为int&,将t绑定到val之上,对t的更改就被应用到val,则if判断条件永远为true。
右值引用通常用于两种情况,模板转发其实参、模板被重载。下面都会介绍到
前面说到,const左值引用做参数和右值引用做参数一样,是可以匹配所有的参数类型,但当重载函数同时出现时,右值引用做参数的函数绑定非const右值,const左值引用做参数的函数绑定左值和const右值(非const右值就是通过右值引用来引用的右值,虽然无法获取右值的地址,但是可以通过定义右值引用来更改右值):
Template<class T> void f(T&&) //绑定到非const右值
Template<class T> void f(const T&) //左值和const右值
2.6 move函数实现
vs2017中move函数的定义如下
using remove_reference_t = typename remove_reference<_Ty>::type;
template<class _Ty>
constexpr remove_reference_t<_Ty>&& move(_Ty&& _Arg) _NOEXCEPT
{
return (static_cast<remove_reference_t<_Ty>&&>(_Arg));
}
template<class _Ty>
struct remove_reference
{ // remove reference
using type = _Ty;
}; template<class _Ty>
struct remove_reference<_Ty&>
{ // remove reference
using type = _Ty;
}; template<class _Ty>
struct remove_reference<_Ty&&>
{ // remove rvalue reference
using type = _Ty;
};
使用右值引用作为参数,前面说过,可以匹配所有类型。以下两种方式都是正确的:
string s1(“s1”),s2
s2 = move(string(“bye!”)) //_Ty推断为string
s2 = move(s1) //_Ty推断为string&
至于remove_reference就好理解了
综上,可以发现move函数不管传入什么类型参数,不管是左值还是右值,都会返回其右值引用类型
三、转发
某些函数需要将其中一个或多个实参连同类型不变地转发给其他函数,在这种情况下,我们需要保持被转发实参的所有性质,包括实参是否是const的、以及是左值还是右值。
有如下的两个函数,在flip中调用f:
void f(int v1, int &v2) {
cout << v1 << " " << ++v2 << endl;
}
template <typename F, typename T1, typename T2>
void flip(F f, T1 t1, T2 t2) {
f(t2, t1);
}
我们会发现f会改变第二个参数的值,但是通过flip调用f之后就不会改变
f(, i)
flip(f, j, )
模板被实例化成如下:
void flip (void(*fcn)(int, int&), int t1, int t2);
j的值被拷贝到t1中,所以flip中的f只会改变t1,而不会改变j
3.1 定义能保持类型信息的函数参数
如果将flip的参数定义成右值引用,根据上面描述过的规则,当给flip传入引用时,T1被推断为int&,t1则被折叠成int&,完美保持了实参的类型。
template <typename F, typename T1, typename T2>
void flip(F f, T1&& t1, T2&& t2) {
f(t2, t1);
}
但是当函数f接受右值引用作为参数的时候,flip就不能正常工作了
void f(int &&I, int j)
{
cout << i << “ ” << j << endl;
}
flip(g,i,) //错误,不能从一个左值实例化int&&
注意,这里的f和g都不是模板函数,所以说前面提到的右值引用作为参数时的两个例外不能成立。所以这里是错误的
3.2 使用forward保持类型信息
Forward需要显示提供实参类型,返回该实参类型的右值引用(在前面可以看到,右值引用是可以赋值给左值引用的)
void flip(F f, T1&& t1, T2&& t2) {
f(forward<T2>(t2), forward<T1>(t1));
}
当显式实参T是int&&时,forward返回int&& &&,折叠成int&&。当当显式实参T是int&时,forward返回int& &&,折叠成int&。所以说forward完美保持了参数的类型。
c++右值引用以及使用的更多相关文章
-
C++右值引用浅析
一直想试着把自己理解和学习到的右值引用相关的技术细节整理并分享出来,希望能够对感兴趣的朋友提供帮助. 右值引用是C++11标准中新增的一个特性.右值引用允许程序员可以忽略逻辑上不需要的拷贝:而且还可以 ...
-
C++ 11 中的右值引用
C++ 11 中的右值引用 右值引用的功能 首先,我并不介绍什么是右值引用,而是以一个例子里来介绍一下右值引用的功能: #include <iostream> #include &l ...
-
图说函数模板右值引用参数(T&;&;)类型推导规则(C++11)
见下图: 规律总结: 只要我们传递一个基本类型是A④的左值,那么,传递后,T的类型就是A&,形参在函数体中的类型就是A&. 只要我们传递一个基本类型是A的右值,那么,传递后,T的类型就 ...
-
c++11的右值引用、移动语义
对于c++11来说移动语义是一个重要的概念,一直以来我对这个概念都似懂非懂.最近翻翻资料感觉突然开窍,因此记下.其实搞懂之后就会发现这个概念很简单,并无什么高深的地方. 先说说右值引用.右值一般指的是 ...
-
VS2012 error C2664: “std::make_pair”:无法将左值绑定到右值引用
在vs2012(c++)make_pair()改动: C++: template <class T1, class T2> pair<V1, V2> make_pair(T1& ...
-
右值引用、move与move constructor
http://blog.chinaunix.net/uid-20726254-id-3486721.htm 这个绝对是新增的top特性,篇幅非常多.看着就有点费劲,总结更费劲. 原来的标准当中,参数与 ...
-
【转】C++11 标准新特性: 右值引用与转移语义
VS2013出来了,对于C++来说,最大的改变莫过于对于C++11新特性的支持,在网上搜了一下C++11的介绍,发现这篇文章非常不错,分享给大家同时自己作为存档. 原文地址:http://www.ib ...
-
move语义和右值引用
C++11支持move语义,用以避免非必要拷贝和临时对象. 具体内容见收藏中的“C++右值引用” .
-
[转载] C++11中的右值引用
C++11中的右值引用 May 18, 2015 移动构造函数 C++98中的左值和右值 C++11右值引用和移动语义 强制移动语义std::move() 右值引用和右值的关系 完美转发 引用折叠推导 ...
-
C++ 11 右值引用
C++11中引入的一个非常重要的概念就是右值引用.理解右值引用是学习“移动语义”(move semantics)的基础.而要理解右值引用,就必须先区分左值与右值. 注意:左值右值翻译可能有些问题 *L ...
随机推荐
-
Oracle 多行转多列,列值转为列名
前段时间做调查问卷,客户创建自定义问卷内容,包括题目和选项内容; 之后需要导出问卷明细,,,,麻烦来咯 于是到网上到处搜索,没有直接结果;于是又找各种相似的,,终于功夫不负有心人 然后最终自己写出来了 ...
-
Session阻塞 读写锁引发的小问题
引子 我们来看两个ajax方法,先后的问题,会有什么样的结果? Javascript: $(function () { //第一个 $.ajax({ type: "POST&qu ...
-
生成freemarker静态页面的工具类
package cn.bocai.pc.util; import java.io.BufferedWriter;import java.io.File;import java.io.FileOutpu ...
-
ASP.NET MVC another entity of the same type already has the same primary key value
ASP.NET MVC项目 Repository层中,Update.Delete总是失败 another entity of the same type already has the same pr ...
-
mysql 创建函数set global log_bin_trust_function_creators=TRUE;
<pre name="code" class="html">set global log_bin_trust_function_creators=T ...
-
大数据开发 | MapReduce介绍
1. MapReduce 介绍 1.1MapReduce的作用 假设有一个计算文件中单词个数的需求,文件比较多也比较大,在单击运行的时候机器的内存受限,磁盘受限,运算能力受限,而一旦将单机版程序扩展 ...
-
Android 常见知识整理(1)
Android Support V4, V7, V13的作用与用法 http://blog.csdn.net/hh2000/article/details/39718623 2. 开源项目 注解 ...
-
兼容IE、火狐、谷歌的页面关闭事件
<html> <head> <script language="javascript"> var blnCheckUnload = true; ...
-
ansible基础-playbooks
1. playbooks介绍 如果说ansible的modules是工具,inventory配置文件是原材料,那么playbook就是一封说明书,这里会记录任务是如何如何执行的,当然如果你愿意,这里也 ...
-
使用try和catch捕获异常
Java程序在执行过程中如果出现异常,会自动生成一个异常对象,该异常对象将被自动提交给JVM,当JVM接收到异常对象时,会寻找能处理这一异常的代码,并把当前异常对象交给其处理,这一过程称为捕获(cat ...