C++11新特性--完美转发

转发的问题

    在模板编程中,常有一种场景是把模板参数转发给另一个函数调用,这时候如果只提供值传递版本会显得效率太低。看一下代码

template<class TYPE, class ARG>
TYPE* get_instance(ARG arg)
{
     TYPE* ret;
     ret = new TYPE(arg);
     return ret;
}

    代码很简单,就是用ARG参数去初始化一个TYPE类型的对象,然后返回该对象指针。考虑一下如果ARG类型是一个自定义类型,那么这样的值传递会是比较大的性能开销。有没有办法改进一下?看一下代码

template<class TYPE, class ARG>
TYPE* get_instance(const ARG& arg)
{
     TYPE* ret;
     ret = new TYPE(arg);
     return ret;
}

    这段代码将传入改为了万能的常量左值引用,可以接受任何类型,可以解决性能开销的问题,但是不够灵活,如果我们想TYPE接受一个右值去初始化呢?那么有没有可以把参数连同类型一起转发的方案呢?C++11提供了这样能力既完美转发。代码如下

template<class TYPE, class ARG>
TYPE* get_instance(ARG&& arg)
{
     TYPE* ret;
     ret = new TYPE(std::forward<ARG>(arg));
     return ret;
}

形参改为了右值引用么?

两个模板函数推导原则

    解释一下前面留下的问题。在模板函数中(注意是模板函数不是模板类,也不是普通函数,只是模板函数),形如上例中的形参即T&&,如果T是一个推导类型(即模板参数)它的意思并不是右值引用,它有一个专用的名字forwarding reference,它遵循以下的特殊推导规则来推导T的类型:
1、引用折叠原则(reference collapsing rule),下面以int为类型举例,以区别于模板参数T
a)int& & (引用的引用) 被转化成 int&
b)int&& & (rvalue的引用)被传化成 int&
c)int& && (引用作rvalue) 被转化成 int&
d)int&& && 被转化成 int&&
2、推导原则
以上面模板函数为例,三个特殊之处(左值引用,常量左值引用如预期被推导出来)
a)如果传入get_instance的参数是int类型(即左值),那么ARG会被推导为int&,即ARG=int&
b)如果传入get_instance的参数是右值int类型(即右值),那么ARG被推导为int,即ARG=int
c)如果传入get_instance的参数是const int类型(即常量左值),那么ARG被推导为const int&,即ARG=const int&

注意:为什么这里没有参数类型是右值引用类型的参数??
其实右值引用是一个左值,即你传入函数的实参类型有可能是一个左值(左值引用,左值,常量左值引用,右值引用),也有可能是一个右值。注意到细微的区别了么?这里本章最后会给出一些代码示例。

以上三个特殊推导之处

结合std::forward代码来看一下

template <class _Tp>
inline
_Tp&&
forward(typename remove_reference<_Tp>::type& __t)
{
    return static_cast<_Tp&&>(__t);
}

template<class TYPE, class ARG>
TYPE* get_instance(ARG&& arg)
{
     TYPE* ret;
     ret = new TYPE(std::forward<ARG>(arg));
     return ret;
}

现在结合上面两个原则来推导一下:
a)调用get_instance传入int类型,ARG推导为int&,传递给std::forward的模板参数就是int&类型,std::forward中的typename remove_reference<_Tp>::type为int类型,static_cast中_Tp&&类型根据引用折叠,推导为int& &&即int&,最后forward转发为int&类型,完美转发。
b)调用get_instance传入int右值类型,ARG推导为int,传递给std::forward的模板参数就是int类型,std::forward中的typename remove_reference<_Tp>::type为int类型,static_cast中_Tp&&类型根据引用折叠,推导为int&&即int&&,最后forward转发为int&&类型,完美转发。
c)调用get_instance传入const int类型,ARG推导为const int&,传递给std::forward的模板参数就是const int&类型,std::forward中的typename remove_reference<_Tp>::type为const int类型,static_cast中_Tp&&类型根据引用折叠,推导为const int& &&即const int&,最后forward转发为const int&类型,完美转发。
d)调用get_instance传入int&类型,ARG推导为int&,传递给std::forward的模板参数就是int&类型,std::forward中的typename remove_reference<_Tp>::type为int类型,static_cast中_Tp&&类型根据引用折叠,推导为int& &&即int&,最后forward转发为int&类型,完美转发。
e)调用get_instance传入const int&类型,ARG推导为const int&,传递给std::forward的模板参数就是const int&类型,std::forward中的typename remove_reference<_Tp>::type为int类型,static_cast中_Tp&&类型根据引用折叠,推导为const int& &&即const int&,最后forward转发为const int&类型,完美转发。

完美转发

    上面分析的情况即为完美转发,总结一下是模板函数形参形如T&&,结合库函数std::forward来将参数实现类型的完美转发。

代码示例

    用代码来补充说明一下上面留下的问题。

template <typename T>
void
wrapper(T&& value)
{
    std::cout<<"T is a ref type(lvalue or rvalue ref)?:"<<std::is_reference<T>::value<<std::endl;
    std::cout<<"T is right value ref?:"<<std::is_rvalue_reference<T>::value<<std::endl; //none T is deduce to rvalue ref
}
static void execute()
{
    int left = 1;
    int &&right = 2;

    wrapper(right); //input is a lvalue
    wrapper(3); //intput is a rvalue
    wrapper(std::move(left)); //input is a rvalue
    wrapper(left); //input is a lvalue
}

输出是
T is a ref type(lvalue or rvalue ref)?:1
T is right value ref?:0
T is a ref type(lvalue or rvalue ref)?:0
T is right value ref?:0
T is a ref type(lvalue or rvalue ref)?:0
T is right value ref?:0
T is a ref type(lvalue or rvalue ref)?:1
T is right value ref?:0

结果跟我们上述的分析是一致的,T不会推导为右值引用,原因是输入参数只有左值和右值两种,不存在右值引用这种输入。
简单分析一下前三个wrapper调用的区别,第一句调用right是个右值引用类型的变量,自然可以被取地址,所以它是一个左值。
第二句调用毋庸置疑传入的是一个右值。
第三句调用跟第二句的效果一模一样,由于std::move返回了一个右值引用类型的返回值,函数返回值通常是个右值,所以这里它的返回值自然是一个右值。

最后几点值得注意的

    再对比一下完美转发和接受一个右值引用的函数对比。

template <typename T>
void
wrapper(T&& value);

void 
wrapper_none_template(int&& value);

    这两个函数的区别是,上面的模板函数即提现c++11所谓的完美转发,T会根据传入实参参数的不同(左值、右值、左值引用、常量左值引用)而推导出不同的类型。
下面的函数是一个只接受右值实参的函数,如果传入的值不是右值,那么编译会报错。
    还有一点,用模板函数forwarding reference去进行转发,所有的传入参数最终都是引用类型(左值的和右值的)即不产生值传递所造成的额外构造和析构。之所以提到这一点,主要是针对传入一个右值的情况。前面提到如果传入一个右值int,T会被推导为int,但是形参还是int&&,也就是说形参是一个右值引用,它绑定了一个右值,所以还是一个引用。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容