前置
首先我们要理解左值和右值的区别
以及如下的引用折叠原则
TR | R | des | |
---|---|---|---|
T& | & | T& | 左值引用+左值引用->左值引用 |
T& | && | T& | 左值引用+右值引用->左值引用 |
T&& | & | T& | 右值引用+左值引用->左值引用 |
T&& | && | T&& | 右值引用+右值引用->右值引用 |
场景
一个set方法,这个方法的作用是将传入的vector对象引用v
,保存到我们的内部成员变量_v
,这里实际上我们只是在做一个引用的传递
void set(const std::vector<T> & v) { _v = v; }
如果传入的对象是一个左值,这样做没什么问题,但是当我们需要赋值的对象是一个右值呢?这里假设赋值的vector是一个方法makeAndFillVector()
的结果,即我们做以下赋值操作:
_v = makeAndFillVector();
针对右值,编译器将会移动这个vector而不是拷贝它,但是当你使用一个set()
来进行这个操作,由于set
接收的是一个左值引用,那么依据引用折叠原则,右值参数的属性将会丢失,这时候就会发生拷贝操作。
set(makeAndFillVector()); // makeAndFillVector的结果将会被拷贝到一个临时变量中
为了避免这个拷贝操作,你需要转发操作,这将会在任何时候都产生最优的代码结果。即当你提供一个左值,你希望你的方法将其作为一个左值执行拷贝,当你提供一个右值,你希望你的方法将其作为一个右值执行转移。
如果你希望实现上述操作,常规做法是重载两个方法分别来处理左值和右值:
set(const std::vector<T> & lv) { _v = v; }
set(std::vector<T> && rv) { _v = std::move(rv); }
这里我们可以通过万能引用来得到一个统一的接口,得到如下的模板方法签名:
template<class T>
void perfectSet(T && t);
下面的方法中,v
会当作左值引用传递
std::vector<T> v;
perfectSet(v);
但是当我们传入一个右值,makeAndFillVector
产生的匿名Vector会当作右值引用传递
perfectSet(makeAndFillVector());
完美转发
在perfect
的实现中,如果我们想加载到正确的set
,就需要使用到完美转发
template<class T>
void perfectSet(T && t) {
set(std::forward<T>(t));
}
那么,如果不使用std::forward
将会发生什么?
我们比较以下两个方法进行参数转发:
void perfectSet(T && t) {
set(t);
set(t); // t 没有变化
}
void perfectSet(T && t) {
set(std::forward<T>(t));
set(t); // t 此时已经是空的了
}
如果你没有明确如何转发t
,编译器必须保守地假设你可能再次访问 t
并选择 set 的左值引用版本。我们这里可以这么认为,虽然我们使用万能引用使prefectSet
既可以传入左值也可以传入右值,但是在函数内部,编译器都是假设我们将会再次访问这个形参,这就造成了在函数内部对形参的转发使用都是以左值的形式进行的。
那么我们如何去获得实参信息呢?
在通过万能引用将参数传递进来时,当传入参数是右值,类型T
会被推导成实参类型(非引用),如果传入的值是左值,类型T
会被推导成左值引用,也就是说模板T
中保存着实参信息。
因此,我们需要通过“完美转发”t
,编译器将保留它的右值属性,并且将调用 set() 的右值引用版本。 这个版本移动了 t 的内容,这意味着原来的变成了空的。
完美转发原理
这里我们来看一下std::forward
的是如何实现“完美转发”的
template<class T>
T&& forward(typename std::remove_reference<T>::type& t) noexcept {
return static_cast<T&&>(t);
}
我们可以看到,std::forward
接收一个T &
类型,返回T &&
类型。这里通过std::remove_reference
解引用获取实参类型t
,返回时依据引用折叠原则,最终返回的实际上就是T
的类型,保持了实参原来的属性。