std:forward 完美转发

来源:互联网 发布:电脑转盘抽奖软件 编辑:程序博客网 时间:2024/04/29 21:02

概述:


    // TEMPLATE CLASS identity
template<class _Ty>
    struct identity
    {    // map _Ty to type unchanged
    typedef _Ty type;

    const _Ty& operator()(const _Ty& _Left) const
        {    // apply identity operator to operand
        return (_Left);
        }
    };

    // TEMPLATE FUNCTION forward
template<class _Ty> inline
    _Ty&& forward(typename identity<_Ty>::type& _Arg)
    {    // forward _Arg, given explicitly specified type parameter
    return ((_Ty&&)_Arg);
    }


         调用方式:      _STD forward<_Ty>(_Val));

简单来说,identity 用于告诉编译器,我不要你帮我推导类型了(写成式子就是 forward(_val), 不带 <_Ty> 的 ),我自己明确指定类型 ( identity<_Ty>::type)。这样在传入参数时, 编译器就不会自己猜想:把 _arg 想成左值了(折叠规则,这个规则是写这个编译器的人规定的,我们就不要费力去问为什么要这样做了,如果你偏要问,那你可以自己定义规则,这样就变成了你自己的编译器了)。

  1. 引用折叠规则:
    X& + & => X&      (1)
    X&& + & => X&   (2)
    X& + && => X&   (3)
    X&& + && => X&& (4)

outer(X&& t)

{

  inner(t)

}

inner(X&&)

如2, 意思是把外部右值 X&& t 传给函数  inner(X&&), 到了 inner函数里, a 就变认为是左值了,即 X&.

给它变成

  inner( _STD forward<_Ty>(t))

那么这时编译器执行函数 inner前 , 会对传给 t 的类型 进行推导, 而当指明类型为  identity<_Ty>::type, 编译器放弃了自动推导,认为调用者已经明确指定了类型为 ...type, 当然 forward 返回的类型也要为 右值 X&&。你也许会问为什么要为 X&&, 而不为 X&. 这不废话吗,旧的编译器由于推导机制会把 右值认为成左值,不会把左值认为是右值,使用新的 forward ,就是为了保持 右值的性质不变。

上篇:

转发问题

在程序员不用写高度泛化的代码的时候,C++98/03 的 lvalue, rvalue, 引用,还有模板看起来是很完美的。假设你要写一个完全泛化的函数 outer(),这个函数的目的是将任意数目个任意类型的参数传递(也就是“转发”)给函数 inner()。已有很多不错的解决方案,比如 factory 函数 make_shared<T>(args) 是把 args 传给 T 的构造函数,然后返回 shared_ptr<T>。(这样就把 T 对象和用于对它进行引用计数的代码存储到同一块动态内存中,性能上与侵入式引用计数一样好); 而像 function<Ret(args)> 这样的包装类是把参数传给其内部存储的函数对象(functor),等等。在这篇文章里,我们只对 outer() 是如何把参数传递给 inner() 这部分感兴趣。至于 outer() 的返回类型是怎么决定的是另外的问题(有时候很简单,如 make_shared<T>(args) 总是返回 shared_prt<T>,),但要在完全搞定这个问题的一般化情况,你就要用到 C++0x的 decltype 特性了)。

如果不带参数,就不存在这样的问题,那么带一个参数情况呢?让我们尝试写个 outer() :

template <typename T> void outer(T& t) {

    inner(t);

}

问 题来了,如果传给它的参数是非常量 rvalue,那我们就无法调用 outer()。如果 inner() 接收 const int& 型的参数,那 inner(5) 是可以通过编译的,但是 outer(5) 就编译不过了。因为 T 会被推导为 int, 而 int& 是不能绑定到常量 5 的。

好吧,让我们试试这个:

template <typename T> void outer(const T& t) {

    inner(t);

}

如果 inner()接收 int& 型参数,那就会违法 const 正确性,编译都过不了。

