一. 什么是Lambda Rp<Xu6r
所谓Lambda,简单的说就是快速的小函数生成。 & aLR'*]6
在C++中,STL的很多算法都要求使用者提供一个函数对象。例如for_each函数,会要求用户提供一个表明“行为”的函数对象。以vector<bool>为例,如果想使用for_each对其中的各元素全部赋值为true,一般需要这么一个函数对象, v[|iuOU
9]YmP8
n)=&=Uj`f
\ D[BRE+
class filler vB
Jva8;Q
{ QAJ>93
public : @KpzxcEoO
void operator ()( bool & i) const {i = true ;} l1:j/[B=
} ; T#BOrT>V
14&EdTG.
{0LdLRNZ
这样实现不但麻烦,而且不直观。而如果使用lambda,则允许用户使用一种直观和见解的方式来处理这个问题。以boost.lambda为例,刚才的问题可以这么解决: aH$~':[93
:qZ^<3+:
drZw#b
f*5"Jh@
for_each(v.begin(), v.end(), _1 = true ); 9BY b{<0tS
UB1/FM4~
W#wM PsB
那么下面,就让我们来实现一个lambda库。 <h}?0NA4
5[R}MhLZ
TB[vpTC9)
NWpRzh8$u
二. 战前分析 j>T''Tf
首先要说明的是,我并没有读过boost.lambda或其他任何lambda库的代码,因此如代码有雷同,纯属巧合。 i!HGM=f
开始实现以前,首先要分析出大致的实现手法。先让我们来看几段使用Lambda的代码 Lf-8G5G
# SXXYh-e
4|e#b(!
for_each(v.begin(), v.end(), _1 = 1 ); Ov|j{}=L=9
/* --------------------------------------------- */ ]@P*&FRcZ
vector < int *> vp( 10 ); DEs?xl]zO
transform(v.begin(), v.end(), vp.begin(), & _1); 4mAtYm
/* --------------------------------------------- */ %G@aZWk
Sa
sort(vp.begin(), vp.end(), * _1 > * _2); @$*c0.
|z
/* --------------------------------------------- */ a9I8WQ
int b = * find_if(v.begin, v.end(), _1 >= 3 && _1 < 5 ); meL'toaJdQ
/* --------------------------------------------- */ "+WR[-n>\
for_each(vp.begin(), vp.end(), cout << * _1 << ' \n ' ); !eq]V9
/* --------------------------------------------- */ ,J^Op
for_each(vp.begin(), vp.end(), cout << constant( ' \n ' ) << * _1); .3&m:P8zV
;H=6u
2ya`2 m
st2>e1vg
看了之后,我们可以思考一些问题: e&5K]W0{
1._1, _2是什么? (wfg84
显然_1和_2都满足C++对于标识符的要求,可见_1和_2都是对象。 p\WUk@4
2._1 = 1是在做什么? 7S`H?},sR
既然_1是一个对象,那么_1的类必然重载了operator=(int)。那么operator=返回什么呢?该函数所返回的对象被传入for_each的第3个参数,可见其返回了一个函数对象。现在整个流程就很清楚了。_1 = 1调用了operator=,其返回了一个函数对象,该函数对象能够将参数1赋值为1。 qcot
T\rq
Ok,回答了这两个问题之后,我们的思路就很清晰了。如果要实现operator=,那么至少要实现2个类,一个用于产生_1的对象,另一个用于代表operator=返回的函数对象。 a#IJ<^[8
U)!AH^{32
8if"U xV(
三. 动工 F"=MU8
首先实现一个能够范型的进行赋值的函数对象类: ,54<U~Lg:
Wg%-m%7O
GN<I|mGLJK
8zCAy@u
template < typename T > hF~B&^dd.
class assignment ]| yH8 m
{ twtDyo(\
T value; $ZU(bEUOG
public : H1[aNwLr
assignment( const T & v) : value(v) {} Vk (bU=w
template < typename T2 > agYKaM1N
T2 & operator ()(T2 & rhs) const { return rhs = value; } ,7(/Il9
} ; `O{Uz?#*x
<@A^C$g
"!tB";n
其中operator()被声明为模版函数以支持不同类型之间的赋值。 Mb>XM7}PU
然后我们就可以书写_1的类来返回assignment ="DgrH
ttnXEF
ge[i&,.&z
?5Fj]Bk]
class holder ["}A#cO652
{ Cf7\>U->
public : M\&~ Dmd
template < typename T > UjaC( c
assignment < T > operator = ( const T & t) const |DW'RopM
{ OK\%cq/U
return assignment < T > (t); A 5 X+Z
} 8j}m\^si
} ; wM)w[
h+UscdUl
|pqpF?h5|
由于该类是一个空类,因此我们可以在其后放心大胆的写上: k)p y\
`<zb
static holder _1; .F2nF8
Ok,现在一个最简单的lambda就完工了。你可以写 {nefS\#{
.6NSt
for_each(v.begin(), v.end(), _1 = 1 ); =T)2wcXBB
而不用手动写一个函数对象。 lt4jnV2"a
fn OkH
^wa9zs2s;/
<k](s
四. 问题分析 0EOX@;}
虽然基本上一个Lambda已经初步实现出来了,但是仔细想想,问题也是很多的。 q4i8Sp>
1, 我们现在是把_1和functor看成两个不同的存在,会导致代码的重复。 j6vZ{Fx;w
2, 目前这个Lambda还无法实现如_1 = 2 = 3这样的链式操作。 $:[BB,$
3, 我们没有设计好如何处理多个参数的functor。 0*?XQV@
下面我们可以对这几个问题进行分析。 >!1 f`
s8[9YfuW
五. 问题1:一致性 4C%>/*%8>
首先来看看1,合并_1和functor的最佳方法就是把_1本身也变成functor。那么_1的operator()会做什么事情呢?| ?+5{HFx
很明显,_1的operator()仅仅应该返回传进来的参数本身。 I_G>W3
iyYY)roB
struct holder A#X.c=
{ *BsDHq-F~
// C|\^uR0
template < typename T > d~jtWd|?
T & operator ()( const T & r) const aT#{t{gkA
{ Db=>7@h3C
return (T & )r; S=,1}
XZ
} $ud>Z;X=P
} ; 1gm/{w6O
O&w3@9KJ?
这样的话assignment也必须相应改动: l;*lPRoW,
1bg@[YN!;
template < typename Left, typename Right > @$d\5Q(G
class assignment AvE^
F1
{ 8(5E<&JP
Left l; `^L<db^A
Right r; I#t9aR+&
public : H?j-=Zka
assignment( const Left & l, const Right & r) : l(l), r(r) {} 9>3Ltnn0
template < typename T2 > eCIRt/ uA
T2 & operator ()(T2 & rhs) const { return l(rhs) = r; } npcBpGL{
} ; D?}m
h1#
yvWzc
uL#
同时,holder的operator=也需要改动: 0DB<hpC:5
BhW]Oq&
template < typename T > |Xm4(FN\
assignment < holder, T > operator = ( const T & t) const T[h}A"yK;
{ -\'.JA_
return assignment < holder, T > ( * this , t); ] ZGvRA&
} 0ITA3v8{
$&=;9="
好,这样holder也成为了一个functor,这为我们以后添加功能节省了很多代码。 &n]Z1e}5
你可能也注意到,常数和functor地位也不平等。 rtL9cw5
f=_?<I{
return l(rhs) = r; C.eV|rc@T
在这一句中,r没有调用operator()而l调用了。这样以后就要不时的区分常数和functor,是不良的设计。 cm@ oun
那么我们仿造holder的做法实现一个常数类: 1LE^dS^V
*OOa)P{^D
template < typename Tp > .8qzU47E
class constant_t 5Vnr"d
{ RO$@>vL
const Tp t; (
ssH=a
public : :+
9Ft>
constant_t( const Tp & t) : t(t) {} 8U2wH
template < typename T > V> a3V'
const Tp & operator ()( const T & r) const {<}I9D5
{ CDW(qq-zD
return t; ]2\2/~l
} 39T&c85
} ; ys[i`~$
|<3Q+EB^
该functor的operator()无视参数,直接返回内部所存储的常数。 K;y\[2;}e,
下面就可以修改holder的operator=了 b6!Q!:GO&
J4Z<Yt/
template < typename T > k[ffs}
assignment < holder, constant_t < T > > operator = ( const T & t) const ?Y0$X>nm
{ x|v[Dxf]
return assignment < holder, constant_t < T > > ( * this , constant_t < T > (t)); }8V;s-1
} =*:[(Py1
ccN &h
同时也要修改assignment的operator() ay:\P.`5)
m>uI\OY{n
template < typename T2 > 0^!,[oh6*
T2 & operator ()(T2 & rhs) const { return l(rhs) = r(rhs); } U.pr} hq
现在代码看起来就很一致了。 @0UwI%.
2>MP:yY;K
六. 问题2:链式操作 Eo {1y
现在让我们来看看如何处理链式操作。 XuFm4DEJ
其实问题1已经为我们处理掉了大量的问题。如果_1,functor,常量彼此之间不统一为functor,那么链式操作的时候就要时刻小心一个对象是_1还是functor还是常量,会大大增加编码的难度。 }U?gKlLg
事实上,首先要解决的是,如何知道一个functor的operator()的返回值的类型。遗憾的是,我并没有找到非常自动的办法,因此我们得让functor自己来告诉我们返回值的类型。 p21=$?k!;
比较麻烦的是,operator()的返回值一般和其参数的类型相关,而operator()通常是一个模版函数,因此其返回值类型并不能用一个简单的typedef来指定,而必须实现一个trait。
krr-ZiK
现在我们在assignment内部声明一个nested-struct mU?&\w=v$
SJ@8[n.x
template < typename T > yToT7 X7F7
struct result_1 e1`)3-f
{ ;ad9{":J#B
typedef typename ref < typename Left::result_1 < T > ::result > ::reference result; 4('0f:9z+
} ; k\Z;Cmh>
neB.Wu~WH
那么如果参数为T,其返回值类型就为result_1<T>::result。上面代码的ref<T>为一个类型转换类,作用是返回T的引用。不直接加上&符号的原因是如果T本身就是Q的引用Q&,那么Q&&是非法的。因此ref的实现即为: +2V%'{:
5gc:Y`7t
template < typename T > ]O[+c*|w
struct ref Q_dXRBv=n
{ ^i`3cCFB<
typedef T & reference; o}mhy`}
} ; e<L 9k}c
template < typename T > w~Tq|kU[
struct ref < T &> ZM-/n>
{ f
$.\o
typedef T & reference; Gh$y#0qr
} ; [L*[j.r7[
3Y1TQ;i,wQ
有了result_1之后,就可以把operator()改写一下: c<+g|@A#
r>@ B+Xi
template < typename T > c?p0#3%L#
typename result_1 < T > ::result operator ()( const T & t) const 1%SJ1oY
{ %;=IMMK
return l(t) = r(t); Imh2~rw;
} PUQ_w
可能大家已经注意到我定义assignment的operator()的返回类型的时候,是直接将其定义为Left的operator()返回类型的引用形式,如果实际上处理的对象的operator=并不是按照常理来声明的,那么这段代码可能就编译不过。这的确是一个很麻烦的事情。实际上,在gcc下,使用typeof关键字可以很容易的得到该类型的operator=的返回类型,就可以让这段代码变得更有通用性。然而为了实现可移植性,我不得不放弃这个诱人的想法。 =#.8$oa^
同理我们可以给constant_t和holder加上这个result_1。 %)<oX9E
OUlxeo/
有了这个result_1,链式操作就简单多了。现在唯一要做的事情就是让所有的functor都重载各种操作符以产生新的functor。假设我们有add和divide两个类,那么 _o &,
_1 / 3 + 5会出现的构造方式是: P;L)1 g
_1 / 3调用holder的operator/ 返回一个divide的对象 (sV]UGrZ
+5 调用divide的对象返回一个add对象。 j#LV7@H.e?
最后的布局是: D y`W5_xSz
Add vy{rwZ$
/ \ x%IXwP0
Divide 5 5A2Y'ms,/
/ \ oN&rq6eN
_1 3 o7c%\v[
似乎一切都解决了?不。 @H3 s2|
你可以想象一下一个完整的Lambda库,它必然能够重载C++几乎所有的操作符。假设其重载了10个操作符,那么至少会有10个代表这些操作符的functor类。大体上来讲,每一种操作符所对应的functor都应当能够由链式操作产生别的任意一种操作符所对应的functor。(例如:*_1 = 2既是由operator*的functor产生operator=的functor)。可想而知这样一共能产生10*10=100种产生方式。这是对编码的一个大挑战。 }{#;;5KrB
如何简化这个问题呢?我们不妨假定,任意一种操作符的functor,都能够产生任意一种操作符的functor,这样,每一种操作符的functor都拥有一样的产生方案。如果某种转换确实是不合法的(例如:A/B=C无论如何也不可能合法),那么在试图产生新functor的时候会出现编译错误。幸好C++的模版是如果不使用就不编译的,因此这种编译错误不会干扰到正常的使用,这正是我们所要的。 ONr?.MJ6j
OK,我们的方法呼之欲出了。既然所有的functor都具有一样的产生方案,那么不如大家都不要实现,等到最后统一的在所有的functor里面加上这么一系列的产生代码吧。例如,如果要添加从某functor XXX到operator=的functor的产生代码: :>tF_6
~zE 1'
template < typename Right > *c~'0|r
assignment < XXX, typename picker_maker < Right > ::result > operator = ( const 3P+4S|@q(4
Right & rt) const 3xmiX{1e
{ r%Q8)nEo
return assignment < XXX, typename picker_maker < Right > ::result > ( * this , rt); hkmTpH1<M
} r+[#%%}ea
下面对该代码的一些细节方面作一些解释 ="5k\1W1M
XXX指的是原来的functor的类型,picker_maker<T>是一个类型变换的trait,如果T是一个常量,那么他会返回constant_t<T>,否则返回T本身。 r/N[7*i
因此如果该函数声明在assignment的内部,那么就实现了连等,如果声明在的dereference(解引用)的内部,就允许(*A = B)的行为发生。 |aI|yq)
最后,如何把这些函数塞到各个functor的声明里边呢?当然可以用宏,但是。。。大家都知道这样不好。 IL+#ynC
除了宏之外还可以用的方式就是继承。我们可以写一个类叫做picker,该类实现了所有的如上的产生函数。然后让所有的functor继承自它。 4DQ07w
且慢,也许立刻就有人跳出来说:这样的话那个XXX怎么写呢?这样不是会导致循环依赖么?这样不是会有downcast么? +X* F<6mZ
正解,让picker做基类确实不是一个好主意。反过来,让picker继承functor却是一个不错的方法。下面是picker的声明: ' D)1ka.
K)Df}fVOc
template < class Action > CU#L *kz
class picker : public Action 27Kc-rcB
{ zK'
_e&*
public : Xmf
picker( const Action & act) : Action(act) {} $n=W2WJ6f
// all the operator overloaded U,%s;
} ; ++Rdv0~
M&|sR+$^
Picker<T>继承自T,唯一的作用就是给T添加上了各种操作符的重载函数。 T =eT^?v
现在所有参与行动的functor都要套上一层picker, _1被声明为 picker<holder>, 并且holder中所重载的操作符除了operator()之外全部被移到了picker内。而picker中的操作符重载的返回的functor也必须套上一个picker: ?VMi!-POE
G zJ9N`
template < typename Right > ;H7EB`
picker < assignment < Action, typename picker_maker < Right > ::result > > operator = ( const Right & rt) const q5:0&:m$4$
{ wo7N7R5
return assignment < Action, typename picker_maker < Right > ::result > ( * this , rt); 8~&F/C*
} 6pM"h5hA
W\I$`gyC/
Piker_maker返回的也是picker<T>,或者picker<constant_t<T> > W; 3
R;
使用picker还带来一个额外的好处。之前提到picker_maker要区分functor和常量,有了picker,区分的方法就非常简单了:凡是属于picker<T>的都是functor,否则就是常量。 1?D8|<
{&\J)oZ
template < typename T > struct picker_maker &K9VEMCEX
{ ".~MmF
typedef picker < constant_t < T > > result; 5z9r S<
} ; T!m42EvIvE
template < typename T > struct picker_maker < picker < T > > $\0cJCQ3
{ jHkyF`<+
typedef picker < T > result; fap|SMGt
} ; 9l]UE0yTL/
v?Z'[l
下面总的结构就有了: i>ESEmb-
functor专心模拟操作符的行为,并实现一个result_1来告诉别人自己的返回类型。 >VRo|o<D
picker专心负责操作符之间的产生关系,由它来联系操作符合functor。 g)=V#Bglv
picker<functor>构成了实际参与操作的对象。 4'+d"Ok
至此链式操作完美实现。 T4V[RN
96.IuwL*.s
SjZd0H0
七. 问题3 C$]5l;`
如何使用多参数的函数对象呢?考虑_1=_2,这个functor必须接受2个参数,因此所产生的assignment对象的operator()必须能接收2个参数。 U-Af7qO
K:}h\ In
template < typename T1, typename T2 > vqrBRlZ
??? operator ()( const T1 & t1, const T2 & t2) const M*g2VyZ
{ i:l80 GK
return lt(t1, t2) = rt(t1, t2); Mq+viU&
} :@:g*w2K
QT`fix{
很明显,这个函数的返回类型会依赖于T1,T2,因此result_1已经无法适用,我们就只好再写一个result_2: pu\b`3C(
#D!$~h&i
template < typename T1, typename T2 > ?~F]@2)5w
struct result_2 2"T8^r|U
{ 98D{{j92
typedef typename ref < typename Left::result_2 < T1, T2 > ::result > ::reference result; X?KGb{
} ; Y
h^WTysBn
2B6^]pSk
显然,各个functor似乎根本不理会各个参数那个是_1, 那个是_2, 那么最后是怎么选择的呢? EG F:xl
这个差事就留给了holder自己。 aj&\CJ
@;||peU
1k!D0f3qb
template < int Order > h=X7,2/<
class holder; 5T!&r
template <> -6uH.
class holder < 1 > 1t0bUf;(M
{ i{<8
hLO
public : ! a86iHU
template < typename T > =L:[cIRrT;
struct result_1 Ly^E& ,)
{ X32RZ9y
typedef T & result; 5\uNEs$T
} ; *}+R{
template < typename T1, typename T2 > FpP\-+Sl
struct result_2 ,)Yao;Cvd
{ 5?^]1P_
typedef T1 & result; 0w^jls
} ; I|$'Q$m~
template < typename T > WEno+Z~=1'
typename result_1 < T > ::result operator ()( const T & r) const Zkw J.SuU
{ 'g. :MQ8
return (T & )r; uEBQoP2
} Xyb8u})p'
template < typename T1, typename T2 > K3La9O)>
typename result_2 < T1, T2 > ::result operator ()( const T1 & r1, const T2 & r2) const +nU' ,E
{ |c<XSX?ir
return (T1 & )r1; CKJAZ 2
} 4#TnXxL
} ; #o"tMh!f
J09*v)L
template <> w(aUEWYL
class holder < 2 > wUbmzP.
{ wh9L(0
public : H(MB5
template < typename T > #X4LLS]VV
struct result_1 Q%rVo4M#2
{ ,xYg
typedef T & result; 2q12yY f
} ; N0]z/}hd@
template < typename T1, typename T2 > &q.)2o#Q.
struct result_2 O ,l\e3;
{ &u&2D$K,tp
typedef T2 & result; @v"T~6M
} ; H1Q''$}Z.
template < typename T > Mk<m6E$L
typename result_1 < T > ::result operator ()( const T & r) const FSv1X
{ cS4xe(n8
return (T & )r;
1U
} tzZ|S<e6=\
template < typename T1, typename T2 > 7Ez}k}aR<
typename result_2 < T1, T2 > ::result operator ()( const T1 & r1, const T2 & r2) const e "_&z#
2_
{ b S,etd
return (T2 & )r2; KvGbDG
} |n)<4%i8J
} ; <Uf|PFVj$
Ks|gL#)*Ku
-H4PRCDH
新的holder变成了holder<int>, holder<n>的n个参数的operator()会返回第n个参数的值。而_1,_2也相应变为picker<holder<1> >, picker<holder<2> >。 JW-|<CJ
现在让我们来看看(_1 = _2)(i. j)是怎么调用的: X!o@f$
首先 assignment::operator(int, int)被调用: "=FIFf
anLbl#UV
return l(i, j) = r(i, j); Q<dba12
先后调用holder<1>::operator()(int, int)和holder<2>::operator()(int, int) *JwFD^<j
av$
return ( int & )i; t`uc3ta"9
return ( int & )j; wtq,`'B
最后执行i = j; }lH;[+u3
可见,参数被正确的选择了。 c$/<l5Uw
{JTmP `&l
>)4.$#H
hUBF/4s\
_'&k#Q
八. 中期总结 2,+d|1(4o
目前的结果是这样的,为了支持一个操作符,我们需要作如下几件事: 70{RDj6{
1。 实现一个functor,该functor的operator()要能执行该操作符的语义 @#A!w;bz
2。 在该functor中实现result_1至result_n,其中n是支持参数的最大值。 T=.-Cl1A
3。 在picker中实现一个操作符重载,返回该functor g2A"1w<-AH
m.!wsw
jBS'g{y-!
Ny]lvgu9X
r-*l1([eW
%S c=_%6
九. 简化 u9BjgK(M
很明显,要支持一个操作符所要做的工作太多了,而且在每个functor中申明result_1至result_n,可见如果n发生变化,维护的开销极大。 f0OgK<.>T
我们现在需要找到一个自动生成这种functor的方法。 'w:bs!
首先,我们注意到result_x的形式很统一。对于各种操作符,其返回值无非下列几种: CNq[4T'~A
1. 返回值。如果本身为引用,就去掉引用。 S3QaYq"v
+-*/&|^等 1}`2\3,
2. 返回引用。 rJX\6{V!_
=,各种复合赋值等 !F-sA: xq
3. 返回固定类型。 _;#9!"&
各种逻辑/比较操作符(返回bool) Gj)uyjct
4. 原样返回。 *]>])ms)
operator, 9+t=|
5. 返回解引用的类型。
K,6OGsh
operator*(单目) C]M7GHe1q
6. 返回地址。 &"xQ~05
operator&(单目)
o7J{+V
7. 下表访问返回类型。 E_]k>bf\
operator[] Xh`"
8. 如果左操作数是一个stream,返回引用,否则返回值 loLKm]yV
operator<<和operator>> }Iip+URG
,2,W^HJ
OK,这样我们将返回值类型总结为以上8种,就可以将各种result_x从functor中剥离出来了。 j|k@MfA
例如针对第一条,我们实现一个policy类: f'i6QMk\&
v O PMgEI
template < typename Left > !n:uiwh
struct value_return ]b> pI;
{ (ZS/@He
template < typename T > wz h.$?~
struct result_1 - {0g#G
{ 4Mi~1iZj
typedef typename const_value < typename Left::template result_1 < T > ::result_type > ::value_type result_type; !M,h79NM
} ; qZ&