C++11的右值引用移动语义(std::move)和完美转发(std::forward)详解
Posted 彼方丶
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++11的右值引用移动语义(std::move)和完美转发(std::forward)详解相关的知识,希望对你有一定的参考价值。
C++11的右值引用、移动语义(std::move)和完美转发(std::forward)详解
1、源码准备
本文是基于gcc-4.9.0的源代码进行分析,右值引用相关内容是C++11才加入标准的,所以低版本的gcc源码是没有这些相关的内容的,建议选择4.9.0或更新的版本去学习,不同版本的gcc源码差异应该不小,但是原理和设计思想的一样的,下面给出源码下载地址
http://ftp.gnu.org/gnu/gcc
2、C++11右值引用概念
右值引用是C++11众多新特性中的一个,也是最重要的特性之一。很多初学者都感觉右值引用晦涩难懂,其实不然,右值引用只不过是一种新的C++语法,真正理解起来有难度的是基于右值引用延伸出的两种C++11编程技巧,即移动语义和完美转发。
2.1、左值和右值
右值引用可以从字面意思上理解,指的是以引用传递(而非值传递)的方式使用C++的右值。在C++或者C语言中,一个表达式(可以是字面量、变量、对象、函数的返回值等)根据其使用场景不同,分为左值表达式和右值表达式,确切来说C++中左值和右值的概念是从C语言继承过来的。
需要注意的是,左值的英文简写为
lvalue
,右值的英文简写为rvalue
,很多人想当然地认为它们分别是left value
和right value
的缩写,其实这种说法是不正确的
lvalue
是loactor value
的缩写,指的是存储在内存中、有明确存储地址(可寻址)的数据
rvalue
是read value
的缩写,指的是那些可以提供数据值的数据(不一定可以寻址,例如存储于寄存器中的数据)
通常情况下,判断某个表达式是左值还是右值,常用的有以下两种方法:
- 可位于赋值号(=)左侧的表达式就是左值;反之,只能位于赋值号右侧的表达式就是右值。举个例子:
int i = 1;
1 = i; //错误,1不能作为左值
其中,变量i
就是一个左值,而字面量1
就是一个右值。值得一提的是,C++中的左值也可以当做右值使用,例如:
int j = 7; // j是一个左值
i = j; // a、b都是左值,只不过可以将j当做右值使用
- 有名称的、可以获取到存储地址的表达式即为左值;反之则是右值。
以上面定义的变量i
、j
为例,i
和j
是变量名,且通过&i
和&j
可以获得他们的存储地址,因此i
和j
都是左值;反之,字面量1
、7
,它们既没有名称,也无法获取其存储地址(字面量通常存储在寄存器中,或者和代码存储在一起),因此1
、7
都是右值。
由于本文主要讲解右值引用相关知识,因此这里适可而止,不再对左值和右值作深度剖析,感兴趣的读者可自行查阅资料去了解更加具体的内容。
2.2、右值引用
其实早在C++98/03标准中就有引用了,使用"&"表示。但此种引用方式有一个缺陷,即正常情况下只能操作左值,无法对右值添加引用。举例如下:
int a = 10;
int& b = a; //正确
int& c = 10; //错误
如上所示,编译器允许我们为a
(左值)建立一个引用,但不可以为10
(右值)建立引用。因此,C++98/03标准中的引用又称为左值引用。
需要注意的是,虽然C++98/03标准不支持为右值建立非常量左值引用,但允许使用常量左值引用操作右值。也就是说,常量左值引用既可以操作左值,也可以操作右值,例如:
int a = 10;
const int& b = a; //正确
const int& c = 10; //正确
我们知道,右值往往是没有名称的,因此要使用它只能借助引用的方式。这就产生一个问题,实际开发中我们可能需要对右值进行修改(实现移动语义时就需要),显然上面这种引用的方式是行不通的。
为此,C++11标准新引入了另一种引用方式,称为右值引用,用&&
表示。需要注意,和声明左值引用一样,右值引用也必须立即进行初始化操作,且只能使用右值进行初始化,比如:
int a = 10;
int&& a = num; //错误,右值引用不能初始化为左值
int&& a = 10;
和常量左值引用不同的是,右值引用还可以对右值进行修改。例如:
int&& a = 10;
a = 100;
std::cout << a << std::endl; // 输出100
另外值得一提的是,C++语法上是支持定义常量右值引用的,例如:
const int&& a = 10;//编译器不会报错
但这种定义出来的右值引用并无实际用处。一方面,右值引用主要用于移动语义和完美转发,其中前者需要有修改右值的权限;其次,常量右值引用的作用就是引用一个不可修改的右值,这项工作完全可以交给常量左值引用完成。
引用类型 | 可以引用的值类型 | 使用场景 | |||
非常量左值 | 常量左值 | 非常量右值 | 常量右值 | ||
非常量左值引用 | Y | N | N | N | 一般场景均适用 |
常量左值引用 | Y | Y | Y | Y | 常用于类中构建拷贝构造函数 |
非常量右值引用 | N | N | Y | N | 移动语义、完美转发 |
常量右值引用 | N | N | Y | Y | 无实际用途 |
其实,C++11标准中对右值做了更细致的划分,分别称为纯右值(Pure value,简称 pvalue)和将亡值(eXpiring value,简称 xvalue )。其中纯右值就是C++98/03标准中的右值,而将亡值则指的是和右值引用相关的表达式(比如某函数返回的
T&&
类型的表达式)。对于纯右值和将亡值,都属于右值,读者知道即可,不必深究。
3、C++11的移动语义(std::move)和完美转发(std::forward)
3.1、移动语义(std::move)
C++11中的移动语义是通过
std::move
实现的,std::move
并不移动任何东西,它唯一的功能就是将一个传入参数强制转化为右值引用,继而可以通过右值引用使用该值
std::move
源代码如下:
template<typename _Tp>
constexpr typename std::remove_reference<_Tp>::type&&
move(_Tp&& __t) noexcept
{
return static_cast<typename std::remove_reference<_Tp>::type&&>(__t);
}
- 首先,函数参数T&&是一个指向模板参数类型的右值引用,通过引用折叠技术,此参数可以与任何类型的实参匹配(既可以传递左值也可以传递右值,这也是
std::move
主要使用的两种场景)
引用折叠技术:
形式一:T& &、T&& &、T& &&都折叠为T&,用于处理左值,例子如下:
std::string s = “haha”;
std::move(s) 展开为 std::move(std::string& &&),套用上面的规则,折叠之后为 std::move(std::string&)
此时整个std::move被实例化为如下形式:
std::string&& move(std::string& __t)
{ return static_cast<std::string&&>(__t); }
形式二:T&& &&折叠成T&&,用于处理右值,例子如下:
std::move(std::string(“hello”)) 展开为 std::move(std::string&& &&),套用上面的规则,折叠之后为 std::move(std::string&&)
此时整个std::move被实例化为如下形式:
std::string&& move(std::string&& __t)
{ return static_cast<std::string&&>(__t); }
- 对于
std::remove_reference
的使用,我们看一下它的源代码(位于libstdc+±v3\\include\\std\\type_traits中),其实从std::remove_reference
存在于type_traits
文件这一点就可以大致推断出,std::remove_reference
使用了模板元技术,模板元的主要思想为:利用模板特化机制实现编译期条件选择结构,利用递归模板实现编译期循环结构,模板元程序则由编译器在编译器解释运行,但是其也有明显的优缺点,优点是运行时速度极快,缺点是程序很难看懂,容易劝退初学者,这里不对其做深入分析,知道是这样一个东西就行,有兴趣的可以去查阅专业的C++书籍去了解其中的奥秘
源代码如下,作用是将模板_Tp
的引用属性分离(左值引用和右值引用都可以),这样的话使用::type
就可以获得一个没有引用属性的类型了
template<typename _Tp>
struct remove_reference
{ typedef _Tp type; };
template<typename _Tp>
struct remove_reference<_Tp&>
{ typedef _Tp type; };
template<typename _Tp>
struct remove_reference<_Tp&&>
{ typedef _Tp type; };
总结:
std::move
首先通过右值引用传递模板,引用折叠原理将右值经过T&&
传递类型保持不变还是右值,而左值经过T&&
变为普通的左值引用,以保证模板可以传递任意实参,且保持类型不变,然后通过static_cast
进行强制类型转换返回T&&
右值引用,而static_cast
之所以能使用类型转换,是通过std::remove_refrence::type
模板移除T&&
、T&
的引用,获取具体原始类型T
。
3.2、完美转发(std::forward)
C++11中的完美转发是通过
std::forward
实现的,std::forward
会将输入的参数原封不动的传递到下一个函数中,这个“原封不动”指的是,如果输入的参数是左值,那么传递给下一个函数的参数的也是左值;如果输入的参数是右值,那么传递给下一个函数的参数的也是右值。
一个经典的完美转发的场景如下:
template <class... Args>
void forward(Args&&... args)
{
f(std::forward<Args>(args)...);
}
std::forward
源代码如下:
template<typename _Tp>
constexpr _Tp&&
forward(typename std::remove_reference<_Tp>::type& __t) noexcept
{
return static_cast<_Tp&&>(__t);
}
template<typename _Tp>
constexpr _Tp&&
forward(typename std::remove_reference<_Tp>::type&& __t) noexcept
{
static_assert(!std::is_lvalue_reference<_Tp>::value, "template argument"
" substituting _Tp is an lvalue reference type");
return static_cast<_Tp&&>(__t);
}
从源代码中可以看出以下几点:
- 使用
std::forward
时必须说明模板类型(std::forward
无法自己推断出_TP
的类型是什么) - 通过使用
std::remove_reference
去掉了_Tp
的引用属性,以此来区分传入参数是左值引用还是右值引用 - 如果
std::forward
接受的参数为左值的话,则_TP
也是一个左值引用的类型,则调用的是第一个函数,将代码展开之后变为以下形式(假设T为随便一种类型),由前面3.1小节
中提到的引用折叠技术我们可以得出,此时返回值是一个左值引用
T& && forward(typename std::remove_reference<T&>::type& __t) noexcept
{
return static_cast<T& &&>(__t);
}
std::forward
接受的参数为右值的话,则_TP
也是一个右值引用的类型,则调用的是第而个函数,将代码展开之后变为以下形式(假设T为随便一种类型),由前面3.1小节
中提到的引用折叠技术我们可以得出,此时返回值是一个右值引用
T&& forward(typename std::remove_reference<T&&>::type&& __t) noexcept
{
return static_cast<T&& &&>(__t);
}
- 这里需要注意一点,并不是对象被变成右值之后就不能再被使用了,右值引用只是区别于左值的另一种对对象的引用方式,只要不经过移动拷贝(赋值),都可以简单地理解为一次普通的引用,不改变原对象的生命周期,经过移动构造(拷贝)之后可以将右值对象的内容给左值,但是右值对象本身不会被析构,其原有的生命周期仍旧不会改变。
4、总结
本文通过几个简单的例子讲解了C++11右值引用的基本概念,从源代码的角度分析了基于右值引用延伸出的两种C++11编程技巧(移动语义和完美转发)
- 移动语义(
std::move
)并不移动任何东西,它唯一的功能就是将一个传入参数转化为右值引用,这个转换是强制的,无条件的 - 完美转发(
std::forward
)则是只有在它的参数绑定到一个右值上的时候,它才将传入参数转换为一个右值 - 需要注意的是,在运行期间,
std::move
和std::forward
没有做任何事情,他们没有产生需要在运行期执行的代码,一字节都没有
以上是关于C++11的右值引用移动语义(std::move)和完美转发(std::forward)详解的主要内容,如果未能解决你的问题,请参考以下文章