一. 什么是Lambda !m1pL0
所谓Lambda,简单的说就是快速的小函数生成。 |C S[>0mV!
在C++中,STL的很多算法都要求使用者提供一个函数对象。例如for_each函数,会要求用户提供一个表明“行为”的函数对象。以vector<bool>为例,如果想使用for_each对其中的各元素全部赋值为true,一般需要这么一个函数对象, `#X{.
";e0-t6:
$sO}l
7j&l2Z
class filler <_H0Q_/(
{ &w3LMOT
public : 8X]j;Rb
void operator ()( bool & i) const {i = true ;} z@ A5t4+3
} ; 1W
HR;!u
? F fw'O
$/45*
这样实现不但麻烦,而且不直观。而如果使用lambda,则允许用户使用一种直观和见解的方式来处理这个问题。以boost.lambda为例,刚才的问题可以这么解决: !{SU G+.2
@11voD
?kb\%pcK
^\mN<z(
for_each(v.begin(), v.end(), _1 = true ); >|7&hj$
zT~ GBC-IX
1)NX;CN
那么下面,就让我们来实现一个lambda库。 (vjQF$Hp
*(d^k;
&^9>h/-XT
j>R7OGg'
二. 战前分析 -ij1%#t z
首先要说明的是,我并没有读过boost.lambda或其他任何lambda库的代码,因此如代码有雷同,纯属巧合。 J\
开始实现以前,首先要分析出大致的实现手法。先让我们来看几段使用Lambda的代码 Ye!=
K"b vUH
,^o^@SI)
for_each(v.begin(), v.end(), _1 = 1 ); mXF
pGo5 s
/* --------------------------------------------- */ <z)MV
oa
vector < int *> vp( 10 ); N
&p=4
transform(v.begin(), v.end(), vp.begin(), & _1); Ze Shn
/* --------------------------------------------- */
VV]{R'
sort(vp.begin(), vp.end(), * _1 > * _2); 4'9h^C&
/* --------------------------------------------- */ :eQxdi'
int b = * find_if(v.begin, v.end(), _1 >= 3 && _1 < 5 ); x)vYc36H
/* --------------------------------------------- */ K{>O.5
for_each(vp.begin(), vp.end(), cout << * _1 << ' \n ' ); S_2I8G^A
/* --------------------------------------------- */ rT}d<cSf
for_each(vp.begin(), vp.end(), cout << constant( ' \n ' ) << * _1); a/^YgrC\T
x'JfRz
-07(#>
fBd +gT\S
看了之后,我们可以思考一些问题: TJsT .DWW~
1._1, _2是什么? 9f,HjRP
显然_1和_2都满足C++对于标识符的要求,可见_1和_2都是对象。 rzaEVXbz1
2._1 = 1是在做什么? 'Dk(jpYB
既然_1是一个对象,那么_1的类必然重载了operator=(int)。那么operator=返回什么呢?该函数所返回的对象被传入for_each的第3个参数,可见其返回了一个函数对象。现在整个流程就很清楚了。_1 = 1调用了operator=,其返回了一个函数对象,该函数对象能够将参数1赋值为1。 =AcbX_[
Ok,回答了这两个问题之后,我们的思路就很清晰了。如果要实现operator=,那么至少要实现2个类,一个用于产生_1的对象,另一个用于代表operator=返回的函数对象。 `(B1 "qRi
a/)TJv
u{p\8v%7
三. 动工 Bdbw!zRR$
首先实现一个能够范型的进行赋值的函数对象类: <6L$:vT_
N{p2@_fnB
<O\z`aA'q
FT(EH
template < typename T > *%)L?*
class assignment vlj|[joXw
{ 4?yc/F=kI
T value; ;- ]f4O8
public : )s=z i"
assignment( const T & v) : value(v) {} tfv]AC7x
template < typename T2 > B4|%E$1+
T2 & operator ()(T2 & rhs) const { return rhs = value; } l3iL.?&Pa
} ; 053W2Si
H#Og0gEE}5
V">Uh@[J_
其中operator()被声明为模版函数以支持不同类型之间的赋值。 dEe/\i'r9
然后我们就可以书写_1的类来返回assignment eIqj7UY_
DD3J2J
w@%W{aUC
KP<J~+_ik
class holder @Qc['V)
{ qo.
6T
public : p-(Z[G*
template < typename T > /{kyjf[o&*
assignment < T > operator = ( const T & t) const *iC
t4J
{ B-&J]H
return assignment < T > (t); Cq(Xa-
} Y6D=tb
} ; ryn)
=v;-{oN!
ZA9']u%EJ
由于该类是一个空类,因此我们可以在其后放心大胆的写上: W>DpDrO4ml
giu~"#0/F
static holder _1; U.^)|IHW
Ok,现在一个最简单的lambda就完工了。你可以写 h;ShNU
"!Qhk3*
for_each(v.begin(), v.end(), _1 = 1 ); ReK@~#hLY
而不用手动写一个函数对象。 )7i?8XiSZF
l5h9Eq
s)M2Z3>+
D 1hKjB&
四. 问题分析 'Yd%Tb|*
虽然基本上一个Lambda已经初步实现出来了,但是仔细想想,问题也是很多的。 `jZX(H
1, 我们现在是把_1和functor看成两个不同的存在,会导致代码的重复。 MZd\.]G@
2, 目前这个Lambda还无法实现如_1 = 2 = 3这样的链式操作。 *UyV@
3, 我们没有设计好如何处理多个参数的functor。 /e7'5#v
下面我们可以对这几个问题进行分析。 $&e(V6A@
xY~
DMcO?
五. 问题1:一致性 BO9Z"|"
首先来看看1,合并_1和functor的最佳方法就是把_1本身也变成functor。那么_1的operator()会做什么事情呢?| Zi[)(agAT
很明显,_1的operator()仅仅应该返回传进来的参数本身。 mJsYY,b8
Iiy:<c
struct holder S|>Up%{n[
{ I Mv^ 9T:
// x1}q!)e
template < typename T > q;>BltU
T & operator ()( const T & r) const d#b{4zF"
{ zPw
R1>gL
return (T & )r; "pWdz}!
} AQiP2`?
} ; TAAsV#l
[y{ag{
这样的话assignment也必须相应改动: <#s-hQ
O?2<rbx
template < typename Left, typename Right > n7MS{`
class assignment c'|MC[^A
{ MV/~Rmd.
Left l; DS$ _"'g%i
Right r; "yz\p,
public : W8P**ze4)
assignment( const Left & l, const Right & r) : l(l), r(r) {} R Nv<kw
template < typename T2 > {//F>5~[
T2 & operator ()(T2 & rhs) const { return l(rhs) = r; } R_N<j
} ; ?}]kIK}MC
Y(:OfC?
同时,holder的operator=也需要改动: O)5PUyC:H
)R +o8C
template < typename T > sTA/2d
assignment < holder, T > operator = ( const T & t) const =3zn
Ta }
{ @NHRuk+
return assignment < holder, T > ( * this , t); '0O[ dN
} m+m6"yE#_
_kx
好,这样holder也成为了一个functor,这为我们以后添加功能节省了很多代码。 z1FbW&V
你可能也注意到,常数和functor地位也不平等。 =Hx~]1
bMUIe\/v[
return l(rhs) = r; vV[dJ%
在这一句中,r没有调用operator()而l调用了。这样以后就要不时的区分常数和functor,是不良的设计。 5"gRz9Ta`
那么我们仿造holder的做法实现一个常数类: =k*0O_
&S3W/lQs
template < typename Tp > #PrV)en
class constant_t :1lE98=
{ g_>ZE
const Tp t; -oZac
public : wqwJpWIe
constant_t( const Tp & t) : t(t) {} .#:,j1L"53
template < typename T > L~oFW'
const Tp & operator ()( const T & r) const y{{EC#
{ 9kF#*
return t; ft'iv
} ,SyUr/D
} ; !U#++Zig%
B@;)$1-UT
该functor的operator()无视参数,直接返回内部所存储的常数。 Gtg)%`
下面就可以修改holder的operator=了 Ky yG8;G%
,Mhe:^3
template < typename T > gZjOlp
assignment < holder, constant_t < T > > operator = ( const T & t) const p\{+l;`
{ X]yERaJ,i
return assignment < holder, constant_t < T > > ( * this , constant_t < T > (t)); 87K)qsv8
} g&Z7h4!\
zkp
Apj].
同时也要修改assignment的operator() V{h@nhq
i)2))C
template < typename T2 > Ft7a\vn*B
T2 & operator ()(T2 & rhs) const { return l(rhs) = r(rhs); } N-rmk
现在代码看起来就很一致了。 ya{>=
qp W#!Vbx
六. 问题2:链式操作 2ZO'X9
现在让我们来看看如何处理链式操作。 j>o +}p?3I
其实问题1已经为我们处理掉了大量的问题。如果_1,functor,常量彼此之间不统一为functor,那么链式操作的时候就要时刻小心一个对象是_1还是functor还是常量,会大大增加编码的难度。 bJ|?5
事实上,首先要解决的是,如何知道一个functor的operator()的返回值的类型。遗憾的是,我并没有找到非常自动的办法,因此我们得让functor自己来告诉我们返回值的类型。 <]'"e]
比较麻烦的是,operator()的返回值一般和其参数的类型相关,而operator()通常是一个模版函数,因此其返回值类型并不能用一个简单的typedef来指定,而必须实现一个trait。 @g75T` N
现在我们在assignment内部声明一个nested-struct N4To#Q1w
xploFw~
template < typename T > s3M84w z
struct result_1 x
ctU.)p
{ gFT~\3jp=
typedef typename ref < typename Left::result_1 < T > ::result > ::reference result; t%U[\\ic
} ; A(n=kx
r}\h\ {
那么如果参数为T,其返回值类型就为result_1<T>::result。上面代码的ref<T>为一个类型转换类,作用是返回T的引用。不直接加上&符号的原因是如果T本身就是Q的引用Q&,那么Q&&是非法的。因此ref的实现即为:
Is@a,k
IMGqJc,7
template < typename T > ~B&*7Q7
struct ref pIu H*4Vz
{ m IzBK]@^
typedef T & reference; %<?ciU
} ; w`}9/s;$
template < typename T > s1vrzze
struct ref < T &> Z)
Xs;7
{ M_1Tx
typedef T & reference; e_=pspnZ
} ; zcB2[eaV
b.4Xn0-M
有了result_1之后,就可以把operator()改写一下: vSYKe
!/}FPM_
template < typename T > `XxG"k\/S
typename result_1 < T > ::result operator ()( const T & t) const e(#IewKp
{ =YGP%}_.p{
return l(t) = r(t); mY`]33??v
} 5!ubY
6Ph
可能大家已经注意到我定义assignment的operator()的返回类型的时候,是直接将其定义为Left的operator()返回类型的引用形式,如果实际上处理的对象的operator=并不是按照常理来声明的,那么这段代码可能就编译不过。这的确是一个很麻烦的事情。实际上,在gcc下,使用typeof关键字可以很容易的得到该类型的operator=的返回类型,就可以让这段代码变得更有通用性。然而为了实现可移植性,我不得不放弃这个诱人的想法。 tin|,jA =
同理我们可以给constant_t和holder加上这个result_1。 ;a#*|vx
9*KMbd^T
有了这个result_1,链式操作就简单多了。现在唯一要做的事情就是让所有的functor都重载各种操作符以产生新的functor。假设我们有add和divide两个类,那么 b!]0mXU
_1 / 3 + 5会出现的构造方式是: f9,EWuQNS
_1 / 3调用holder的operator/ 返回一个divide的对象 ^QAiySR`0
+5 调用divide的对象返回一个add对象。 fhV0S>*<
最后的布局是: z8[H:W#G
Add <{/;1Dru
/ \ `.'i V[fr
Divide 5 lV<Tsk'
/ \ 20VVOnDY
_1 3 Lq-33#n/
似乎一切都解决了?不。 oM<!I0"gC+
你可以想象一下一个完整的Lambda库,它必然能够重载C++几乎所有的操作符。假设其重载了10个操作符,那么至少会有10个代表这些操作符的functor类。大体上来讲,每一种操作符所对应的functor都应当能够由链式操作产生别的任意一种操作符所对应的functor。(例如:*_1 = 2既是由operator*的functor产生operator=的functor)。可想而知这样一共能产生10*10=100种产生方式。这是对编码的一个大挑战。 A*;?U2
如何简化这个问题呢?我们不妨假定,任意一种操作符的functor,都能够产生任意一种操作符的functor,这样,每一种操作符的functor都拥有一样的产生方案。如果某种转换确实是不合法的(例如:A/B=C无论如何也不可能合法),那么在试图产生新functor的时候会出现编译错误。幸好C++的模版是如果不使用就不编译的,因此这种编译错误不会干扰到正常的使用,这正是我们所要的。 *w/WHQ`xI
OK,我们的方法呼之欲出了。既然所有的functor都具有一样的产生方案,那么不如大家都不要实现,等到最后统一的在所有的functor里面加上这么一系列的产生代码吧。例如,如果要添加从某functor XXX到operator=的functor的产生代码: /u)Rppu
:B=8_M
template < typename Right > NGD*ce"w
assignment < XXX, typename picker_maker < Right > ::result > operator = ( const Q0cY/'>4
Right & rt) const ck+b/.gw`
{ qon{
g
return assignment < XXX, typename picker_maker < Right > ::result > ( * this , rt); tKZ&1E
} C4{\@v}t
下面对该代码的一些细节方面作一些解释 ISS\uj63M
XXX指的是原来的functor的类型,picker_maker<T>是一个类型变换的trait,如果T是一个常量,那么他会返回constant_t<T>,否则返回T本身。 ESMG<vW&f
因此如果该函数声明在assignment的内部,那么就实现了连等,如果声明在的dereference(解引用)的内部,就允许(*A = B)的行为发生。 *J_iXu|
最后,如何把这些函数塞到各个functor的声明里边呢?当然可以用宏,但是。。。大家都知道这样不好。 VD24X
除了宏之外还可以用的方式就是继承。我们可以写一个类叫做picker,该类实现了所有的如上的产生函数。然后让所有的functor继承自它。 poD\C;o"
且慢,也许立刻就有人跳出来说:这样的话那个XXX怎么写呢?这样不是会导致循环依赖么?这样不是会有downcast么? ,?k%jcR
正解,让picker做基类确实不是一个好主意。反过来,让picker继承functor却是一个不错的方法。下面是picker的声明: _(6`{PWY
]G0dS
Fh{j
template < class Action > RxZm/:yuJ.
class picker : public Action Taf
n:Nw}
{ xP/OsaxN
public : MCeu0e^)
picker( const Action & act) : Action(act) {} @8nLQh^
// all the operator overloaded qWO]s=V!
} ; HK0::6n{
's[BK/
Picker<T>继承自T,唯一的作用就是给T添加上了各种操作符的重载函数。 t'R':+0Vf
现在所有参与行动的functor都要套上一层picker, _1被声明为 picker<holder>, 并且holder中所重载的操作符除了operator()之外全部被移到了picker内。而picker中的操作符重载的返回的functor也必须套上一个picker: t<sNc8x
3@)obb
template < typename Right > :)p)=c8%
picker < assignment < Action, typename picker_maker < Right > ::result > > operator = ( const Right & rt) const @Y
UY9+D&
{ .G}k/`a
return assignment < Action, typename picker_maker < Right > ::result > ( * this , rt); BR36}iS;V
}
)C
{h1
`
pp~3@_)b
Piker_maker返回的也是picker<T>,或者picker<constant_t<T> > / Mod=/e
使用picker还带来一个额外的好处。之前提到picker_maker要区分functor和常量,有了picker,区分的方法就非常简单了:凡是属于picker<T>的都是functor,否则就是常量。 5Lsm_"0
gF[6c`-s
template < typename T > struct picker_maker dTN$y\
{ mz\NFC<
typedef picker < constant_t < T > > result; R-pH Quu3
} ; gg-};0P-
template < typename T > struct picker_maker < picker < T > > mX&xn2}qZ"
{ h2wN<