一. 什么是Lambda F4-$~v@
所谓Lambda,简单的说就是快速的小函数生成。 ^+>laOzC`8
在C++中,STL的很多算法都要求使用者提供一个函数对象。例如for_each函数,会要求用户提供一个表明“行为”的函数对象。以vector<bool>为例,如果想使用for_each对其中的各元素全部赋值为true,一般需要这么一个函数对象, .GPT!lDc
YNyk1cE
j|DsG,
` xEx^P^7
class filler $kdB |4C
{ g#pr yYz
public : FBe;1OU
void operator ()( bool & i) const {i = true ;}
9]([\% )
} ; r,8 [O
5FPM`hLT
B?gOHG*vd>
这样实现不但麻烦,而且不直观。而如果使用lambda,则允许用户使用一种直观和见解的方式来处理这个问题。以boost.lambda为例,刚才的问题可以这么解决: Drgv`z
6RU~"C
#>("CAB02T
~|DUt
for_each(v.begin(), v.end(), _1 = true ); UawyDs
:gv{F} ##
lV3x *4O=
那么下面,就让我们来实现一个lambda库。 Fh&G;aEq
Wa>}wA=v
lwxaMjaL4K
d=$Mim
二. 战前分析 Z!a=dnwHz
首先要说明的是,我并没有读过boost.lambda或其他任何lambda库的代码,因此如代码有雷同,纯属巧合。 `!3SF|x&
开始实现以前,首先要分析出大致的实现手法。先让我们来看几段使用Lambda的代码 T*/rySs
XB;7!8|
6m/r+?'
for_each(v.begin(), v.end(), _1 = 1 ); U/66L+1
/* --------------------------------------------- */ [x=s(:qy
vector < int *> vp( 10 ); :(U,x<>
transform(v.begin(), v.end(), vp.begin(), & _1); Fo (fWvz
/* --------------------------------------------- */ hlvK5Z
sort(vp.begin(), vp.end(), * _1 > * _2); &.)^
%Tp\z
/* --------------------------------------------- */ x$A+lj]x
int b = * find_if(v.begin, v.end(), _1 >= 3 && _1 < 5 ); z3{G9Np
/* --------------------------------------------- */ n:I,PS0H<
for_each(vp.begin(), vp.end(), cout << * _1 << ' \n ' ); c)6m$5]
/* --------------------------------------------- */ ^KnU4sD
for_each(vp.begin(), vp.end(), cout << constant( ' \n ' ) << * _1); .O5Z8 p
kUL'1!j7
RtkEGxw*^
/Y:sLGQLD
看了之后,我们可以思考一些问题: zJKv'>?
1._1, _2是什么? > ym,{EHK
显然_1和_2都满足C++对于标识符的要求,可见_1和_2都是对象。 P[G)sA_"
2._1 = 1是在做什么? )` Sr fGp8
既然_1是一个对象,那么_1的类必然重载了operator=(int)。那么operator=返回什么呢?该函数所返回的对象被传入for_each的第3个参数,可见其返回了一个函数对象。现在整个流程就很清楚了。_1 = 1调用了operator=,其返回了一个函数对象,该函数对象能够将参数1赋值为1。 Hp|kQJ[L E
Ok,回答了这两个问题之后,我们的思路就很清晰了。如果要实现operator=,那么至少要实现2个类,一个用于产生_1的对象,另一个用于代表operator=返回的函数对象。 b"<liGh"n-
#X+JHl
T8?Ghbn
三. 动工 5 Aw"B
首先实现一个能够范型的进行赋值的函数对象类: ;RZ )
Di,^%
P8OaoPj
:_`F{rDB
template < typename T > \S `:y?[Y
class assignment y;m|
{ "=HA Y
T value; B{n,t}z
public : w8")w*9Lmg
assignment( const T & v) : value(v) {} 9d0@wq.
template < typename T2 > =g7x'
kN
T2 & operator ()(T2 & rhs) const { return rhs = value; } ;Zcswt8]u
} ; ih-#5M@
gMi0FO'
]\-A;}\e
其中operator()被声明为模版函数以支持不同类型之间的赋值。 kYE9M8s;
然后我们就可以书写_1的类来返回assignment >4x(e\B
{ T/[cu<
T=
8 0,
kUb>^-
-K
class holder nmee 'oEw
{ |"q5sym8Y_
public : W<h)HhyG
template < typename T > rm'SOJVA
assignment < T > operator = ( const T & t) const ]6k\)#%2
{ f=+mIZ
return assignment < T > (t); JMCKcZ%N
} g.k"]lP
} ; .r=4pQ@#
g i3F`
m
rET\n(AJ
由于该类是一个空类,因此我们可以在其后放心大胆的写上: @W.S6;GA\
<q58uuK
static holder _1; ^`i#$
Ok,现在一个最简单的lambda就完工了。你可以写 ;HfmzY(
EgEa1l!NSQ
for_each(v.begin(), v.end(), _1 = 1 ); &C5_g$Ma.Z
而不用手动写一个函数对象。 IV~>I-rd
+zqn<<9
7uqzm
A;q9rD,_
四. 问题分析 "m):Y;9iQ?
虽然基本上一个Lambda已经初步实现出来了,但是仔细想想,问题也是很多的。 J/`<!$<c
1, 我们现在是把_1和functor看成两个不同的存在,会导致代码的重复。 YsC>i`n9
2, 目前这个Lambda还无法实现如_1 = 2 = 3这样的链式操作。 ,C\i^>=
3, 我们没有设计好如何处理多个参数的functor。 Gq)]s'r2
下面我们可以对这几个问题进行分析。 #Qw0&kM7I
.fqN|[>
五. 问题1:一致性 c1(RuP:S
首先来看看1,合并_1和functor的最佳方法就是把_1本身也变成functor。那么_1的operator()会做什么事情呢?| dZl5Ic
很明显,_1的operator()仅仅应该返回传进来的参数本身。 )N{Pw$l_
G{~J|{t\yz
struct holder (Bb5?fw
{ 5X:AbF
// 6D;Sgc5"
template < typename T > oi7@s0@
T & operator ()( const T & r) const fivw~z|[@
{ zy?|ODM
return (T & )r; 3@_xBz,I .
} b<[Or^X
]
} ; *uRBzO}
k!j5tsiR
这样的话assignment也必须相应改动: ^]Y>[[
20h}
[Q(
template < typename Left, typename Right > 4&lv6`G `
class assignment D(op)]8
{ W\$`w
Left l; H064BM
Right r; /|m2WxK)
public : <Xhm`rH
assignment( const Left & l, const Right & r) : l(l), r(r) {} ];$L &5^
template < typename T2 > s*KhF'fN
T2 & operator ()(T2 & rhs) const { return l(rhs) = r; } XAKs0*J>
} ; h]&GLb&<?
hg]]Ok~cAs
同时,holder的operator=也需要改动: 5J.bD)yrP
#6aW9GO
template < typename T > 4}baSV
assignment < holder, T > operator = ( const T & t) const ?T8}K>a
{ +zN-!5x
return assignment < holder, T > ( * this , t); q s!j>x
} dh\'<|\K
Xh"n]TK
好,这样holder也成为了一个functor,这为我们以后添加功能节省了很多代码。 gnf8l?M
你可能也注意到,常数和functor地位也不平等。 [ZwjOi:)
lN
4oW3QT
return l(rhs) = r; fCn^=8KOZ
在这一句中,r没有调用operator()而l调用了。这样以后就要不时的区分常数和functor,是不良的设计。 r| wS<cA2
那么我们仿造holder的做法实现一个常数类: ha<[bu e
#pow ub
template < typename Tp > e;q!6%
class constant_t w$iX.2|9%u
{ @Sn(lnlB
const Tp t; &{n.]]%O.
public : LzKj=5'Y
constant_t( const Tp & t) : t(t) {} ?#G$=4;i
template < typename T > a 7V-C
const Tp & operator ()( const T & r) const 2DDtu[}
{ 'W^YM@
return t; .k%72ez
} ,.8KN<A2]'
} ; vzAax k%
qH>d
该functor的operator()无视参数,直接返回内部所存储的常数。 oUlY?x1
下面就可以修改holder的operator=了 /)>3Nq4Zx
Y;M|D'y+
template < typename T > "Qc7dRmSxm
assignment < holder, constant_t < T > > operator = ( const T & t) const 1~_{$5[X?
{ #$07:UJ
return assignment < holder, constant_t < T > > ( * this , constant_t < T > (t)); B)g[3gQ
} N0Lw}@p
',@3>T**
同时也要修改assignment的operator() `:KY\
M#6W(|V/
template < typename T2 > ifQ*,+@fxR
T2 & operator ()(T2 & rhs) const { return l(rhs) = r(rhs); } `wEb<H
现在代码看起来就很一致了。 >z>!Luw
'3fu
六. 问题2:链式操作 s?}e^/"v
现在让我们来看看如何处理链式操作。 :J@gmY:C
其实问题1已经为我们处理掉了大量的问题。如果_1,functor,常量彼此之间不统一为functor,那么链式操作的时候就要时刻小心一个对象是_1还是functor还是常量,会大大增加编码的难度。 +.[ <%
事实上,首先要解决的是,如何知道一个functor的operator()的返回值的类型。遗憾的是,我并没有找到非常自动的办法,因此我们得让functor自己来告诉我们返回值的类型。 ,/I.t DH
比较麻烦的是,operator()的返回值一般和其参数的类型相关,而operator()通常是一个模版函数,因此其返回值类型并不能用一个简单的typedef来指定,而必须实现一个trait。 prF%.(G2)
现在我们在assignment内部声明一个nested-struct =z69e%.
`p-cSxR_
template < typename T > 6,"Q=9k4[
struct result_1 19)i*\+
{ ES7>H
typedef typename ref < typename Left::result_1 < T > ::result > ::reference result; -<!NXm|kvz
} ; }B+C~@j
j{A y\n (
那么如果参数为T,其返回值类型就为result_1<T>::result。上面代码的ref<T>为一个类型转换类,作用是返回T的引用。不直接加上&符号的原因是如果T本身就是Q的引用Q&,那么Q&&是非法的。因此ref的实现即为: "Ac-tzhE
DV-d(@`K
template < typename T > %s|Ely)
struct ref }<SQ
{ E6ElNgL
typedef T & reference; K=k"a
} ; n
M*%o-
template < typename T > }2.`N%[
struct ref < T &> /nNN,hz
{ J=I:CD%
typedef T & reference; Y"aJur=`
} ; Vn}0}Jz
?P`K7
有了result_1之后,就可以把operator()改写一下: oW*16>IN9l
0R'?~`aTt
template < typename T > !)0;&e5
typename result_1 < T > ::result operator ()( const T & t) const d.d/<
{ Id .nu/
return l(t) = r(t); pJ"qu,w
} M`!H"R 7
可能大家已经注意到我定义assignment的operator()的返回类型的时候,是直接将其定义为Left的operator()返回类型的引用形式,如果实际上处理的对象的operator=并不是按照常理来声明的,那么这段代码可能就编译不过。这的确是一个很麻烦的事情。实际上,在gcc下,使用typeof关键字可以很容易的得到该类型的operator=的返回类型,就可以让这段代码变得更有通用性。然而为了实现可移植性,我不得不放弃这个诱人的想法。 P@Oo$ o
同理我们可以给constant_t和holder加上这个result_1。 W+?4jwqw
Ckuh:bs
有了这个result_1,链式操作就简单多了。现在唯一要做的事情就是让所有的functor都重载各种操作符以产生新的functor。假设我们有add和divide两个类,那么 <uw9DU7G
_1 / 3 + 5会出现的构造方式是: 7'V@+5
_1 / 3调用holder的operator/ 返回一个divide的对象 ZDYJ\ }=
+5 调用divide的对象返回一个add对象。 EgCAsSx(
最后的布局是: .jE{ 3^
Add U$ElV]N
/ \ k"zv~`i'
Divide 5 )U:m:cr<
/ \ &.Qrs:U
_1 3 'XjZ_ng
似乎一切都解决了?不。 dOH&
你可以想象一下一个完整的Lambda库,它必然能够重载C++几乎所有的操作符。假设其重载了10个操作符,那么至少会有10个代表这些操作符的functor类。大体上来讲,每一种操作符所对应的functor都应当能够由链式操作产生别的任意一种操作符所对应的functor。(例如:*_1 = 2既是由operator*的functor产生operator=的functor)。可想而知这样一共能产生10*10=100种产生方式。这是对编码的一个大挑战。 |FZ/[9*
如何简化这个问题呢?我们不妨假定,任意一种操作符的functor,都能够产生任意一种操作符的functor,这样,每一种操作符的functor都拥有一样的产生方案。如果某种转换确实是不合法的(例如:A/B=C无论如何也不可能合法),那么在试图产生新functor的时候会出现编译错误。幸好C++的模版是如果不使用就不编译的,因此这种编译错误不会干扰到正常的使用,这正是我们所要的。 @9RM9zK.q
OK,我们的方法呼之欲出了。既然所有的functor都具有一样的产生方案,那么不如大家都不要实现,等到最后统一的在所有的functor里面加上这么一系列的产生代码吧。例如,如果要添加从某functor XXX到operator=的functor的产生代码: {qJ1ko)$
G@X% +$I
template < typename Right > 051E6-
assignment < XXX, typename picker_maker < Right > ::result > operator = ( const |{NYkw
Right & rt) const oQVgyj.
{ : bq8N@P/
return assignment < XXX, typename picker_maker < Right > ::result > ( * this , rt); Hd ={CFip
} A[{yCn`tM
下面对该代码的一些细节方面作一些解释 CxW>~O:
XXX指的是原来的functor的类型,picker_maker<T>是一个类型变换的trait,如果T是一个常量,那么他会返回constant_t<T>,否则返回T本身。 ^%{7}g&$u
因此如果该函数声明在assignment的内部,那么就实现了连等,如果声明在的dereference(解引用)的内部,就允许(*A = B)的行为发生。 T_5H&;a
最后,如何把这些函数塞到各个functor的声明里边呢?当然可以用宏,但是。。。大家都知道这样不好。 kv{za4,&
除了宏之外还可以用的方式就是继承。我们可以写一个类叫做picker,该类实现了所有的如上的产生函数。然后让所有的functor继承自它。 "e>;'%W
且慢,也许立刻就有人跳出来说:这样的话那个XXX怎么写呢?这样不是会导致循环依赖么?这样不是会有downcast么? vw/J8'
正解,让picker做基类确实不是一个好主意。反过来,让picker继承functor却是一个不错的方法。下面是picker的声明: >jLY"
O-hAFKx
template < class Action > L\ "d
class picker : public Action
|TH\`U
{ sBg.u
public : %pL''R9VF
picker( const Action & act) : Action(act) {} 0znR0%~
// all the operator overloaded z,p~z*4
} ; 0pd'93C
3~{:`[0Q
Picker<T>继承自T,唯一的作用就是给T添加上了各种操作符的重载函数。 ={&j07,*a
现在所有参与行动的functor都要套上一层picker, _1被声明为 picker<holder>, 并且holder中所重载的操作符除了operator()之外全部被移到了picker内。而picker中的操作符重载的返回的functor也必须套上一个picker: H40p86@M
*P=VFP
template < typename Right > HBXOjr<,{
picker < assignment < Action, typename picker_maker < Right > ::result > > operator = ( const Right & rt) const 3;{kJQ
{ v$wIm, j
return assignment < Action, typename picker_maker < Right > ::result > ( * this , rt); ;'@9[N9
} <<5(0#y#
U$A]8NZ$S
Piker_maker返回的也是picker<T>,或者picker<constant_t<T> > ^k">A:E2
使用picker还带来一个额外的好处。之前提到picker_maker要区分functor和常量,有了picker,区分的方法就非常简单了:凡是属于picker<T>的都是functor,否则就是常量。 :OT0yA=U
Y]2A&0
template < typename T > struct picker_maker qfm|@v|De5
{ n 0L^e
typedef picker < constant_t < T > > result; /7F:T[
} ; _Q 4)X)F
template < typename T > struct picker_maker < picker < T > > YPk fx
{ _A9AEi'.
typedef picker < T > result; z46~@y%k
} ; d{3QP5
}|NCboM^_
下面总的结构就有了: >0TxUc_va
functor专心模拟操作符的行为,并实现一个result_1来告诉别人自己的返回类型。 HQhM'x
picker专心负责操作符之间的产生关系,由它来联系操作符合functor。 {hrX'2:ClT
picker<functor>构成了实际参与操作的对象。 I,vJbvvl!
至此链式操作完美实现。 ]GkfEh7/J
"@0]G<H
+iRh
七. 问题3 ENs&RZ;
如何使用多参数的函数对象呢?考虑_1=_2,这个functor必须接受2个参数,因此所产生的assignment对象的operator()必须能接收2个参数。 t-bB>q#3>
Lk}J8 V^2
template < typename T1, typename T2 > 7~.9=I'A
??? operator ()( const T1 & t1, const T2 & t2) const V {ddr:]4
{ u\;C;I-? '
return lt(t1, t2) = rt(t1, t2); YUy0!`!`
} F{;((VboN
+VOK%8,p
很明显,这个函数的返回类型会依赖于T1,T2,因此result_1已经无法适用,我们就只好再写一个result_2: BUXpCxQ
c 3)jccWTc
template < typename T1, typename T2 > M%P:n/j
struct result_2 )1`0PJoHE
{ w_K1]<Q*
typedef typename ref < typename Left::result_2 < T1, T2 > ::result > ::reference result; .p"
xVfi6
} ; $DaNbLV
r52gn(,
显然,各个functor似乎根本不理会各个参数那个是_1, 那个是_2, 那么最后是怎么选择的呢? 6mxfLlZ
这个差事就留给了holder自己。 -X2Buz8
9EibIOD^/
I:1C8*/
template < int Order > U8n V[
class holder; M-Y_ Wb3
template <> R8Fv{7]c
class holder < 1 > =MDysb&:
{ Q sCheHP
public : B*Dz{a^.:
template < typename T > oQ[f,7u
struct result_1 ;+hH
{ jasy<IqT!{
typedef T & result; K`fuf=
} ; 7?w*]
template < typename T1, typename T2 > Si;H0uP O
struct result_2 MeZf*'
J
{ K_Eux rPn
typedef T1 & result; 5MJS
~(
} ; #BH*Z(
template < typename T > `1IgzKL9
typename result_1 < T > ::result operator ()( const T & r) const R`E ~ZWC4V
{ $c(nF01
return (T & )r; -;WGS o
} B>P{A7Q
template < typename T1, typename T2 > )R1<N
typename result_2 < T1, T2 > ::result operator ()( const T1 & r1, const T2 & r2) const ^RIl
{ 0[W:d=C`a
return (T1 & )r1; U26}gT)
} 5vnrA'BhBU
} ; ~6LN6}~|.
@*KZ}i@._
template <> 5#E`=C%
class holder < 2 > &`2)V;t
{ 8$Y9ORs4
public : $X,D(
template < typename T > (V2fRv
struct result_1 8XE7]&)];
{ iSs:oH3l
typedef T & result; [FR`Z=%
} ; /R wjCUf
template < typename T1, typename T2 > l}K37f
struct result_2 mrtb*7`$
{ 4ID5q~
typedef T2 & result; +A?U{q
} ; 8&b,qQ~
template < typename T > <x>Mo
typename result_1 < T > ::result operator ()( const T & r) const or}[h09qA
{ Z=vU}S>r|v
return (T & )r; OYn}5RN
} FXkM#}RgNm
template < typename T1, typename T2 > IF:;`r@%
typename result_2 < T1, T2 > ::result operator ()( const T1 & r1, const T2 & r2) const "oO%`:pb
{ }b.%Im<3R
return (T2 & )r2; FJ)$f?=Qd
} n,WqyNt*
} ; s`~IUNJ@P
gV_}-VvP
k~1?VQ+?M
新的holder变成了holder<int>, holder<n>的n个参数的operator()会返回第n个参数的值。而_1,_2也相应变为picker<holder<1> >, picker<holder<2> >。 >}6%#CAf
现在让我们来看看(_1 = _2)(i. j)是怎么调用的: draN0vf
首先 assignment::operator(int, int)被调用: wNd isI
PB\x3pV!}
return l(i, j) = r(i, j); u.xnO cOH!
先后调用holder<1>::operator()(int, int)和holder<2>::operator()(int, int) \(2sW^fY
1#+S+g@#
return ( int & )i; ^s"R$?;h
return ( int & )j; I51@QJX
最后执行i = j; NqWdRU
可见,参数被正确的选择了。 nZYBE030
/f;~X"!
t;\Y{`
XU(eEnmom
4@ai6,<
八. 中期总结 Qq|57X)P*
目前的结果是这样的,为了支持一个操作符,我们需要作如下几件事: FVJGL
1。 实现一个functor,该functor的operator()要能执行该操作符的语义 Oxd]y1
2。 在该functor中实现result_1至result_n,其中n是支持参数的最大值。 2g! +<YZ~
3。 在picker中实现一个操作符重载,返回该functor j|#Bo:2km
9p(.A$
,Ko!$29[
H"WprHe
hkQ"OsU
$yNS
pNmT0
九. 简化 tK\~A,=
很明显,要支持一个操作符所要做的工作太多了,而且在每个functor中申明result_1至result_n,可见如果n发生变化,维护的开销极大。 E hMNap}5"
我们现在需要找到一个自动生成这种functor的方法。 z-)O9PV
首先,我们注意到result_x的形式很统一。对于各种操作符,其返回值无非下列几种: Lw>N rY(Y
1. 返回值。如果本身为引用,就去掉引用。 BnasI;yWb
+-*/&|^等 wz%NbLy-
2. 返回引用。 *gWwALGo5
=,各种复合赋值等 $-sHWYZ
3. 返回固定类型。 @E|}Y
各种逻辑/比较操作符(返回bool) oXF.1f/h
4. 原样返回。 #QMz<P/Gl6
operator, )\$|X}uny&
5. 返回解引用的类型。 97!;.f-
operator*(单目) +52{-a,>
6. 返回地址。 g3y+&Y_
operator&(单目) oNF6<A(@$
7. 下表访问返回类型。 pFjK}JOF
operator[] *J`O"a
8. 如果左操作数是一个stream,返回引用,否则返回值 /9fR'EO{x
operator<<和operator>> O:Tj"@h
Xc&9Glf
OK,这样我们将返回值类型总结为以上8种,就可以将各种result_x从functor中剥离出来了。 Qzw;i8n{
例如针对第一条,我们实现一个policy类: /mzlH
NTs aW}g
template < typename Left > Z(CkZll
struct value_return "=Me M)K
{ e$rZ5X
template < typename T > b d!Y\OD
struct result_1 },-H"Qs
{ Pe3o;mx
typedef typename const_value < typename Left::template result_1 < T > ::result_type > ::value_type result_type; X=&KayD
} ; hp|YE'uYT
U&q