之前介绍过std::move
,今天我们就接着来说说std::forward
。C++11引入了一个新特性:右值引用,这个特性可以避免不必要的拷贝从而提高性能。
std::forward
我们先看看std::forward
是干什么的,然后说说为什么需要它。
根据前文,资源高效转移的问题不是已经有std::move
来解决了么,为什么还需要另外一个std::forward
来参和?注意“不必要”这个词,既然有不必要,那么就说明有时候是有必要的。
虽然std::move
和std::forward
都和右值引用有关,但是侧重点不同。std::move
用在需要只右值引用的地方;而std::forward
用在一个需要统一引用(universal references)的地方,这个通用引用是什么?我更喜欢叫它薛定谔的引用,因为它到底是左值引用还是右值引用是不确定的,如果你给他传递左值它就是左值引用,如果给它传个右值它就是右值引用。形如
template<typename T>
T f(T&& param) {}
这种T&&
就是通用引用。
假设我们有下面这么一个类,Foo
,在使用的过程中会出现以下两种初始化方式:
class Foo {
public:
std::string member_;
Foo(const std::string& member): member{member} {}
}
// Two use cases
// Case#1
std::string bar = "bar";
Foo foo(bar);
// Case#2
std::string bar = "bar";
Foo foo("foo" + bar);
这两种方式有什么不同呢?第一种使用场景中,我们已经有了一个字符串"bar"
和引用bar
绑定在了一起,我们希望用它来初始化Foo
,但是这个bar
我们后续还需要使用,所以我们希望它拷贝一份给Foo
;第二种情况中,"foo" + bar
这个表达式生成了一个临时字符串"foobar"
,由于它是临时的,外部是没有任何引用和他绑定的,很快就会被销毁,因此我们希望能将它的内存资源直接转移给Foo
而不是拷贝一份。鉴于存在上述两种使用场景,常规情况下我们需要分别定义两个构造函数:
class Foo
{
public:
std::string member;
// Copy member.
Foo(const std::string& member): member{member} {}
// Move member.
Foo(std::string&& member): member{std::move(member)} {}
};
但是我们懒,不想写那么多构造函数,有没有办法实现?有。我们使用std::forward
:
class Foo
{
public:
std::string member;
template<typename T>
Foo(T&& member): member{std::forward<T>(member)} {}
};
如果上面不够清晰的话,我们来看看下面这个例子:
#include <iostream>
#include <string>
#include <utility>
void foo(std::string& param) {
std::cout << "std::string& version" << std::endl;
}
void foo(std::string&& param) {
std::cout << "std::string&& version" << std::endl;
}
template<typename T>
void wrapper(T&& param) {
// foo(param);
foo(std::forward<T>(param));
}
int main() {
std::string foo("foo");
wrapper(foo);
wrapper(foo + "bar");
}
再上面的例子中,如果在wrapper
中没有使用std::forward
,也就如果使用注释掉的那个方法调用foo
函数,得到的结果将是这样子:
std::string& version
std::string& version
而如果使用目前的方式调用foo
,结果将是:
std::string& version
std::string&& version
std::forward
到底做了什么?
它主要作用如下:根据模板参数T
,将模板函数的形参param
变成在右值传递给函数foo
或者将param
保留为左值传递给函数foo
。什么意思呢?就是如果传递个形参param
的值是左值,例如上面例子中的foo
,那么std::forward
返回的是一个左值;果传递个形参param
的值是右值,例如上面例子中的表达式foo + "bar"
得到的是一个右值,那么std::forward
返回的是一个右值。因为根据C++语义,在函数wrapper
的内部,param
是一个左值引用。
总的一句话就是std::forward
能够保留传给形参param
的实参的全部信息。wrapper(foo);
中参数foo
是左值,那么wrapper
传给函数foo
的就是左值;wrapper(foo + "bar");
中参数foo + "bar"
是右值,那么wrapper
传给函数foo
的就是右值。
但是,std::forward
是怎么知道一个形参的原本类型的呢?这里又引出两个知识点:模板参数类型推导( template argument deduction)和引用则叠(Reference collapsing)
引用则叠和模板参数类型推导
关于引用则叠和模板参数推断,可以说上一天,所以这里步打算展开,仅仅简单介绍下什么是引用则叠。假设有下面这种情况:
// T denotes the int& type
typedef int& T;
// TR is an lvalue reference to T
typedef T& TR;
// The declared type of var is TR
TR var;
变量var
的类型是TR
,而TR
是类型T
的移用,T
右是int
类型的一个引用,这样一串下来,var
的真实类型是什么呢?
引用的引用,不管是左值引用还是右值引用,在C++11之前是非法的,但是上面例子中的这种情况又是很可能出现的。为了解决这一问题,C++11定义了一套规则去处理引用的引用是什么的问题,这就是移用则叠。
引用则叠主要右以下四条规则:
T | TR | Type of var |
---|---|---|
A& | T& | A& |
A& | T&& | A& |
A&& | T& | A& |
A&& | T&& | A&& |
套用到上面的小例子,var
的类型就是一个int&
。那这个到底和std::forward
有什么关系呢?这里留下一个坑,关于模板参数类型,以后有机会再说啦。
总结
std::forward
与std::move
一样,都与C++11引入的新特性右值引用相关。但是,与std::move
不同的是,std::forward
可以将参数保留它的类型信息,原样转发给下一个被调用的函数。实现这一动作的原理是模板参数推导和引用则叠。
References
[1] ppreference.com
[2] Perfect Forwarding in C++11
[3] Reference collapsing (C++11)
[4] Advantages of using forward