现在,你可以重载两个分别带 T& 和 const T& 参数的 outer(),这确实管用。当你调用 outer()时,就像直接调用 inner() 一样。

可惜的是,这中方法在多参数的情况下就麻烦了(译注:要写的重载函数太多了)。你就得为每一个参数像 T1& 和 const T1&, T2& 和 const T2& 等这样进行重载,要重载的函数数目呈指数级增长。(VC9 SP1 的 tr1::bind() 就够让人感到绝望了,它为 5 个参数这么重载出了 63 个函数。如果不这么蛮干的话,没有像这里的长篇累述,我们就很难跟使用者解释为什么不能调用用 1729 这样的 ravlue 做参数的函数。为了产生出这些重载函数使用了令人作呕的预处理机制,恶心到你都不想知道它)。

在 C++98/03 中,转发问题是很严重的,而且本质上无解(必须求助于恶心的预处理机制,这会严重拖慢编译速度,还让代码变得难以阅读)。总算, rvalue 优雅地解决了这个问题。

完美转发: 模式

完美转发让你能简单而清晰地只写一个模板函数就可以转发所有的参数给任意函数,不管它带几个参数,也不管参数类型是什么。而且参数的非常量/常量, lvalue/rvalue 属性都能得以保留,让你可以像使用 inner() 一样使用 outer(),还可以和 move 语意一起用从而获得额外的好处。( C++0x 的变长模板技术解决了“任意数目”这部分,我们在这里把 N 看做任意数目)。乍看之下很神奇,实际上很简单:

C:\Temp>type perfect.cpp

#include <iostream>

#include <ostream>

using namespace std;

template <typename T> struct Identity {

    typedef T type;

};

template <typename T> T&& Forward(typename Identity<T>::type&& t) {

    return t;

}

void inner(int&, int&) {

    cout << "inner(int&, int&)" << endl;

}

void inner(int&, const int&) {

    cout << "inner(int&, const int&)" << endl;

}

void inner(const int&, int&) {

    cout << "inner(const int&, int&)" << endl;

}

void inner(const int&, const int&) {

    cout << "inner(const int&, const int&)" << endl;

}

template <typename T1, typename T2> void outer(T1&& t1, T2&& t2) {

    inner(Forward<T1>(t1), Forward<T2>(t2));

}

int main() {

    int a = 1;

    const int b = 2;

    cout << "Directly calling inner()." << endl;

    inner(a, a);

    inner(b, b);

    inner(3, 3);

    inner(a, b);

    inner(b, a);

    inner(a, 3);

    inner(3, a);

    inner(b, 3);

    inner(3, b);

    cout << endl << "Calling outer()." << endl;

    outer(a, a);

    outer(b, b);

    outer(3, 3);

    outer(a, b);

    outer(b, a);

    outer(a, 3);

    outer(3, a);

    outer(b, 3);

    outer(3, b);

}

C:\Temp>cl /EHsc /nologo /W4 perfect.cpp

perfect.cpp

C:\Temp>perfect

Directly calling inner().

inner(int&, int&)

inner(const int&, const int&)

inner(const int&, const int&)

inner(int&, const int&)

inner(const int&, int&)

inner(int&, const int&)

inner(const int&, int&)

inner(const int&, const int&)

inner(const int&, const int&)

Calling outer().

inner(int&, int&)

inner(const int&, const int&)

inner(const int&, const int&)

inner(int&, const int&)

inner(const int&, int&)

inner(int&, const int&)

inner(const int&, int&)

inner(const int&, const int&)

inner(const int&, const int&)

两行!完美转发只用了两行!够简洁吧!

这个例子示范了怎么把 t1 和 t2 从 outer() 透明地转发给 inner(); inner() 可以知道它们的非常量/常量, lvalue/ravlue 属性,就像inner是被直接调用的那样。

