浅谈C++右值/Universal引用和std::move/forward()实现原理

C++ 11为实现移动语义而引入了右值引用,其主要被用于表达当具有右值引用入参的函数被执行后其原实参将处于无效状态:

struct A { };
void f(A &&a) { destroyData(a); } // 表明f()将消耗入参的数据,执行后原实参将处于无效状态
void f(const A &a) {}
A a;
f(std::move(a));  // 执行后a将处于无效状态

右值引用和移动语义是std::uniqueptr()等标准库函数的实现基础,其在现代C++中有很重要的作用。

本文接下来将讨论C++右值引用和语法与之相似的模板函数Universal引用入参,以及std::move/forward()的实现原理。

1. 右值引用与Universal引用

C++表达式结果可以被分为lvalue (left) / rvalue (right) / xvalue (eXpiring) /glvalue (generalized) / prvalue (pure), 它们之间的关系如下图所示:

expression_type.png

其中,左值lvalue为可以出现在赋值表达式左侧的值(表示身份,一般为可以取址的变量),右值rvalue则为只能出现在赋值表达式右边的值(表示值,一般为临时对象,不可取址)。 此外,xvalue为即将被销毁的值,如右值引用类型的函数返回值;prvalue则为xvalue之外的rvalue,比如非右值引用的普通函数返回值。

右值引用为只可以绑定到右值上的引用,如​A &&a = A()​,右值引用的最终消费者主要为移动构造函数和移动赋值函数,其具体用法可以参考C++ Primer等教材,此处不再赘述。 此外,具有右值引用入参的函数也是右值引用的消费者,此时右值引用的主要目的是表明当函数被执行后其原实参数据将被消耗殆尽。

1.1. 右值引用变量本身是左值

右值引用变量本身是左值,因此不可将一个右值引用变量赋值给另外一个右值引用变量,如下:

A &&ra1 = A();
A &&ra2 = ra1;  //  error: cannot bind rvalue reference of type ‘A&&’ to lvalue of type ‘A’
A &&ra2 = std::move(ra1);

C++这样规定的原因为​确保rvalue在被move前一定处于有效状态,从而可以被正常使用​。 若不如此规定的话,将难以确认rvalue的有效时间,比如:

A &&ra = A();
f(ra);  // 若允许A &&ra2 = ra, 则f(ra)之后ra所值对象将处于无效状态
A a2(std::move(ra));

而如此规定的话,则可以保证f(ra)执行后ra仍然处于有效状态,进而确保a2可以获取到ra有效值。

1.2. 模板函数Universal引用入参

Universal引用的在语法上很像右值引用,但其实两者完全不同。 Universal引用是一种函数模板参数,其主要被用于参数完美转发(保留原始引用/const属性等信息),如下:

template <typename T>
void f(T &&t) {
    doSometing();
    g(std::forward<T>(t));  // 将t从f()完美转发至g()
}

2. std::move/forward()实现原理

2.1. std::move()

move()被用于生成右值引用,其是基于staticcast实现的,具体GCC实现源码如下:

template <typename T>
typename remove_reference<T>::type &&move(T &&t)  // 引用折叠,且在T中保留reference/const属性
{
   // 由static_cast完成从左值引用到rvalue引用的转换,从lvalue到rvalue的转换本来就是合情合理的需求
   // A &&ra = std::move(a)本质上为A &&ra = static_cast<A &&>(a);
   return static_cast<typename remove_reference<T>::type &&>(t);
}

其中,removereference()的作用为移除引用,其实现原理为模板类偏特化,具体GCC实现源码如下:

template<typename _Tp> struct remove_reference { using type = _Tp; };  // 通用版本
template<typename _Tp> struct remove_reference<_Tp&> { using type = _Tp; };  // 针对lvalue引用的偏特化版本
template<typename _Tp> struct remove_reference<_Tp&&> { using type = _Tp; };  // 针对rvalue引用的偏特化版本

staticcast<T&&>()的结果为右值,因此可以被绑定到右值引用上。 ​staticcast可以接受任意类型入参的原因为其是关键字而不是函数​。 ​此处的staticcast仅被用于在编译时绕过类型检测系统,并没有对应的运行时代码(其它某些情况下staticcast会生成运行时代码),因此右值引用最底层的实现和普通引用一样都是指针。​ 另外,removereference()也仅在编译时生效,也没有对应的运行时代码。 因此,​move()并不会生成运行时代码​,本质上只是绕过类型检测系统。

2.2. std::forward()

forward()需要在使用时显示指明参数类型,其实现为相互重载的两个函数模板​, 两者函数体相同,其目的是确保显示实例化后的两个forward()实例可以匹配到可能的左/右值两种实参(move()则通过universal引用来接受这两种可能的实参),进而实现完美转发左/右值。 forward()是基于staticcast和引用折叠来实现的,具体的GCC实现源码如下:

template<typename _Tp> _GLIBCXX_NODISCARD constexpr _Tp&& forward(typename std::remove_reference<_Tp>::type& __t) noexcept { return static_cast<_Tp&&>(__t); }  // forward1()
template<typename _Tp> _GLIBCXX_NODISCARD constexpr _Tp&& forward(typename std::remove_reference<_Tp>::type&& __t) noexcept { return static_cast<_Tp&&>(__t); }  // forward2()

forward()在使用时已被实例化了,此处的Tp为具体类型​,Tp&&也不是universal引用,但仍然可以进行引用折叠。 以int类型为例,Tp的可能类型和对应的转发效果如下

_Tp实例化类型 forward()形参类型 forward()实参类型 返回值类型
int int& (forward1), int&& (forward2) (const) int (匹配forward1), (const) int& (匹配forward1), int&& (匹配forward2) (const) int&, (const) int&, int&&
int& int& (forward1), int&& (forward2) (const) int (匹配forward1), (const) int& (匹配forward1), int&& (匹配forward2) (const) int&, (const) int&, int&&
int&& int& (forward1), int&& (forward2) (const) int (匹配forward1), (const) int& (匹配forward1), int&& (匹配forward2) (const) int&, (const) int&, int&&

其中,forward1()和forward2()为上述forward()实现源码中的两个函数模板。 从上表可以看出,forward()确实可以完美转发所有实参的引用和const属性。

std::forward()在使用时必须显示指明参数类型而无法依赖函数模板参数推导的根本原因为​无法根据入参中的removereference<T>::type来反推T​, 因为一个type可能对应到多个T:

int i = 0;
std::forward(i);
// error: no matching function for call to ‘forward(int&)’
// /usr/include/c++/12/bits/move.h:77:5: note: candidate: ‘template<class _Tp> constexpr _Tp&& std::forward(typename remove_reference<_Tp>::type&)’
// /usr/include/c++/12/bits/move.h:77:5: note:   template argument deduction/substitution failed:
// note:   couldn’t deduce template parameter ‘_Tp’

template<typename T> void f(typename std::remove_reference<T>::type t) {}
f(i);
// error: no matching function for call to ‘f(int&)’
// note: candidate: ‘template<class T> void f(typename std::remove_reference<_Tp>::type)’
// note:   template argument deduction/substitution failed:
// note:   couldn’t deduce template parameter ‘T’