上篇文章我们讲了移动语义相关知识
本篇文章我们介绍下std::forward,我们会讲到引用折叠,完美转发,及std::forward与std::move进行比较
1 引用折叠
我们都知道通常情况下不能将一个右值引用绑定到左值上,即:
int i = 0;
int &&ii = i; // error
当然也有例外的情况,左值传递给右值引用参数,且右值引用指向模板类型参数(T&&)
template<typename T>
void foo(T&& u) {
std::cout << u;
}
int main() {
int i = 0;
foo(i);
return 0;
}
这时编译器推断类型是为实参的左值引用类型(int&), 那么foo的类型就是int & &&,在这时就会发生引用折叠。折叠的规则为,以X类型为例:
- X& &,X& &&和X&& &都折叠成X&
- X&& &&折叠成X&&
2 完美转发
某些函数需要将其一个甚至多个参数连同类型不变的转发给其他函数。
即通过函数转发保证参数的左值/右值,const/non-const属性不变。
封装一个通用的f函数转发给g,我们看下如果来写f函数
template<typename T>
void f(/*这里应该如何写*/ t) {
g(t);
}
大家可以做下尝试,参数处如何来写,能够满足g呢?
我们这里选择直接给出答案T&&,,但是我们调用g的时候也要做改变。如下
template<typename T>
void f(T&& t) {
g(static_cast<T&&>(t));
}
结合我们上边的引用折叠,我们来分析一下。假如类型是int
- 如果我们传递是non-const左值,T推断成int &,T&& &是左值,g接受的也还是左值
- 同理我们传递是const左值,T推断成const int&,T&& &是const左值,g接受的也是const左值
- 如果是non-const右值,T推断成int,g接受的也还是右值
- const右值同理
除此之外,大家也可以试下我们使用别的写法也可以达到同样效果。
在这里我们使用static_cast做了下转化,让其能够达到我们希望的结果。
3 forward
讲到这里大家发现和std::forward好像也没啥关系,我们看下std::forward的实现
template <class _Tp>
inline _LIBCPP_CONSTEXPR
_Tp&&
forward(typename remove_reference<_Tp>::type& __t) _NOEXCEPT
{
return static_cast<_Tp&&>(__t);
}
template <class _Tp>
inline _LIBCPP_CONSTEXPR
_Tp&&
forward(typename remove_reference<_Tp>::type&& __t) _NOEXCEPT
{
static_assert(!is_lvalue_reference<_Tp>::value,
"can not forward an rvalue as an lvalue");
return static_cast<_Tp&&>(__t);
}
看下forward的实现,是不是和我们的程序是一样的,所以我们程序可以改写成:
template<typename T>
void f(T&& t) {
g(std::forward<T>(t));
}
其实到这里,我们发现这些东西只是c++帮我们做了下封装,使得我们使用起来很清晰明朗。
3 forward与move对比
到这里我就忍不住要带大家看下std::move的实现,然后来做下对比:
template <class _Tp>
inline _LIBCPP_CONSTEXPR
typename remove_reference<_Tp>::type&&
move(_Tp&& __t) _NOEXCEPT
{
typedef _LIBCPP_NODEBUG_TYPE typename remove_reference<_Tp>::type _Up;
return static_cast<_Up&&>(__t);
}
是不是看起来很像,std::move的实现也是一个static_cast。
对比1. 模板参数是一样的
对比2. 返回值,forward是Tp&&,move是remove_reference<Tp>&&
这里可以看出来,move就是返回一个右值引用,而forward是既然可能返回右值引 > 用又可能返回值引用.
对比3. 参数类型,move就是Tp&&,forward是个重载,可以接受remove_reference<Tp>&和remove_reference<Tp>&&,其实这两个函数参数类型是一样的
对比4. 实现,因为返回值类型不一样,所以实现也是不一样的,而forward仅仅是根据返回值的类型进行的转化。
这里我们做下总结,其实比较明显,forward是move的超集,move可以做的forward也是可以做到。move仅仅是转成右值引用,而forward是根据传进来的模板参数转成相应的引用类型,同时这也是forward需要写模板参数,而move不需要的理由。
std::forward的使用
我们学完了forward,总得知道该怎么用的吧,哈哈。
- 使用forward在完美转发中
我们看下几个stl的例子:
template<class _Tp, class... _Args>
inline
typename __unique_if<_Tp>::__unique_single
make_unique(_Args&&... __args)
{
return unique_ptr<_Tp>(new _Tp(_VSTD::forward<_Args>(__args)...));
}
make_unique这个函数,这里接受的参数是_Args&&… __args,然后里边使用std::forward来转发参数,单独讨论这个函数,接受的是构造Tp对象的参数,这样就省掉了我们自己来写new _Tp等等。
当我们要调用某个函数,但是却要隐藏他的调用方式,仅仅需要接受调用这个函数的参数时,就可以用到完美转发了,也即会用到forward。
然后我们再来看下vector的emplace_back:
template <class _Tp, class _Allocator>
template <class... _Args>
inline
void
vector<_Tp, _Allocator>::emplace_back(_Args&&... __args)
{
if (this->__end_ < this->__end_cap())
{
__RAII_IncreaseAnnotator __annotator(*this);
__alloc_traits::construct(this->__alloc(),
_VSTD::__to_raw_pointer(this->__end_),
_VSTD::forward<_Args>(__args)...);
__annotator.__done();
++this->__end_;
}
else
__emplace_back_slow_path(_VSTD::forward<_Args>(__args)...);
}
简单讲一下这个函数,如果之前申请的空间不够了,需要重新申请空间,然后构造Tp对象,如果空间足够,直接构造Tp对象。
这里也用到了forward函数,而整个emplace_back是不是也类似于完美转发的函数呢。通过完美转发,我们可以使用用户传进来的参数,直接构造对象在内存中。这也是emplace_back性能略优于push_back的原因。
2. 同时我们也可以使用forward的转化功能,转为右值引用,转为左值引用等。这里也就不展开讲了
ref
- cpp primer
- C++0x漫谈》系列之:右值引用(或“move语意与完美转发”)(下)