跟 std::move() 一样, std::identify 和 std::forward() 都是在 C++<utility> 中定义的( VC10 会有, VC10 CTP中没有)。我将演示怎么来实现它们。(再次,我将交替使用 std::identity 和我的 Identity, std::forward() 和我的 Forward(),因为他们的实现是等价的。)

现在,让我们来揭开“魔术“的神秘面纱,其实它靠的就是模板参数推导和引用折叠(reference collapsing)技术。

rvalue 引用:模板参数推导和引用折叠(reference collapsing)

rvalue 引用与模板以一种特别的方式相互作用。下面是一个示例:

C:\Temp>type collapse.cpp

#include <iostream>

#include <ostream>

#include <string>

using namespace std;

template <typename T> struct Name;

template <> struct Name<string> {

    static const char * get() {

        return "string";

    }

};

template <> struct Name<const string> {

    static const char * get() {

        return "const string";

    }

};

template <> struct Name<string&> {

    static const char * get() {

        return "string&";

    }

};

template <> struct Name<const string&> {

    static const char * get() {

        return "const string&";

    }

};

template <> struct Name<string&&> {

    static const char * get() {

        return "string&&";

    }

};

template <> struct Name<const string&&> {

    static const char * get() {

        return "const string&&";

    }

};

template <typename T> void quark(T&& t) {

    cout << "t: " << t << endl;

    cout << "T: " << Name<T>::get() << endl;

    cout << "T&&: " << Name<T&&>::get() << endl;

    cout << endl;

}

string strange() {

    return "strange()";

}

const string charm() {

    return "charm()";

}

int main() {

    string up("up");

    const string down("down");

    quark(up);

    quark(down);

    quark(strange());

    quark(charm());

}

C:\Temp>cl /EHsc /nologo /W4 collapse.cpp

collapse.cpp

C:\Temp>collapse

t: up

T: string&

T&&: string&

t: down

T: const string&

T&&: const string&

t: strange()

T: string

T&&: string&&

t: charm()

T: const string

T&&: const string&&

这里藉由 Name 的显式规格说明来打印出类型。

当我们调用 quark(up) 时,会进行模板参数推导。 quark() 是一个带有模板参数 T 的模板函数,但是我们还没有为它提供显式的类型参数(比如像 quark<X>(up)这样的)。通过比较函数形参类型 Type&& 和函数实参类型(一个 string 类型的 lvalue)我们就能推导出模板实参类型。(译注:原文用 argument 表示实参,parameter 表示形参)

C++0x 会转换函数实参的类型和形参的类型,然后再进行匹配。

