一. 什么是Lambda )k{zRq:d
所谓Lambda,简单的说就是快速的小函数生成。 1
@tVfn}
在C++中,STL的很多算法都要求使用者提供一个函数对象。例如for_each函数,会要求用户提供一个表明“行为”的函数对象。以vector<bool>为例,如果想使用for_each对其中的各元素全部赋值为true,一般需要这么一个函数对象, lt[{u$
J[du>1D
Ns?y)
G>:
H"6Sj-<=
class filler aovRm|aOo'
{ }>>lgW>n,;
public : P'xq+Q
void operator ()( bool & i) const {i = true ;} ojni+} >_
} ; 9;NR
*^ g7kCe(
vE^Hk!^
这样实现不但麻烦,而且不直观。而如果使用lambda,则允许用户使用一种直观和见解的方式来处理这个问题。以boost.lambda为例,刚才的问题可以这么解决: L]I)E`s
5v<BB`XWp
_0<qS{RW
XOAZ
for_each(v.begin(), v.end(), _1 = true ); .A//Q|ot!
<: f jWy
dnSjXyjFB
那么下面,就让我们来实现一个lambda库。 Ni7~
Mjjt
9K-=2hvv
;<OIu&,*
3~iIo&NZ
二. 战前分析 <p;cR` %uE
首先要说明的是,我并没有读过boost.lambda或其他任何lambda库的代码,因此如代码有雷同,纯属巧合。 [/.o>R#J(
开始实现以前,首先要分析出大致的实现手法。先让我们来看几段使用Lambda的代码 9X/c%:)\=
uW},I6g
Y1vl,Yi
for_each(v.begin(), v.end(), _1 = 1 ); 9l5l"Wj&
/* --------------------------------------------- */ ^(r?k_i/
vector < int *> vp( 10 ); L&H4fy!>
transform(v.begin(), v.end(), vp.begin(), & _1); |f#~#Y2v
/* --------------------------------------------- */ CXwDG_e
sort(vp.begin(), vp.end(), * _1 > * _2); *W~+Nho.A
/* --------------------------------------------- */ ]#z^[XG
int b = * find_if(v.begin, v.end(), _1 >= 3 && _1 < 5 ); epqX2`!V
/* --------------------------------------------- */
s>~ h<B
for_each(vp.begin(), vp.end(), cout << * _1 << ' \n ' ); +}@1X&v:
/* --------------------------------------------- */ b`)^Ao:
for_each(vp.begin(), vp.end(), cout << constant( ' \n ' ) << * _1); +ffs{g{
%}t.+z(S
dcew`$SJp
-$yNJ5F`
看了之后,我们可以思考一些问题: { AdPC?R`
1._1, _2是什么? gpB3\
显然_1和_2都满足C++对于标识符的要求,可见_1和_2都是对象。 Q&S\?cKe
2._1 = 1是在做什么? $yS7u
既然_1是一个对象,那么_1的类必然重载了operator=(int)。那么operator=返回什么呢?该函数所返回的对象被传入for_each的第3个参数,可见其返回了一个函数对象。现在整个流程就很清楚了。_1 = 1调用了operator=,其返回了一个函数对象,该函数对象能够将参数1赋值为1。 Rs_bM@
Ok,回答了这两个问题之后,我们的思路就很清晰了。如果要实现operator=,那么至少要实现2个类,一个用于产生_1的对象,另一个用于代表operator=返回的函数对象。 `VM@-;@w
!)FM/Xj,o
q{?Po;\D
三. 动工 }@>=,A4Y
首先实现一个能够范型的进行赋值的函数对象类: W7r1!/ccj
K6d9[;F
<1cYz\/!M
*J&XM[t
template < typename T > LT']3w
class assignment l(
/yaZ`
{ 1$vsw
T value; dP}=cZ~
public : KAH9?zI)M
assignment( const T & v) : value(v) {} 2A'!kd$2
template < typename T2 > U`Bw2Vdk]S
T2 & operator ()(T2 & rhs) const { return rhs = value; } Uv?s <
} ; Q$r1beA
Vw0cf;
u?6L.^Op
其中operator()被声明为模版函数以支持不同类型之间的赋值。 gx~79;6
然后我们就可以书写_1的类来返回assignment /ZlPEs)
hDTiXc
:d\ne
1D159 NLB
class holder 3}V`]B#a
{ X;25G
public : 4
qMO@E_
template < typename T > IMjz#|c
assignment < T > operator = ( const T & t) const uSh!A
{ %5.aC|^}
return assignment < T > (t); huVw+vAA
} .4P5tIn\
} ; DdJ>1504
Wm! lWQu7
ocOzQ13@Y
由于该类是一个空类,因此我们可以在其后放心大胆的写上: }+ ";W) R
/cM<
static holder _1; S?_/Po|
Ok,现在一个最简单的lambda就完工了。你可以写 *[K\_F?^h
Ct2m l
for_each(v.begin(), v.end(), _1 = 1 ); IO3`/R-
而不用手动写一个函数对象。 ?\[2Po]n
#'m&<g,
} m5AO 4:
v%N/mL+5L
四. 问题分析 aD)XxXwozm
虽然基本上一个Lambda已经初步实现出来了,但是仔细想想,问题也是很多的。 lYEMrr!KQw
1, 我们现在是把_1和functor看成两个不同的存在,会导致代码的重复。 M| r6"~i
2, 目前这个Lambda还无法实现如_1 = 2 = 3这样的链式操作。 el
GP2x#:
3, 我们没有设计好如何处理多个参数的functor。 g_ 'F(An
下面我们可以对这几个问题进行分析。 r,F~Vwa}
"BSSA%u?c
五. 问题1:一致性 i
Lr*W#E
首先来看看1,合并_1和functor的最佳方法就是把_1本身也变成functor。那么_1的operator()会做什么事情呢?| WrWJ!
很明显,_1的operator()仅仅应该返回传进来的参数本身。 ZuF"GNUC
g%z'#E97
struct holder }@Rq'VPZd
{ n/*BK;
// ,9jq
@_
template < typename T > sDNV_}
h
T & operator ()( const T & r) const *j9{+yO{ZE
{ FgA'X<
return (T & )r; )c~1s
} <k'JhMwN
} ; RW19I,d
`
O;+N"v
这样的话assignment也必须相应改动: ?S&pq?
F-K=Otj
template < typename Left, typename Right > F~j
U; L
class assignment my+y<C-o`
{ }2dz];bR
Left l; {c5%.<O
Right r; bMWL^ *I
public : Gd^K,3:. T
assignment( const Left & l, const Right & r) : l(l), r(r) {} LvP{"K;
template < typename T2 > |KSd@
T2 & operator ()(T2 & rhs) const { return l(rhs) = r; } Fh t$7V
} ; Z#H] yG
q:2V w`g'
同时,holder的operator=也需要改动: $r0~&$T&
x\HHu]
template < typename T > t\YN\`XD
assignment < holder, T > operator = ( const T & t) const d:KUJ
Y.
{ .1F(-mLd
return assignment < holder, T > ( * this , t); xRum q
} $gKMVgD"
0sxZa+G0o
好,这样holder也成为了一个functor,这为我们以后添加功能节省了很多代码。 Om
#m":
你可能也注意到,常数和functor地位也不平等。 5:[<pY!s#
^@W98_bd;
return l(rhs) = r; *5KV DOd
在这一句中,r没有调用operator()而l调用了。这样以后就要不时的区分常数和functor,是不良的设计。 } Ej^M~Vv
那么我们仿造holder的做法实现一个常数类: 00s&<EM
)na8a!
template < typename Tp > ^H]q[XFR
class constant_t )C>4?)
{ ^(,qkq'u
D
const Tp t; `<R;^qCt
public : p4},xQzB
constant_t( const Tp & t) : t(t) {} eK]g FXk
template < typename T > M#v#3:&5
const Tp & operator ()( const T & r) const gcLwQ-
{ MD ETAd
return t; \)H}
} NpS*]vSO
} ; V?KACYd@O
t{)Z$)'
该functor的operator()无视参数,直接返回内部所存储的常数。 c;\}R#
下面就可以修改holder的operator=了 ,PG d
HEZgHL
template < typename T > 'n'83d)z
assignment < holder, constant_t < T > > operator = ( const T & t) const LR :Qb]|"
{ :^
9sy
return assignment < holder, constant_t < T > > ( * this , constant_t < T > (t)); &{#4^.Q
} bcgh}D
OC)~psQK
同时也要修改assignment的operator() [Yt!uhww
PbR6>'
template < typename T2 > _Ju@<V$
T2 & operator ()(T2 & rhs) const { return l(rhs) = r(rhs); } z'cK,psq(
现在代码看起来就很一致了。 @S#>:o|
}jj@A !N
六. 问题2:链式操作 S@Rw+#QE
现在让我们来看看如何处理链式操作。 -w8c;5X
其实问题1已经为我们处理掉了大量的问题。如果_1,functor,常量彼此之间不统一为functor,那么链式操作的时候就要时刻小心一个对象是_1还是functor还是常量,会大大增加编码的难度。 8Lm}x_
事实上,首先要解决的是,如何知道一个functor的operator()的返回值的类型。遗憾的是,我并没有找到非常自动的办法,因此我们得让functor自己来告诉我们返回值的类型。 8
1Ar.<
比较麻烦的是,operator()的返回值一般和其参数的类型相关,而operator()通常是一个模版函数,因此其返回值类型并不能用一个简单的typedef来指定,而必须实现一个trait。 AGwFD
现在我们在assignment内部声明一个nested-struct /SLAg&
e_Cns&
template < typename T > HS1Gy/6'
struct result_1 ;Od;q]G7L
{ a3o4> 9
typedef typename ref < typename Left::result_1 < T > ::result > ::reference result; hg8gB8Xq
} ; t\[aU\4-7
uXx c2}
那么如果参数为T,其返回值类型就为result_1<T>::result。上面代码的ref<T>为一个类型转换类,作用是返回T的引用。不直接加上&符号的原因是如果T本身就是Q的引用Q&,那么Q&&是非法的。因此ref的实现即为: ^G5BD_
}lN@J,q
template < typename T > 5k&tRg
struct ref +APf[ZpU
{ "2cJ'n/L
typedef T & reference; d'1L#`?
} ; uFd.2,XNP
template < typename T > 5)=XzO0
struct ref < T &> Z4eu'.r-y~
{ [/.5{|&GSt
typedef T & reference; iUcDj:
} ; eBZ^YY<*g
hdFIriE3
有了result_1之后,就可以把operator()改写一下: L2v
j)(
-#yLH
template < typename T > eK
}AVz}k
typename result_1 < T > ::result operator ()( const T & t) const & <{=
{ YuO-a$BP
return l(t) = r(t); JXR_klx
} g.CUo:c
可能大家已经注意到我定义assignment的operator()的返回类型的时候,是直接将其定义为Left的operator()返回类型的引用形式,如果实际上处理的对象的operator=并不是按照常理来声明的,那么这段代码可能就编译不过。这的确是一个很麻烦的事情。实际上,在gcc下,使用typeof关键字可以很容易的得到该类型的operator=的返回类型,就可以让这段代码变得更有通用性。然而为了实现可移植性,我不得不放弃这个诱人的想法。 4AI\'M"d
同理我们可以给constant_t和holder加上这个result_1。 n}8J-/(|+
m@K5eh
有了这个result_1,链式操作就简单多了。现在唯一要做的事情就是让所有的functor都重载各种操作符以产生新的functor。假设我们有add和divide两个类,那么 y@&Cn
_1 / 3 + 5会出现的构造方式是: rh;@|/<l
_1 / 3调用holder的operator/ 返回一个divide的对象 u&Ze$z
+5 调用divide的对象返回一个add对象。 !ueyVE$1
最后的布局是: cO$
PK
Add wKe$(>d"L
/ \ M[wd.\
%
Divide 5 Q}G'=Q]Juz
/ \ aL63=y
_1 3 MMs#Y1dH
似乎一切都解决了?不。 3q*y~5&I
你可以想象一下一个完整的Lambda库,它必然能够重载C++几乎所有的操作符。假设其重载了10个操作符,那么至少会有10个代表这些操作符的functor类。大体上来讲,每一种操作符所对应的functor都应当能够由链式操作产生别的任意一种操作符所对应的functor。(例如:*_1 = 2既是由operator*的functor产生operator=的functor)。可想而知这样一共能产生10*10=100种产生方式。这是对编码的一个大挑战。 I`%\ "bF@
如何简化这个问题呢?我们不妨假定,任意一种操作符的functor,都能够产生任意一种操作符的functor,这样,每一种操作符的functor都拥有一样的产生方案。如果某种转换确实是不合法的(例如:A/B=C无论如何也不可能合法),那么在试图产生新functor的时候会出现编译错误。幸好C++的模版是如果不使用就不编译的,因此这种编译错误不会干扰到正常的使用,这正是我们所要的。 A aLj.HR
OK,我们的方法呼之欲出了。既然所有的functor都具有一样的产生方案,那么不如大家都不要实现,等到最后统一的在所有的functor里面加上这么一系列的产生代码吧。例如,如果要添加从某functor XXX到operator=的functor的产生代码: "^A4 !.
fJ!i%</V
template < typename Right > d8 1u
assignment < XXX, typename picker_maker < Right > ::result > operator = ( const f<.43kv@
Right & rt) const d
]LF5*i
{ 5B+>28G%
return assignment < XXX, typename picker_maker < Right > ::result > ( * this , rt); >Le L%$
} _c}@Fi+E
下面对该代码的一些细节方面作一些解释 R-Y |;
XXX指的是原来的functor的类型,picker_maker<T>是一个类型变换的trait,如果T是一个常量,那么他会返回constant_t<T>,否则返回T本身。 *&VH!K#@{
因此如果该函数声明在assignment的内部,那么就实现了连等,如果声明在的dereference(解引用)的内部,就允许(*A = B)的行为发生。 u(ep$>[F#_
最后,如何把这些函数塞到各个functor的声明里边呢?当然可以用宏,但是。。。大家都知道这样不好。 ]lj,GD)c
除了宏之外还可以用的方式就是继承。我们可以写一个类叫做picker,该类实现了所有的如上的产生函数。然后让所有的functor继承自它。 -eKi}e
且慢,也许立刻就有人跳出来说:这样的话那个XXX怎么写呢?这样不是会导致循环依赖么?这样不是会有downcast么? YmP`Gg#>p
正解,让picker做基类确实不是一个好主意。反过来,让picker继承functor却是一个不错的方法。下面是picker的声明: E|u#W3-:
PCl@Ff
template < class Action > Vmj7`w&
class picker : public Action %j],6wW5J
{ L%,tc~)A
public : $+` YP
picker( const Action & act) : Action(act) {} RhM]OJd'
// all the operator overloaded !mFx= +
} ; imcq
H
cU\Er{
k
Picker<T>继承自T,唯一的作用就是给T添加上了各种操作符的重载函数。 <{rRcFR
现在所有参与行动的functor都要套上一层picker, _1被声明为 picker<holder>, 并且holder中所重载的操作符除了operator()之外全部被移到了picker内。而picker中的操作符重载的返回的functor也必须套上一个picker:
t#s?:
Y,O)"6ev
template < typename Right > R:+2}kS5e{
picker < assignment < Action, typename picker_maker < Right > ::result > > operator = ( const Right & rt) const ]w!gv
/;
{ ,fS}cpV
return assignment < Action, typename picker_maker < Right > ::result > ( * this , rt); @WIcH:_w-
} {3=\x
KjR^6v
Piker_maker返回的也是picker<T>,或者picker<constant_t<T> > w*.q t<rH)
使用picker还带来一个额外的好处。之前提到picker_maker要区分functor和常量,有了picker,区分的方法就非常简单了:凡是属于picker<T>的都是functor,否则就是常量。 Yk',a$.S
]"SH
pq
template < typename T > struct picker_maker E\N?D
{ %mR roR6
typedef picker < constant_t < T > > result; (P;z*
"q
} ; =ogzq.+|
template < typename T > struct picker_maker < picker < T > > .k5
TQt
{ }V.Wp6"S
typedef picker < T > result; A|sTnhp~
} ; Jd_w:H.
d4c-(ZRl
下面总的结构就有了: /s.O3x._'
functor专心模拟操作符的行为,并实现一个result_1来告诉别人自己的返回类型。 $x&@!/&|pv
picker专心负责操作符之间的产生关系,由它来联系操作符合functor。 y7#$:+jQv
picker<functor>构成了实际参与操作的对象。 tqLn A
至此链式操作完美实现。 J{$+\
+RexQE
x2B~1edf
七. 问题3
Sbub|
如何使用多参数的函数对象呢?考虑_1=_2,这个functor必须接受2个参数,因此所产生的assignment对象的operator()必须能接收2个参数。 #W#GI"K
;Ab`b1B
template < typename T1, typename T2 > *ayn<Vlh`^
??? operator ()( const T1 & t1, const T2 & t2) const mQt';|X@
{ %1ofu,%
return lt(t1, t2) = rt(t1, t2); h4CDZ
} r(` ;CY]@
(p<QRb:&Z
很明显,这个函数的返回类型会依赖于T1,T2,因此result_1已经无法适用,我们就只好再写一个result_2: '| Enc"U
<VD^f
template < typename T1, typename T2 > ?qr-t+
struct result_2 XWvT(+J
{ 9tmYrhb$
typedef typename ref < typename Left::result_2 < T1, T2 > ::result > ::reference result; <b!ieK?\F3
} ; MCHRNhb9
q0Fq7rWP
显然,各个functor似乎根本不理会各个参数那个是_1, 那个是_2, 那么最后是怎么选择的呢? Ojj:YLlY>
这个差事就留给了holder自己。 ?vL\VI9
=G9%Hz5~:
a~YFJAkg9
template < int Order > L-_dq0T
class holder; 0;z-I"N
template <> yoTbIQ
class holder < 1 > ?29zcuRaru
{ @xR7>-$0p
public : )e.Y"5My
template < typename T > v)@EK6Nty
struct result_1 frS1<+
{ <VV./W8e9
typedef T & result; xq_%|p}y
} ; hNB;29r~
template < typename T1, typename T2 > .$b]rx7$~
struct result_2 %zE_Q
{ lcgT9m#
typedef T1 & result; 96;17h$
} ; xQ4D| &
template < typename T > g|*2O}<
typename result_1 < T > ::result operator ()( const T & r) const QjETu
{ iMRb`
\KH
return (T & )r; K1>.%m
} %]%.{W\j3
template < typename T1, typename T2 > \&\_[y8U
typename result_2 < T1, T2 > ::result operator ()( const T1 & r1, const T2 & r2) const BQVpp,]
{ Mw!?2G[|
return (T1 & )r1; [ P\3XSR
} EqzS={Olj
} ; "pq#A*
A+%oE
template <> ul~>eZ
class holder < 2 > ?&Si P-G
{ L%`~`3%n-
public : xP3_
template < typename T > r,=xI`XH
struct result_1 !cnun Lc`
{ *leQd^47
typedef T & result; E>Ukxi1
} ; u`Djle
template < typename T1, typename T2 > jLC,<V*
struct result_2 6N(Wv0b $
{ %Qc#v$;+J
typedef T2 & result; f XxdOn.
} ; "m +Eu|{
template < typename T > uy\<t
typename result_1 < T > ::result operator ()( const T & r) const vC~];!^
{ B&A4-w v
return (T & )r; LNml["
} P8!Vcy938
template < typename T1, typename T2 > S!8eY `C.
typename result_2 < T1, T2 > ::result operator ()( const T1 & r1, const T2 & r2) const 9)l-5o:D
{ N;tUrdgQ
return (T2 & )r2; dA>t
} PCnE-$QH
} ; #C,M8~Q7
*A2J[,?c
6gwjrGje\
新的holder变成了holder<int>, holder<n>的n个参数的operator()会返回第n个参数的值。而_1,_2也相应变为picker<holder<1> >, picker<holder<2> >。 a`(6hL3IT
现在让我们来看看(_1 = _2)(i. j)是怎么调用的: 6XUcJ0
首先 assignment::operator(int, int)被调用: bs
U$mtW
o%h"gbvMY!
return l(i, j) = r(i, j); ,GXwi|Y
先后调用holder<1>::operator()(int, int)和holder<2>::operator()(int, int) &H,5f#
qa#Fa)g*
return ( int & )i; 6FG h=~{3,
return ( int & )j; t
),~w,7(J
最后执行i = j; Cl[ '6Lk
可见,参数被正确的选择了。 o!L1Qrh
`;WiTE)&)
Z `O.JE
~R-S$qizAC
Yo@>O98
八. 中期总结 1B=vrGq
目前的结果是这样的,为了支持一个操作符,我们需要作如下几件事: Da1BxbDeI
1。 实现一个functor,该functor的operator()要能执行该操作符的语义 =[(1u|H9
2。 在该functor中实现result_1至result_n,其中n是支持参数的最大值。 X;flA*6V
3。 在picker中实现一个操作符重载,返回该functor /pgfa-<
W%b<(T;
%1SA!1>j
aq~hl7MTj
W?~G_4
q,VJpqQ
九. 简化 3 1KMn
很明显,要支持一个操作符所要做的工作太多了,而且在每个functor中申明result_1至result_n,可见如果n发生变化,维护的开销极大。 LtbL[z>]
我们现在需要找到一个自动生成这种functor的方法。 EHkb{Q8
首先,我们注意到result_x的形式很统一。对于各种操作符,其返回值无非下列几种: k:s}`h_n
1. 返回值。如果本身为引用,就去掉引用。 k(<5tv d
+-*/&|^等 _CAWD;P
2. 返回引用。 tY !fO>Fn~
=,各种复合赋值等 ~1wAk0G`n
3. 返回固定类型。 xB3;%Lc
各种逻辑/比较操作符(返回bool) >8Zz<S&z
4. 原样返回。 67%eAS
operator, Z"'rc.>a
5. 返回解引用的类型。 [VIdw92
operator*(单目) <