就TM你叫std::forward啊?

之前介绍过std::move,今天我们就接着来说说std::forward。C++11引入了一个新特性:右值引用,这个特性可以避免不必要的拷贝从而提高性能。

std::forward

我们先看看std::forward是干什么的,然后说说为什么需要它。
根据前文,资源高效转移的问题不是已经有std::move来解决了么,为什么还需要另外一个std::forward来参和?注意“不必要”这个词,既然有不必要,那么就说明有时候是有必要的。
虽然std::movestd::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::forwardstd::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


本文首发于个人微信公众号TensorBoy。如果你觉得内容还不错,欢迎分享并关注我的微信公众号TensorBoy,扫描下方二维码获取更多精彩原创内容!
公众号二维码

发布了45 篇原创文章 · 获赞 4 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/ZM_Yang/article/details/104659101
tm
今日推荐