首先,转换函数实参的类型。这遵循一条特殊规则(提案N2798

然后,转换函数形参的类型。不管是 C++98/03 还是 C++0x 都会解除引用( lvalue 引用和 rvalue 引用在 C++0x 中都会被解除掉)。在前面例子的四种情形中,这样我们会把 T&& 转换成 T 。

于是, T 会被推导成函数实参转换之后的类型。up 和 down 都是 lvalue,它们遵循那条特殊规则,这就是为什么 quark(up)  打印出"T:string&" ,而 quark(down) 打印出 "T: cosnt string&"的原因。strange() 和 charm() 都是右值,它们遵循一般规则,这就是为什么 quark(strange()) 打印出 "T: string" 而 quark(charm()) 打印出"T: const string" 的原因。

替换操作会在类型推导之后进行。模板形参 T 出现的每一个地方都会被替换成推导出来的模板实参类型。在 quark(string()) 中 T 是 string ,因此 T&& 会是 string&& 。同样,在 quark(charm()) 中,T 是 const string , 因此 T&& 是 const string&& 。但 quark(up) 和 quark(down) 不同,它们遵循另外的特殊规则。

在 quark(up) 中, T 是 string& 。进行替换的话 T&& 就成了 string& && ,在 C++0x 中会折叠(collapse)引用的引用,引用折叠的规则就是“lvalue 引用是传染性的”。 X& &, X& && 和 X&& & 都会被折叠成 X& ,只有 X&& && 会被折叠成 X&& 。因此 string& && 被折叠成 string& 。在模板世界里,那些看起来像 rvalue 引用的东西并不一定真的就是。 因而 quark(up) 被实例化为 quark<string&>() ,进而 T&& 经替换与折叠之后变成 string& 。我们可以调用 Name<T&&>::get() 来验证这个。 同样, quark(down) 被实例化为 quark<const string&>() ,进而 T&& 经替换与折叠之后变成 const string& 。在 C++98/03中,你可能习惯了常量性(constness)隐藏于模板形参中(也就是说可以传 const Foo 对象作实参来调用形参为 T& 的模板函数,就像 T& 会是 const Foo& 一样),在 C++0x 中,左值属性(lvalueness) 也能隐藏于模板形参中。

那好,这两条特殊规则对我们有什么影响?在 quark() 内部,类型 T&& 有着和传给 quark() 的实参一样的左/右值属性(lvalueness/rvalueness)和常量性。这样 rvalue 引用就能保持住左右值属性和常量性,做到完美转发。

完美转发: std::forward() 和 std::identidy 是怎样工作的

让我们再来看看 outer() :

template <typename T1, typename T2> void outer(T1&& t1, T2&& t2) {

    inner(Forward<T1>(t1), Forward<T2>(t2));

}

现在我们明白了为什么 outer() 的形参是 T1&& 和 T2&& 类型的了,因为它们能够保持住传给 outer() 的实参的信息。那为什么这里要调用 Forward<T1>() 和 Forward<T2>() 呢?还记得么,具名 lvalue 引用和具名 rvalue 引用都是 lvalue 。如果 outer() 调用 inner(t1, t2) ,那么 inner() 总是会当 lvalue 来引用 t1 和 t2 ,这就破坏了完美转发。

幸 运的是,不具名 lvalue 引用是 lvalue,不具名 rvalue 引用还是 rvalue 。因此,为了将 t1 和 t2 转发给 inner(),我们需要将它们传到一个帮助函数中去,这个帮助函数移除它们的名字,保持住它们的属性信息。这就是 std::forward() 做的事情:

template <typename T> struct Identity {

    typedef T type;

};

template <typename T> T&& Forward(typename Identity<T>::type&& t) {

    return t;

}

当我们调用 Forward<T1>(t1) , Identidy 并没有修改 T1 (很快我们讲到 Identidy 对 T1 做了什么)。因此 Forward<T1>() 接收 T1&& ,返回 T1&& 。这样就移除了 t1 的名字,保持住 t1 的类型信息(而不论 t1 是什么类型, string& 也好, const string& 也好, string&& 也好或 const string&& 也好)。这样 inner() 看到的 Forward<T1>(t1) ,与 outer() 接收的第一个实参有着相同的信息,包括类型,lvalueness/rvalueness,常量性等等。完美转发就是这样工作的。

你可能会好奇如果不小心写成 Forward<T1&&>(t1) 又会怎样呢?(这个错误还是蛮诱人的,因为 outer() 接收的就是 T1&& t1 )。很幸运,没什么坏事情会发生。 Forward<T1&&>() 接收与返回的都是 T1&& && ,这会被折叠成 T1&& 。于是,Forward<T1>(t1) 和 Forward<T1&&>(t1) 是等价的,我们更偏好前者,是因为它要短些。

Identidy 是做什么用的呢?为什么下面的代码不能工作?

template <typename T> T&& Forward(T&& t) { // BROKEN

    return t;

}

如果 Forward() 像是上面那样,它就能被隐式调用(不带明确的模板参数)。当我们传给 Forward() 一个 lvalue 实参时,模板参数推导就介入了,如我们前面看到的那样会将 T&& 变成 T&,也就是变成一个 lvalue 引用。问题来了,即使形参 T1&& 和 T2&& 指明是 rvalue 引用,但在 outer() 中,具名的 t1 和 t2 却是 lvaue ,这个问题是我们一直想要解决的!使用上面那个错误的实现, Forward<T1>(t1) 是可以工作的,而 Foarward(t1) 虽然能通过编译(很诱人哦)但会出错,就如它就是 t1 一样。真是痛苦的源泉啊,因此,Identity 被用来阻止模板参数推导。typename Identity<T>::type 中的那对冒号就像绝缘体,模板参数推导无法穿越它,有模板编程经验的程序员应该对此很熟悉了,因为这在 C++98/03 和 C++0x 中是一样的。(要解释这个是另外的事情了)

//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////.//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

下篇:

文 / 李博(光宇广贞)

       《C++ 0x 之左值与右值》文中提到 std::forward() 和 std::move()。本文开头对之补充一句:

    在操作函数返回值或函数参数时,匿名左值仍然为左值,左值可以具名;匿名右值仍然为右值,右值一旦具名成功,立即转变为左值。

       举一个例子。使用 std::move() 方法向 Outer 传递右值后,使用 std::forward() 保证调用右值重载,而直接访问参数 t 的调用绑定到了左值重载。见下图:

c1

       因此,操作右值引用不能直接操作其变量名,否则将使右值引用具名,从而转为左值引用。操作右值引用必须使用 std::move()、std::forword() 等方法。将两个方法的实体展开如下:

5

       这两个 identity 和 Remove_Reference 是干嘛用的?若不要它们,直接用 T&& 做为 forward 里 arg 的类型或 move 里返回类型呢?

       不行。首先说 forward 方法。重新看如下代码:

       template < typename T > void Outer ( T&& t ) 
       { 
              Inner ( std::forward<T> ( t ) ); 
       }

       在《C++ 0x 之左值与右值》文中提到,我们使用 forward 的目的是保证参数的左右值性和只读性的准确传导。若不使用 identity 而只使用 T&& 的话,调用 std::forward<T>(t) 仍然没有问题;而当调用 std::forward(t),即不指明模板参数类型时,T&& 将由 t 推导,问题便来了。注意到 t 在传参数时,是点名调用,使 t 具名,故而无论原来 t 是左值还是右值,此刻都将视为左值,从而 T 被推导为左值引用,且 T&& 归化为左值引用,于是 forward 方法以左值引用类型接收参数 t,并以左值作为其返回类型。如是便违背了 forward 的本意。

       而使用 identity 后,type 被指定为 T。注意这里,“::”算符就像一面墙,挡住了类型推导看见其左侧。因此,编译器不会认为 arg 参数是需要类型推导的,是被 identity::type 指定类型的(当然也强制使用 forward 时要注明模板参数,否则将无从推导)。这就保证了 forward 总是以右值引用类型接收参数 t。前文已经提到右值引用类型参数可以保留实参的一切信息。如是保证了 forward 的本意的实现。

       最后,move 方法里面的 Remove_Reference 就好解释了。模板将根据 arg 推导类型,若 arg 是 const Type& 型,则 T 推导为 const Type& 型,而后代入特化模板,使 type 为 const Type,从而返回类型为 const Type&& 型。若 arg 是右值类型,则 type 也将归化为右值引用型。总之,Remove_Reference 模板类保证返回类型为右值引用。


       所属分类:C++

       参考:

       C++ 0x 之 Lambda:贤妻与娇娃,你娶谁当老婆?听 FP 如何点化 C++

       从 C++ 模板元编程生产质数看 F# 函数式编程思想

       人类最伟大的王安石时代,自古至今乃至不可预见的未来

       天生是兵家——一代神将“蒋方震”

       汉族姓氏与基因——为何有“两个汉族”?同姓是否同源?随父随母、生男生女是否一样?

       缅甸攻打果敢汉人与中印边界对峙的关系——东段麦克马洪线!

       限次连续若干同色球概率算法

       Vista 高级扫雷 105 秒到 135 秒各记录截图

       做为中国人,以下“历史片断”绝对颠覆你的想像!

       王夫人向贾母挑战

0 0