实现std::move和std::forward
你所需要的基础知识
通用引用和右值引用
在实现std::move()
以及std::forward()
前,我们要对通用引用以及右值引用之间做一个基本的了解。一般来讲,当我们见到&&
的时候,我们都会认为这是个右值引用,是用来绑定到右值上一个引用类型。但是如果这种情况出现模板参数的推导时,事情就会发生变化。
1 | template <typename T> |
上面这种情况如果在不知道通用引用的情况下,我们会认为这个函数的参数只能绑定到右值上,因为很明显,&&
代表的是右值引用。但是实际上下面的两个调用都可以通过编译并且给出运行结果。
1 | std::string value = "Hello world!"; |
也就是说神奇的事情发生了,Func
即接受左值,又接收右值,通常来讲我们只会在const&
上看到这种情况,但是这里居然也发生了相同的事情。要想理解这种行为,我们就不得不了解一个全新的知识点,也就是引用折叠(reference collapsing)。
引用折叠
我们知道,在C++里面,我们是没有办法声明引用的引用的,因为引用实际上是另一个变量的别名,是一种指针的语法糖。要是按照权威的书籍解释的话,也就是C++ primer中所述的
因为引用本身不是一个对象,所以不能定义引用的引用。
不过话虽然是这么说,但是实际上编译器不得不处理这种情况。例如上面的Func(value)
,实际上如果进行展开的话,编译器会看到这么一个东西。
1 | void Func(string& &¶m); |
这里先忽略掉为什么T
为什么会被推导为string&
(感兴趣的话可以去看看effective modern C++),我们可以看到在类型推导中出现了左值引用的右值引用。但是又因为是没有引用的引用这一说法,所以编译器就会做一点手脚,来确保程序可以运行下去,而所做的这一手脚也就是所谓的引用折叠。其规则如下:
发生引用的引用情况下,如果任一引用为左值引用,那么结果是左值引用,否则就是右值引用。
好,当我们记住这个之后,我们就可以来实现std::move()
以及std::forward()
了。
GO! 来进行具体实现吧
std::move()
std::move()
所做的事情很简单,就是不管传进来的是什么东西,我都把它给转成右值引用。这个的实现并不需要我们之前所讲的引用折叠。其实现如下
1 | template<typename T> |
可以看到就是利用std::remove_reference
把参数本身的引用性去了,然后将值类型static_cast
到具体的右值引用。
std::forward()
std::forward()
做的事就有点不一样了,它可以看作一个有选择的std::move()
, 常用于函数参数的转发中。因为虽然我们平时嘴上喊着左值引用,右值引用什么的,但实际上,无论什么参数,它都是左值,而这些引用绑定到左值上还是右值上,与它们本身是左值毫无关系。所以为了保证其能够保持所谓的左值性与右值性,我们才需要std::forward()
函数。
它的行为具体表现为,一个函数的实参要是被右值初始化,那么我们就把对应的参数转化为右值引用。代码如下
1 | template<typename T> |
可以看到与std::move()
的不同就是最后在static_cast
的时候,std::forward()
接受的参数是T&&
,这样的话,通过引用折叠,我们就可以将参数完美的转发出去。
还是用上面的Func(value)
和Func(std::move(value))
来进行举例子。
1 | Func(value)-> T==string& -> static_cast<string& &&> -> 折叠! -> static_cast<string&> 大功告成,传进来左值,转化到左值! |
这里T
的推导规则要是不懂的话,还是推荐之前的effective modern c++,里面把规则说的很明白了。