一. 什么是Lambda RC?gozBFJ
所谓Lambda,简单的说就是快速的小函数生成。 AQ+MjS,
在C++中,STL的很多算法都要求使用者提供一个函数对象。例如for_each函数,会要求用户提供一个表明“行为”的函数对象。以vector<bool>为例,如果想使用for_each对其中的各元素全部赋值为true,一般需要这么一个函数对象, i7D[5!
wr>[Eo@%\
AH-B/c5
S\5%nz\
class filler ~;$,h ET
{ *Cf5D6=Q
public : {02$pO
void operator ()( bool & i) const {i = true ;} c[VVCN8dA
} ; ;\a?xtIy
R `K1L!`3
cH>@ZFTF
这样实现不但麻烦,而且不直观。而如果使用lambda,则允许用户使用一种直观和见解的方式来处理这个问题。以boost.lambda为例,刚才的问题可以这么解决: [>--U)/
e7tp4M9!%
^IW5c>;|
hNU$a?eVpR
for_each(v.begin(), v.end(), _1 = true ); `st3iTLZY
%[S-"k
t?1b(oJ
那么下面,就让我们来实现一个lambda库。 u-</G-y
wH]5VltUT1
Z?JR6;@W
"xWrYq'"
二. 战前分析 !U::kr=t
首先要说明的是,我并没有读过boost.lambda或其他任何lambda库的代码,因此如代码有雷同,纯属巧合。 y[`>,?ns5
开始实现以前,首先要分析出大致的实现手法。先让我们来看几段使用Lambda的代码 N$ oQK(
BN7]u5\7
<8)cr0~zy>
for_each(v.begin(), v.end(), _1 = 1 ); UA4="/
/* --------------------------------------------- */ Z-%zR'-?*
vector < int *> vp( 10 ); 65 ]>6D43
transform(v.begin(), v.end(), vp.begin(), & _1); *? V boyU
/* --------------------------------------------- */ rF ?gKk
sort(vp.begin(), vp.end(), * _1 > * _2); O,.c gX
/* --------------------------------------------- */ Yw(O}U 5e
int b = * find_if(v.begin, v.end(), _1 >= 3 && _1 < 5 ); _p*a`,tK
/* --------------------------------------------- */ kF]sy8u]
for_each(vp.begin(), vp.end(), cout << * _1 << ' \n ' ); G]v BI=
/* --------------------------------------------- */ UpTVLx^c
for_each(vp.begin(), vp.end(), cout << constant( ' \n ' ) << * _1); wE~&Y?^
CH9Psr78
x3AAn,m8
CKE):kHu
看了之后,我们可以思考一些问题: MD9 8N{+[|
1._1, _2是什么? E4N/or
显然_1和_2都满足C++对于标识符的要求,可见_1和_2都是对象。 DbWaF5\yD
2._1 = 1是在做什么? 1VKu3
既然_1是一个对象,那么_1的类必然重载了operator=(int)。那么operator=返回什么呢?该函数所返回的对象被传入for_each的第3个参数,可见其返回了一个函数对象。现在整个流程就很清楚了。_1 = 1调用了operator=,其返回了一个函数对象,该函数对象能够将参数1赋值为1。 "%(SLQOyy
Ok,回答了这两个问题之后,我们的思路就很清晰了。如果要实现operator=,那么至少要实现2个类,一个用于产生_1的对象,另一个用于代表operator=返回的函数对象。 9QP- ~V{$
:_8Nf1B+T
~`97?6*Ra
三. 动工 -kk0zg
&|i
首先实现一个能够范型的进行赋值的函数对象类: Talmc|h
"LNLM
=O%Hf bx
G!)Q"+
template < typename T > ;~,)6UX7
class assignment N?EeT}m _
{ rSa=NpFxLu
T value; FW"n+7T
public : Nn#;Kjul.
assignment( const T & v) : value(v) {} <EKTFHJ!
template < typename T2 > U3**x5F_
T2 & operator ()(T2 & rhs) const { return rhs = value; } v?Zo5uVoq
} ; m)l'i!Y
:y.~IQN
Y'y
yrn}
其中operator()被声明为模版函数以支持不同类型之间的赋值。 8|L;y[v
然后我们就可以书写_1的类来返回assignment 7!F -.kG
KwHlpW*
XvSng"f.
icK$W2<8mg
class holder =4[
U<opP
{ Hk
f<.U
public : 3ytlD '
template < typename T > Na>w~
assignment < T > operator = ( const T & t) const !aB~G}'
{ B ({g|}|G+
return assignment < T > (t); HDO_r(i
} <KX fh
} ; }U'VVPh_
OF} ."a
}
fa
由于该类是一个空类,因此我们可以在其后放心大胆的写上: p%R+ c
+'/C(5y)0X
static holder _1; ~ <36vsk
Ok,现在一个最简单的lambda就完工了。你可以写 I@oSRB
WF_v>g:g
for_each(v.begin(), v.end(), _1 = 1 ); gNJdP!(t
而不用手动写一个函数对象。 !bIE%cq
B[IWgvB(e
!]3kFWs
a9u2Wlz
四. 问题分析
RnSll-
虽然基本上一个Lambda已经初步实现出来了,但是仔细想想,问题也是很多的。 bkuJN%
1, 我们现在是把_1和functor看成两个不同的存在,会导致代码的重复。 ^[&,MQU{7
2, 目前这个Lambda还无法实现如_1 = 2 = 3这样的链式操作。 Wl7S<>hg4
3, 我们没有设计好如何处理多个参数的functor。 Q?V+
0J
下面我们可以对这几个问题进行分析。 */HW]x|?V~
|~o0-: 'C
五. 问题1:一致性 I!#WXK
首先来看看1,合并_1和functor的最佳方法就是把_1本身也变成functor。那么_1的operator()会做什么事情呢?| 8VtRRtl
很明显,_1的operator()仅仅应该返回传进来的参数本身。 |>RNIJ]
sd%m{P2
struct holder Y P,>vzW
{ }_BNi;H
// nAC>']K4$
template < typename T > mp)+wZAN&
T & operator ()( const T & r) const 388vdF
{ AJ3%Z$JJ;s
return (T & )r; 6zi 5#23
} (tyky&$!
} ; GExr] 2r
kl1/(
这样的话assignment也必须相应改动: ;|`<B7xf
}eF
r,bJ
template < typename Left, typename Right > u#y#(1
=
class assignment ,D'm#Fti
{ .D;6
r4S
Left l; Ob{Tn@
Right r; GYg.B<Q.
public : ({zWyl
assignment( const Left & l, const Right & r) : l(l), r(r) {} u* G+=aV.6
template < typename T2 > g^}C/~b[
T2 & operator ()(T2 & rhs) const { return l(rhs) = r; } W] WH4.y
} ; gA`QV''/:
JZK93R
同时,holder的operator=也需要改动: 7GTDe'T
CpB,L
template < typename T > CH#K0hi
assignment < holder, T > operator = ( const T & t) const n.i8?:
{ .SLpgYFL{
return assignment < holder, T > ( * this , t); (xE |T f
} /M JI^\CA
/~Bs5f.]?
好,这样holder也成为了一个functor,这为我们以后添加功能节省了很多代码。 MsZx 0]
你可能也注意到,常数和functor地位也不平等。 $o0.oY#
N/'8W9#6
return l(rhs) = r;
peHjKK
在这一句中,r没有调用operator()而l调用了。这样以后就要不时的区分常数和functor,是不良的设计。 i&8|@CACb
那么我们仿造holder的做法实现一个常数类: FQ>kTm`d
~<-mxOe
template < typename Tp > =~"X/>'
class constant_t B&7NF}CF2
{ eY-h<K)y
const Tp t; R={#V8D~
public : 6$0<&')Yb
constant_t( const Tp & t) : t(t) {} OwEu S#-
template < typename T > tJ7F.}\;C
const Tp & operator ()( const T & r) const #.!#"8{0_
{ UCXRF
return t; xHqF_10S#
} fs:yx'mxV
} ; ?pcbso
hs5>Gx
该functor的operator()无视参数,直接返回内部所存储的常数。 j0j!oj)7I
下面就可以修改holder的operator=了 [?hvx}
[Y~~C J
template < typename T > MN8>I=p
assignment < holder, constant_t < T > > operator = ( const T & t) const &CcW(-
{ ]Y-Y.&b7t
return assignment < holder, constant_t < T > > ( * this , constant_t < T > (t)); |N^"?bSt
} Qwt0~9n(
ZJenwo
同时也要修改assignment的operator()
x.4z)2MO
OrYN-A4{
template < typename T2 > //;(KmU9
T2 & operator ()(T2 & rhs) const { return l(rhs) = r(rhs); } Hq+QsplG
现在代码看起来就很一致了。 d3|/&gDBK
(w{T[~6
六. 问题2:链式操作 j!y9E~Zz
现在让我们来看看如何处理链式操作。 :p,|6~b$
其实问题1已经为我们处理掉了大量的问题。如果_1,functor,常量彼此之间不统一为functor,那么链式操作的时候就要时刻小心一个对象是_1还是functor还是常量,会大大增加编码的难度。 ya{`gjIlW
事实上,首先要解决的是,如何知道一个functor的operator()的返回值的类型。遗憾的是,我并没有找到非常自动的办法,因此我们得让functor自己来告诉我们返回值的类型。 ] jY^*o[
比较麻烦的是,operator()的返回值一般和其参数的类型相关,而operator()通常是一个模版函数,因此其返回值类型并不能用一个简单的typedef来指定,而必须实现一个trait。 -8Hc M\b
现在我们在assignment内部声明一个nested-struct z9g ++]rkJ
U[|5:qWs
template < typename T > 3tCTPZy
struct result_1 tjwnFqI
{ D(;+my2
typedef typename ref < typename Left::result_1 < T > ::result > ::reference result; C
#iZAR
} ; 2Wu`Dp;&l
[\#ANA"
那么如果参数为T,其返回值类型就为result_1<T>::result。上面代码的ref<T>为一个类型转换类,作用是返回T的引用。不直接加上&符号的原因是如果T本身就是Q的引用Q&,那么Q&&是非法的。因此ref的实现即为: G0|}s&$yL
$,J0) ~
template < typename T > 4H(8BNgzV
struct ref 2m]4
{ P3]K'*Dyd
typedef T & reference; c|JQ0] K
} ; NmXRA(m
template < typename T > &A*E)T#>#
struct ref < T &> %\(-<aT
{ |(ab0b #
typedef T & reference; qJ(uak
} ; p^*a>d:d]
ZG2EOy
有了result_1之后,就可以把operator()改写一下: rAAx]nQ@
deArH5&!
template < typename T > rdd-W>+
typename result_1 < T > ::result operator ()( const T & t) const ~nhO*bs}7{
{ j~1K(=Ng
return l(t) = r(t); !yPy@eP~
} OdZ/ \_Z
可能大家已经注意到我定义assignment的operator()的返回类型的时候,是直接将其定义为Left的operator()返回类型的引用形式,如果实际上处理的对象的operator=并不是按照常理来声明的,那么这段代码可能就编译不过。这的确是一个很麻烦的事情。实际上,在gcc下,使用typeof关键字可以很容易的得到该类型的operator=的返回类型,就可以让这段代码变得更有通用性。然而为了实现可移植性,我不得不放弃这个诱人的想法。 %qz-b.
同理我们可以给constant_t和holder加上这个result_1。 ;y. ;U#O
\Cu=Le^
有了这个result_1,链式操作就简单多了。现在唯一要做的事情就是让所有的functor都重载各种操作符以产生新的functor。假设我们有add和divide两个类,那么 k(pJVez
_1 / 3 + 5会出现的构造方式是: 1;1;-4k7I
_1 / 3调用holder的operator/ 返回一个divide的对象 A$N%deb
+5 调用divide的对象返回一个add对象。 6IV):S~
最后的布局是: &Z[+V)6,,
Add #h^nvRmON
/ \ 0 K#|11r
Divide 5 C3Q #[
/ \ ?gUraSFU
_1 3 87[ ,.W
似乎一切都解决了?不。 G![d_F"e
你可以想象一下一个完整的Lambda库,它必然能够重载C++几乎所有的操作符。假设其重载了10个操作符,那么至少会有10个代表这些操作符的functor类。大体上来讲,每一种操作符所对应的functor都应当能够由链式操作产生别的任意一种操作符所对应的functor。(例如:*_1 = 2既是由operator*的functor产生operator=的functor)。可想而知这样一共能产生10*10=100种产生方式。这是对编码的一个大挑战。 xjiV9{w
如何简化这个问题呢?我们不妨假定,任意一种操作符的functor,都能够产生任意一种操作符的functor,这样,每一种操作符的functor都拥有一样的产生方案。如果某种转换确实是不合法的(例如:A/B=C无论如何也不可能合法),那么在试图产生新functor的时候会出现编译错误。幸好C++的模版是如果不使用就不编译的,因此这种编译错误不会干扰到正常的使用,这正是我们所要的。 z/`+jIB
OK,我们的方法呼之欲出了。既然所有的functor都具有一样的产生方案,那么不如大家都不要实现,等到最后统一的在所有的functor里面加上这么一系列的产生代码吧。例如,如果要添加从某functor XXX到operator=的functor的产生代码: l^ay*H
0?8>{!I
template < typename Right > _hyqHvP
assignment < XXX, typename picker_maker < Right > ::result > operator = ( const -&`_bf%M
Right & rt) const E
b:iym0
{ i+mU(/l2{
return assignment < XXX, typename picker_maker < Right > ::result > ( * this , rt); |9%~z0
} {q`8+$Z;
下面对该代码的一些细节方面作一些解释 >n3GvZ5%
XXX指的是原来的functor的类型,picker_maker<T>是一个类型变换的trait,如果T是一个常量,那么他会返回constant_t<T>,否则返回T本身。 &gruYZGK
因此如果该函数声明在assignment的内部,那么就实现了连等,如果声明在的dereference(解引用)的内部,就允许(*A = B)的行为发生。 p\6}<b"p
最后,如何把这些函数塞到各个functor的声明里边呢?当然可以用宏,但是。。。大家都知道这样不好。 b9vudr
除了宏之外还可以用的方式就是继承。我们可以写一个类叫做picker,该类实现了所有的如上的产生函数。然后让所有的functor继承自它。 C5-u86F
且慢,也许立刻就有人跳出来说:这样的话那个XXX怎么写呢?这样不是会导致循环依赖么?这样不是会有downcast么? -%Vh-;Ie(
正解,让picker做基类确实不是一个好主意。反过来,让picker继承functor却是一个不错的方法。下面是picker的声明: d@g2 9rs
H390<`
template < class Action > L=qhb;[L
class picker : public Action 3))CD,|
{ $(;Ts)P
public : Ycm .qud
?
picker( const Action & act) : Action(act) {} ~EY)c~H
// all the operator overloaded 3'kKbrk [
} ; 7Z`4Kdh .
a'|]_`36x
Picker<T>继承自T,唯一的作用就是给T添加上了各种操作符的重载函数。 [KYq01cj
现在所有参与行动的functor都要套上一层picker, _1被声明为 picker<holder>, 并且holder中所重载的操作符除了operator()之外全部被移到了picker内。而picker中的操作符重载的返回的functor也必须套上一个picker:
8|{ZcW
8tR6.09'
template < typename Right > J)B3o$
picker < assignment < Action, typename picker_maker < Right > ::result > > operator = ( const Right & rt) const rhQ+ylt8I
{ gh*k\0
return assignment < Action, typename picker_maker < Right > ::result > ( * this , rt); ]gVA6B?&9
} B=K<k+{6"
.eg'Z@o
Piker_maker返回的也是picker<T>,或者picker<constant_t<T> > -s2)!Iko&
使用picker还带来一个额外的好处。之前提到picker_maker要区分functor和常量,有了picker,区分的方法就非常简单了:凡是属于picker<T>的都是functor,否则就是常量。 *Vq'%b9
]S s63Vd
template < typename T > struct picker_maker g2TK(S|#
{ Uz,P^\8^$
typedef picker < constant_t < T > > result; Jj[3rt?8
} ; Mn/
template < typename T > struct picker_maker < picker < T > > gizY4~
j
{ 1}|y^oB\-
typedef picker < T > result; yN{**?b
} ; jZqa+nG51
[dP<A?s
下面总的结构就有了: ]Xnar:5
functor专心模拟操作符的行为,并实现一个result_1来告诉别人自己的返回类型。 ;kZD>G8
picker专心负责操作符之间的产生关系,由它来联系操作符合functor。 u`Nrg<
picker<functor>构成了实际参与操作的对象。 ";(m,if-
至此链式操作完美实现。 qXq#A&
nbP}a?XC
:KvZP:T
七. 问题3 &$CyT6mb^
如何使用多参数的函数对象呢?考虑_1=_2,这个functor必须接受2个参数,因此所产生的assignment对象的operator()必须能接收2个参数。 ~s4JGV~R
EH2):
template < typename T1, typename T2 > lshSRir
??? operator ()( const T1 & t1, const T2 & t2) const ym6Emf]
{ sq#C|v/
return lt(t1, t2) = rt(t1, t2); U:$zlfV
} n8!|}J
)E=B;.FH
很明显,这个函数的返回类型会依赖于T1,T2,因此result_1已经无法适用,我们就只好再写一个result_2: ,/Gp>Yqx
{@7UfJh>
template < typename T1, typename T2 > ^Ff fc@=
struct result_2 |>U<EtA"
{ "gI-S[
typedef typename ref < typename Left::result_2 < T1, T2 > ::result > ::reference result; g~K-'Nw
} ; bt=D<YZk
8M!9gvcaO
显然,各个functor似乎根本不理会各个参数那个是_1, 那个是_2, 那么最后是怎么选择的呢? $<Gt^3e
这个差事就留给了holder自己。 EB+4]MsD
u"v$[8
"[["naa
template < int Order > 9mMQ
class holder; C'A
D[`p
template <> `{"V(YMEV
class holder < 1 > Bq~S=bAB>R
{ otjT?R2g'
public : ^8oN~HLZ
template < typename T > p +JOUW
struct result_1 R6;229e
{ w\d1
typedef T & result; 6I=d0m.io
} ; gPKO-Fsd"
template < typename T1, typename T2 > |Zn,|-iW
struct result_2 %iIr %P?
{ l@UF-n~[
typedef T1 & result; >/C,1}p[
} ; /P3Pv"r|8]
template < typename T > :k.>H.8+~
typename result_1 < T > ::result operator ()( const T & r) const JK^%V\m
{ DPnrzV)
return (T & )r; 0[ n;ZL~
} *yI( (G/
template < typename T1, typename T2 > _%rkN0-(a
typename result_2 < T1, T2 > ::result operator ()( const T1 & r1, const T2 & r2) const r
H9}VA:h
{ T^|6{ S\
return (T1 & )r1; iuEe#B;!
} PB8U+
} ; E(S$Q^
:Oj!J&A
template <> Us&~d"n
class holder < 2 > vy5{Vm".4
{ H9VdoxKo
public : ?5d[BV
template < typename T > A#~CZQY^$
struct result_1 PL\4\dXB
{ !C' Y
7
typedef T & result; M#],#o*G
} ; 9J49s1
template < typename T1, typename T2 > u`+kH8#
struct result_2 /6N!$*8
{ =1B;<aZH!
typedef T2 & result; v%c--cO(S4
} ; ]a~gnz&1
template < typename T > S|RUc}(
typename result_1 < T > ::result operator ()( const T & r) const Jn0L_@
{ Fok`-U
return (T & )r; LwQYO'X
} ]tK<[8Y
template < typename T1, typename T2 > gavf$be
typename result_2 < T1, T2 > ::result operator ()( const T1 & r1, const T2 & r2) const V,tYqhQ3
{ :VRQd}$Pi
return (T2 & )r2; bq5?fPBrq
} x*^)B~7}
} ; 1G, '
A sf]sU..
kafj?F
新的holder变成了holder<int>, holder<n>的n个参数的operator()会返回第n个参数的值。而_1,_2也相应变为picker<holder<1> >, picker<holder<2> >。 F+Hmp\rM#
现在让我们来看看(_1 = _2)(i. j)是怎么调用的: %`dVX
EO
首先 assignment::operator(int, int)被调用: Y#-pK)EeU
U3>ES"N
return l(i, j) = r(i, j); .a]av
先后调用holder<1>::operator()(int, int)和holder<2>::operator()(int, int) gWjz3ob
|2X+( F Ed
return ( int & )i; ]'i}}/}u2
return ( int & )j; >Cr'dKZ}
最后执行i = j; ve/|"RB
可见,参数被正确的选择了。 Z=s]@r
#k)J);&ZA
)Oj%3
pEGHW;
^zS|O]Tx
八. 中期总结 ~ln96*)M;
目前的结果是这样的,为了支持一个操作符,我们需要作如下几件事: P.t7_v>
1。 实现一个functor,该functor的operator()要能执行该操作符的语义 s)~H_,
2。 在该functor中实现result_1至result_n,其中n是支持参数的最大值。 /$ueLa
3。 在picker中实现一个操作符重载,返回该functor D
z>7.'3
/(ArA=#
_H2%6t/V
9[\$\l
'F8:|g
&