一. 什么是Lambda XOU
9r(
所谓Lambda,简单的说就是快速的小函数生成。 {D$+~lO
在C++中,STL的很多算法都要求使用者提供一个函数对象。例如for_each函数,会要求用户提供一个表明“行为”的函数对象。以vector<bool>为例,如果想使用for_each对其中的各元素全部赋值为true,一般需要这么一个函数对象,
KrB"2e+J
uZCPxog
L+&$/1h]
zpJQ7hym
class filler Zv-#v
{ q.*k
J/L
public : _G@)Bj^*
void operator ()( bool & i) const {i = true ;} [:Sl^ Z&6M
} ; -GH>12YP
:U=*@p4?
o^mW`g8[
这样实现不但麻烦,而且不直观。而如果使用lambda,则允许用户使用一种直观和见解的方式来处理这个问题。以boost.lambda为例,刚才的问题可以这么解决: MGK%F#PM
t~3!| @3i
`$05+UU
H+` Zp
for_each(v.begin(), v.end(), _1 = true ); jx J5F3d
nwf(`=TC
(V&$KDOA
那么下面,就让我们来实现一个lambda库。 xtyOG
^tI
,eZ
`Ps&N^[
?|kwYA$4o
二. 战前分析 Ch>r.OfP
首先要说明的是,我并没有读过boost.lambda或其他任何lambda库的代码,因此如代码有雷同,纯属巧合。 )m|)cLT&
开始实现以前,首先要分析出大致的实现手法。先让我们来看几段使用Lambda的代码 ,XU<2jv]
H>X:#xOA_
1
Qln|b8<
for_each(v.begin(), v.end(), _1 = 1 ); zt6GJz1q
/* --------------------------------------------- */ Kqm2TMO]>V
vector < int *> vp( 10 ); m9 1Gc?c
transform(v.begin(), v.end(), vp.begin(), & _1); @kd`9Yw
/* --------------------------------------------- */ :>f}rq
sort(vp.begin(), vp.end(), * _1 > * _2); /@ m]@
/* --------------------------------------------- */ -V7dSi
int b = * find_if(v.begin, v.end(), _1 >= 3 && _1 < 5 ); /V0[Urc@
/* --------------------------------------------- */ wt]onve}%
for_each(vp.begin(), vp.end(), cout << * _1 << ' \n ' ); Z):q 1:y
/* --------------------------------------------- */ MR}=tO
for_each(vp.begin(), vp.end(), cout << constant( ' \n ' ) << * _1); ~7ZWtg;B
x. 8fxogz
NYw>Z>TD8c
~wvu7
看了之后,我们可以思考一些问题: {\hjKP
1._1, _2是什么? f3^Anaa]l
显然_1和_2都满足C++对于标识符的要求,可见_1和_2都是对象。 *PM#ngLX}r
2._1 = 1是在做什么? f?W_/daP
既然_1是一个对象,那么_1的类必然重载了operator=(int)。那么operator=返回什么呢?该函数所返回的对象被传入for_each的第3个参数,可见其返回了一个函数对象。现在整个流程就很清楚了。_1 = 1调用了operator=,其返回了一个函数对象,该函数对象能够将参数1赋值为1。 4
Fl>XM
Ok,回答了这两个问题之后,我们的思路就很清晰了。如果要实现operator=,那么至少要实现2个类,一个用于产生_1的对象,另一个用于代表operator=返回的函数对象。 ]Q$S ei5
t^
Ge "
!Ah v07SI
三. 动工 )V d^#p
首先实现一个能够范型的进行赋值的函数对象类: $t0o*i{
c^3,e/H
iSbPOC7
||D PIn]
template < typename T > !y+uQ_IS@
class assignment x n?$@
{ >jz9o9?8
T value; *+(rQ";x
public : w$iQ,--
assignment( const T & v) : value(v) {} R#HVrzOO|T
template < typename T2 > ^p)#;$6b
T2 & operator ()(T2 & rhs) const { return rhs = value; } OYSq)!:
} ; 'hR0JXy
5\V""fH
KT[ZOtu
其中operator()被声明为模版函数以支持不同类型之间的赋值。 agt/;>q\~
然后我们就可以书写_1的类来返回assignment Hsn'"
C~Hhi-Xl)
qA0PGo
# ~Doz7~
class holder sKCYGt$
{ hi`[
public : DG?g~{Y~b
template < typename T > t'1g+g
assignment < T > operator = ( const T & t) const Qo32oT[DM
{ ,BUrZA2\U$
return assignment < T > (t); ;.'?(iEB
} ulE5lG0c
} ; LAkBf
PriLV4?
FY<Q|Ov
由于该类是一个空类,因此我们可以在其后放心大胆的写上: 4M#i_.`z
h+=IxF4
static holder _1; hjyM xg;Q?
Ok,现在一个最简单的lambda就完工了。你可以写 By waD?
{xx}xib3
for_each(v.begin(), v.end(), _1 = 1 ); "}MP {/
而不用手动写一个函数对象。 {]2^b )
47N,jVt4
_K}q%In
?r0rY?
四. 问题分析 `WIZY33V
虽然基本上一个Lambda已经初步实现出来了,但是仔细想想,问题也是很多的。 , #=TputM
1, 我们现在是把_1和functor看成两个不同的存在,会导致代码的重复。 9#TD1B/
2, 目前这个Lambda还无法实现如_1 = 2 = 3这样的链式操作。 @R%*; )*F
3, 我们没有设计好如何处理多个参数的functor。 ~7 `,}) d
下面我们可以对这几个问题进行分析。 G9NI`]k
n]df)a
五. 问题1:一致性 "iTjiH)Q(
首先来看看1,合并_1和functor的最佳方法就是把_1本身也变成functor。那么_1的operator()会做什么事情呢?| <8(=Lv`)q
很明显,_1的operator()仅仅应该返回传进来的参数本身。 !(
>U3N
LaO8)lqR
struct holder a*-9n-U@[k
{ _V4O#;%?
// !KMl'kswe:
template < typename T > <rtKPlb//
T & operator ()( const T & r) const /jNvHo^B
{ fcxg6W'
return (T & )r; P0y DL:X[
} v^ "qr?3V
} ; A|GtF3:G
]!ox2m_U
这样的话assignment也必须相应改动: VwpC UW
?r KbL^2
template < typename Left, typename Right > 10fxK
class assignment Vbt!, 2_)
{ ^R=`<jx
Left l; ;89kL]
Right r; 8T1zL.u>q
public : [3"F$?e5
assignment( const Left & l, const Right & r) : l(l), r(r) {} vn+XY=Qnr
template < typename T2 > gUNhN1=
T2 & operator ()(T2 & rhs) const { return l(rhs) = r; } G &xtL
} ; Pr1qX5> =
y{/7z}d
同时,holder的operator=也需要改动: 0KnL{Cj
M^[;{p2uZ
template < typename T > _tJt
eDRY
assignment < holder, T > operator = ( const T & t) const jB8Q% {%
{ ele@xl
return assignment < holder, T > ( * this , t); yAfwQ$Ll7
} q[_qZ
pd:7K'yaw
好,这样holder也成为了一个functor,这为我们以后添加功能节省了很多代码。 "h#R>3I1)
你可能也注意到,常数和functor地位也不平等。 Wk\(jaL%
M#;
ks9
return l(rhs) = r; @Wc5r#
在这一句中,r没有调用operator()而l调用了。这样以后就要不时的区分常数和functor,是不良的设计。 .6P.r}
那么我们仿造holder的做法实现一个常数类: YZ5,K6u
`mzlOB
template < typename Tp > M2Jf-2
class constant_t g35!a<JW
{ x>}ml\R
const Tp t; =nHKTB>
public : iP0m1
constant_t( const Tp & t) : t(t) {} N2O *g`YC
template < typename T > r5DRF4,7
const Tp & operator ()( const T & r) const V_:`K$
{ HD^#"
return t; SK
lvZ
} _8a;5hS
} ; qS#G7~ur>y
c`soVqT$?
该functor的operator()无视参数,直接返回内部所存储的常数。 '|DW#l\n
下面就可以修改holder的operator=了 -T,?'J0 2
lFGuQLuqA{
template < typename T > &1$d`>fn
assignment < holder, constant_t < T > > operator = ( const T & t) const r|EN 5
{ R3~,&ab
return assignment < holder, constant_t < T > > ( * this , constant_t < T > (t)); B:Ts_9*
} J-hJqR*;K
Jqj!k*=/
同时也要修改assignment的operator() g%&E~V/g$
>E>yA d
template < typename T2 > HEBeJ2w
T2 & operator ()(T2 & rhs) const { return l(rhs) = r(rhs); } q7X#LY k
现在代码看起来就很一致了。 @khFk.LBD
x"{aO6M
六. 问题2:链式操作 SI=$s>1
现在让我们来看看如何处理链式操作。 =0pt-FQ
其实问题1已经为我们处理掉了大量的问题。如果_1,functor,常量彼此之间不统一为functor,那么链式操作的时候就要时刻小心一个对象是_1还是functor还是常量,会大大增加编码的难度。 h+}BtKA
事实上,首先要解决的是,如何知道一个functor的operator()的返回值的类型。遗憾的是,我并没有找到非常自动的办法,因此我们得让functor自己来告诉我们返回值的类型。 /~Y\KOH|
比较麻烦的是,operator()的返回值一般和其参数的类型相关,而operator()通常是一个模版函数,因此其返回值类型并不能用一个简单的typedef来指定,而必须实现一个trait。 r,Uk)xa/^
现在我们在assignment内部声明一个nested-struct O;H6`JQ
hyH[`wiq
template < typename T > ysz =Xw
struct result_1 m+0yf(w
{ dymq
Z<
typedef typename ref < typename Left::result_1 < T > ::result > ::reference result; .\ ;'>qy
} ; UJL2IF-x
1uAjy(y
那么如果参数为T,其返回值类型就为result_1<T>::result。上面代码的ref<T>为一个类型转换类,作用是返回T的引用。不直接加上&符号的原因是如果T本身就是Q的引用Q&,那么Q&&是非法的。因此ref的实现即为: I/`"lAFe
8@t8P5(vL
template < typename T > UGSZg|&6#*
struct ref {V6&((E8
{ oZa'cZNs
typedef T & reference; J,F1Xmr4
} ; p?i.<Z
template < typename T > fOV_ >]u
struct ref < T &> lI<jYd
0fZ
{ GGp.u@\r
typedef T & reference; uzBQK
} ; sp,-JZD
oX|T&"&
有了result_1之后,就可以把operator()改写一下: e9o\qEm
xqt?z n
template < typename T > $fmTa02q>
typename result_1 < T > ::result operator ()( const T & t) const `,qft[1
{ (QDKw}O2b
return l(t) = r(t); #7]Jz.S
} 3YHEH\60^
可能大家已经注意到我定义assignment的operator()的返回类型的时候,是直接将其定义为Left的operator()返回类型的引用形式,如果实际上处理的对象的operator=并不是按照常理来声明的,那么这段代码可能就编译不过。这的确是一个很麻烦的事情。实际上,在gcc下,使用typeof关键字可以很容易的得到该类型的operator=的返回类型,就可以让这段代码变得更有通用性。然而为了实现可移植性,我不得不放弃这个诱人的想法。 BpZ~6WtBq
同理我们可以给constant_t和holder加上这个result_1。 lL}NiN-)t
'X;cgAq8(
有了这个result_1,链式操作就简单多了。现在唯一要做的事情就是让所有的functor都重载各种操作符以产生新的functor。假设我们有add和divide两个类,那么 (`1io
_1 / 3 + 5会出现的构造方式是: G-d7}Uz?
_1 / 3调用holder的operator/ 返回一个divide的对象 hzo> :U
+5 调用divide的对象返回一个add对象。 G?s9c0f
最后的布局是: o;$xN3f,
Add 'JOUx_@z
/ \ ;7'O=%
Divide 5 KqK]R6>
/ \ Ymz/:
_1 3 gJQ#j~'
似乎一切都解决了?不。 /65YHXg,
你可以想象一下一个完整的Lambda库,它必然能够重载C++几乎所有的操作符。假设其重载了10个操作符,那么至少会有10个代表这些操作符的functor类。大体上来讲,每一种操作符所对应的functor都应当能够由链式操作产生别的任意一种操作符所对应的functor。(例如:*_1 = 2既是由operator*的functor产生operator=的functor)。可想而知这样一共能产生10*10=100种产生方式。这是对编码的一个大挑战。 ,<v0(
如何简化这个问题呢?我们不妨假定,任意一种操作符的functor,都能够产生任意一种操作符的functor,这样,每一种操作符的functor都拥有一样的产生方案。如果某种转换确实是不合法的(例如:A/B=C无论如何也不可能合法),那么在试图产生新functor的时候会出现编译错误。幸好C++的模版是如果不使用就不编译的,因此这种编译错误不会干扰到正常的使用,这正是我们所要的。 wZ(1\
M(
OK,我们的方法呼之欲出了。既然所有的functor都具有一样的产生方案,那么不如大家都不要实现,等到最后统一的在所有的functor里面加上这么一系列的产生代码吧。例如,如果要添加从某functor XXX到operator=的functor的产生代码: fz(YP=@ZnP
?9`j1[0
template < typename Right > =W~7fs
assignment < XXX, typename picker_maker < Right > ::result > operator = ( const 8A#,*@V[
Right & rt) const ~CNB3r5R
{ @G4Z
return assignment < XXX, typename picker_maker < Right > ::result > ( * this , rt); ], lLDUZ\
} C%z)D1-
下面对该代码的一些细节方面作一些解释 Tqt-zX|>
XXX指的是原来的functor的类型,picker_maker<T>是一个类型变换的trait,如果T是一个常量,那么他会返回constant_t<T>,否则返回T本身。 "w:h
因此如果该函数声明在assignment的内部,那么就实现了连等,如果声明在的dereference(解引用)的内部,就允许(*A = B)的行为发生。 !"N,w9MbD
最后,如何把这些函数塞到各个functor的声明里边呢?当然可以用宏,但是。。。大家都知道这样不好。 /6')B !&
除了宏之外还可以用的方式就是继承。我们可以写一个类叫做picker,该类实现了所有的如上的产生函数。然后让所有的functor继承自它。 yaR>?[h
且慢,也许立刻就有人跳出来说:这样的话那个XXX怎么写呢?这样不是会导致循环依赖么?这样不是会有downcast么? @IL04' \
正解,让picker做基类确实不是一个好主意。反过来,让picker继承functor却是一个不错的方法。下面是picker的声明: wlXs/\es
T#ls2UL*xh
template < class Action > Xq? >a+B
class picker : public Action B!wN%>U
{ 8,U~ p<Gz
public : !D=!
picker( const Action & act) : Action(act) {} 8 0tA5AP
// all the operator overloaded sY;h~a0n
} ; Uu_qy(4
vNSUrf,r
Picker<T>继承自T,唯一的作用就是给T添加上了各种操作符的重载函数。 c,a8#Og
现在所有参与行动的functor都要套上一层picker, _1被声明为 picker<holder>, 并且holder中所重载的操作符除了operator()之外全部被移到了picker内。而picker中的操作符重载的返回的functor也必须套上一个picker: o(hUC$vW
JP>EW&M
template < typename Right > GHsDZ(d3.
picker < assignment < Action, typename picker_maker < Right > ::result > > operator = ( const Right & rt) const s<!A<+Sh
{ JWNN5#=fQ
return assignment < Action, typename picker_maker < Right > ::result > ( * this , rt); WZ'<iI
} Jh-yIk
E=I'$*C\D
Piker_maker返回的也是picker<T>,或者picker<constant_t<T> > ]3 "0#Y
使用picker还带来一个额外的好处。之前提到picker_maker要区分functor和常量,有了picker,区分的方法就非常简单了:凡是属于picker<T>的都是functor,否则就是常量。 &W\e 5X<A
?MH=8Cl1w
template < typename T > struct picker_maker `i`P}W!F
{ w|f+OlPXq
typedef picker < constant_t < T > > result; "S;4hO
} ; j9fBl:Fr
template < typename T > struct picker_maker < picker < T > > 2xNR=u`
{ 7nB4(A2[S4
typedef picker < T > result; b7sfr!t_d
} ; "9;
HxO+JI`'3
下面总的结构就有了: A?MM9Y}K
functor专心模拟操作符的行为,并实现一个result_1来告诉别人自己的返回类型。 TAYh#T=S
picker专心负责操作符之间的产生关系,由它来联系操作符合functor。 [j6]!p]S$
picker<functor>构成了实际参与操作的对象。 V D#q\
至此链式操作完美实现。 sl$6Zv-l%0
^(q .f=I!a
QD-\'Bp/X
七. 问题3 /nO_e
如何使用多参数的函数对象呢?考虑_1=_2,这个functor必须接受2个参数,因此所产生的assignment对象的operator()必须能接收2个参数。 TzKM~a#
&& ]ix3
template < typename T1, typename T2 > WSozDNF!'f
??? operator ()( const T1 & t1, const T2 & t2) const lV'?X%
{ 1K/HVj+'.
return lt(t1, t2) = rt(t1, t2); ?8O5%IrJ
} g:!U,<C^a
(-S^L'v62v
很明显,这个函数的返回类型会依赖于T1,T2,因此result_1已经无法适用,我们就只好再写一个result_2: <-1:o*8:}
rZgu`5<a
template < typename T1, typename T2 > -
|pe D
L
struct result_2 ThV>gn5
{ y3;M$Jr
typedef typename ref < typename Left::result_2 < T1, T2 > ::result > ::reference result; H,D5)1Uu
} ; JZ}zXv
Q&I #
显然,各个functor似乎根本不理会各个参数那个是_1, 那个是_2, 那么最后是怎么选择的呢? Uh0g !zzp
这个差事就留给了holder自己。 fq>{5ODO
wqG#jC!5
&k'<