一. 什么是Lambda /a%KS3>V*
所谓Lambda,简单的说就是快速的小函数生成。 cX"G7Bh
在C++中,STL的很多算法都要求使用者提供一个函数对象。例如for_each函数,会要求用户提供一个表明“行为”的函数对象。以vector<bool>为例,如果想使用for_each对其中的各元素全部赋值为true,一般需要这么一个函数对象, vUfO4yfdg
5xv,!/@
Fs9W>*(
#,Bj!'Q'-
class filler q5gP~*?
{ MVuP
|&:n
public : 7X:hIl
void operator ()( bool & i) const {i = true ;} ypT9 8
} ; &O{t^D)F
d:3= 1x
h~.V[o7=
这样实现不但麻烦,而且不直观。而如果使用lambda,则允许用户使用一种直观和见解的方式来处理这个问题。以boost.lambda为例,刚才的问题可以这么解决: #[(0tc/
7?]!Ecr"
z{V8@q/
T;%+ ]:w<
for_each(v.begin(), v.end(), _1 = true ); %rFllb7
?7 X3P
u
dUXc6U
那么下面,就让我们来实现一个lambda库。 ;l#?SYY
U*xxrt/On/
,"C&v~
:9O|l)N)W=
二. 战前分析 `0[fLEm
首先要说明的是,我并没有读过boost.lambda或其他任何lambda库的代码,因此如代码有雷同,纯属巧合。 tQ6| PV
开始实现以前,首先要分析出大致的实现手法。先让我们来看几段使用Lambda的代码 tQCj)Ms 'X
Z0z)
xF^r`
for_each(v.begin(), v.end(), _1 = 1 ); wISzT^RS
/* --------------------------------------------- */ }(rzH}X@
vector < int *> vp( 10 ); *q[^Q'jnN
transform(v.begin(), v.end(), vp.begin(), & _1); Y/!0Q6<[2Y
/* --------------------------------------------- */ tdb4?^.s
sort(vp.begin(), vp.end(), * _1 > * _2); fIlIH
/* --------------------------------------------- */ `v<f}
int b = * find_if(v.begin, v.end(), _1 >= 3 && _1 < 5 ); 3V!W@[ }:
/* --------------------------------------------- */ } _VZ
for_each(vp.begin(), vp.end(), cout << * _1 << ' \n ' ); {8W |W2o$!
/* --------------------------------------------- */ J
b|mXNcL
for_each(vp.begin(), vp.end(), cout << constant( ' \n ' ) << * _1); n_ OUWvs
` C ?a
34]%d<;A
_]Z$YM
看了之后,我们可以思考一些问题: 1(D1}fcul
1._1, _2是什么? i|[S5QXCh
显然_1和_2都满足C++对于标识符的要求,可见_1和_2都是对象。 fV v$K&
2._1 = 1是在做什么? 6.vNe
既然_1是一个对象,那么_1的类必然重载了operator=(int)。那么operator=返回什么呢?该函数所返回的对象被传入for_each的第3个参数,可见其返回了一个函数对象。现在整个流程就很清楚了。_1 = 1调用了operator=,其返回了一个函数对象,该函数对象能够将参数1赋值为1。 ?~]>H A:
Ok,回答了这两个问题之后,我们的思路就很清晰了。如果要实现operator=,那么至少要实现2个类,一个用于产生_1的对象,另一个用于代表operator=返回的函数对象。 }"g@E-]N
dfXV1B5
q
w"e0q% )
三. 动工 G+;g:_E=
首先实现一个能够范型的进行赋值的函数对象类: 2%*|fF}I
Dj/Q1KY$m
)8\Z=uC
Vc{/o=1u
template < typename T > Wa@6VY
class assignment o^N%;d1%E
{ 8g&uE*7N
T value; ~V|KT}H
public : 1.xw'i
assignment( const T & v) : value(v) {} ~91uk3ST?
template < typename T2 > wP+'04H0
T2 & operator ()(T2 & rhs) const { return rhs = value; } 8HB?=a2Q<'
} ; >E{#HPpBi
"F04c|oR<X
FUH*]U
其中operator()被声明为模版函数以支持不同类型之间的赋值。 z, :+Oc
然后我们就可以书写_1的类来返回assignment $d5&~I
]q@rGD85K
QZ_nQ3K
Ynv 9v\n|
class holder ,[+ZjAyG}#
{ 9?v)
public : \q|e8k4p
template < typename T > p3i
qW,[@
assignment < T > operator = ( const T & t) const >V3W>5 X
{ 6eVe}V4W
return assignment < T > (t); 3Ro7M=]
} BZ8h*|uT"
} ; =#J9
Q2??Kp]1
8j({=xbg&
由于该类是一个空类,因此我们可以在其后放心大胆的写上: ?yda.<"g9Y
,|=iv
static holder _1; D}3cW2!9
Ok,现在一个最简单的lambda就完工了。你可以写 wpJ^}+kF
]g>T9,)l
for_each(v.begin(), v.end(), _1 = 1 ); qM+!f2t
而不用手动写一个函数对象。 bi,rMgW
c'>8pd
c1=;W$T(s
a .B\=3xn
四. 问题分析 PLlx~A
虽然基本上一个Lambda已经初步实现出来了,但是仔细想想,问题也是很多的。 zhD`\&G.
1, 我们现在是把_1和functor看成两个不同的存在,会导致代码的重复。 6oe$)iV
2, 目前这个Lambda还无法实现如_1 = 2 = 3这样的链式操作。 ~W5>;6f\
3, 我们没有设计好如何处理多个参数的functor。 DRS;lJ2
下面我们可以对这几个问题进行分析。 KHiYV
L8%=k%H(1
五. 问题1:一致性 &ij^FAM
首先来看看1,合并_1和functor的最佳方法就是把_1本身也变成functor。那么_1的operator()会做什么事情呢?| h=mI{w*
很明显,_1的operator()仅仅应该返回传进来的参数本身。 GZ-n!
^
aa'0EU:
struct holder (*c`<|)
{ 5[0l08'D
// \ltE rd-
template < typename T > !'c6 Hs
T & operator ()( const T & r) const %t(, *;
{ H znI R
return (T & )r; INeWi= 1
} %u<&^8EL+#
} ; AX^3uRQJ
U{.+*e18
这样的话assignment也必须相应改动: '{1W)X
;FIMCJS
template < typename Left, typename Right > yBD.Cs@
class assignment 8O}A/*1FJ
{ &)/H?S;yN
Left l; j/; @P
Right r; 5Od(J5`
public : Qg86XU%l
assignment( const Left & l, const Right & r) : l(l), r(r) {} ;Ln7_
template < typename T2 > ph5xW<VNP
T2 & operator ()(T2 & rhs) const { return l(rhs) = r; } "J0Oa?
} ; l)2HHu<
kKI!B`j=
同时,holder的operator=也需要改动: ^y;OHo
9X*eE
template < typename T > P"[l86:
assignment < holder, T > operator = ( const T & t) const ) J:'5hz
{ /(z0I.yE
return assignment < holder, T > ( * this , t); EUYa =-
} p'9
V._h
aL$c).hq0
好,这样holder也成为了一个functor,这为我们以后添加功能节省了很多代码。 UC<[z#]\;
你可能也注意到,常数和functor地位也不平等。 {{#a%O
LU 5
`!0m
return l(rhs) = r; tkUW)ScJ
在这一句中,r没有调用operator()而l调用了。这样以后就要不时的区分常数和functor,是不良的设计。 y}H*p
那么我们仿造holder的做法实现一个常数类: d5Eee^Qu/
2*UE&Gp
template < typename Tp > 9-e[S3ziM
class constant_t (J?}eb;>n
{ IQIb\OUo!v
const Tp t; hr
6LB&d_
public : _|Kv~\G!
constant_t( const Tp & t) : t(t) {} vVvt
]h
template < typename T > .w*{=x0k
const Tp & operator ()( const T & r) const 3:CQMZ|;@
{ f T+n-B
return t; Wy0a2Ve
} McMK|_H
} ; iTtAj~dfZ
SS<+fWXE
该functor的operator()无视参数,直接返回内部所存储的常数。 v"?PhO/{=
下面就可以修改holder的operator=了 \c@qtIc
%<#$:Qb.
template < typename T > sD8xH
assignment < holder, constant_t < T > > operator = ( const T & t) const -EX3'
[*'
{ N_WA4?rB
return assignment < holder, constant_t < T > > ( * this , constant_t < T > (t)); \]d*h]Hms
} 8b#Yd
<LA`PbQa
同时也要修改assignment的operator() ~Hs]} Xo
h0EGhJs
template < typename T2 > m6ZbYF-7W
T2 & operator ()(T2 & rhs) const { return l(rhs) = r(rhs); } IUBps0.T\
现在代码看起来就很一致了。 r~BQy'
a[{QlD^D
六. 问题2:链式操作 ?p/kuv{\o#
现在让我们来看看如何处理链式操作。 |@n{tog+-
其实问题1已经为我们处理掉了大量的问题。如果_1,functor,常量彼此之间不统一为functor,那么链式操作的时候就要时刻小心一个对象是_1还是functor还是常量,会大大增加编码的难度。 [HZCnO|N
事实上,首先要解决的是,如何知道一个functor的operator()的返回值的类型。遗憾的是,我并没有找到非常自动的办法,因此我们得让functor自己来告诉我们返回值的类型。 ch2e#Jf8
比较麻烦的是,operator()的返回值一般和其参数的类型相关,而operator()通常是一个模版函数,因此其返回值类型并不能用一个简单的typedef来指定,而必须实现一个trait。 DF&jZ[##
现在我们在assignment内部声明一个nested-struct dXcMysRc%&
3B_} :
template < typename T > )9sr,3w
struct result_1 2|_Jup
{ K+TTYQ
typedef typename ref < typename Left::result_1 < T > ::result > ::reference result; JNz"lTt>[g
} ; eG)/&zQ8
ez<wEtS
那么如果参数为T,其返回值类型就为result_1<T>::result。上面代码的ref<T>为一个类型转换类,作用是返回T的引用。不直接加上&符号的原因是如果T本身就是Q的引用Q&,那么Q&&是非法的。因此ref的实现即为: cB"F1~z
Exo`Z`m`U
template < typename T > =[-- Hf
struct ref 7g<`wLAH
{ DEeL48{R
typedef T & reference; xo"4mbTV
} ; 5Vm}<8{
template < typename T > nN]vu
struct ref < T &> !A<XqzV]
{ ?lw[
typedef T & reference; @p'v.;~#
} ; 5FR#_}k]_F
y&I|m
有了result_1之后,就可以把operator()改写一下: X52jqXjg
nQYS{`hk
template < typename T > BU?MRcHC
typename result_1 < T > ::result operator ()( const T & t) const U;A5-|C
{ 7 V1k$S(
return l(t) = r(t); Vv"wf;#
} $.]t1e7s
可能大家已经注意到我定义assignment的operator()的返回类型的时候,是直接将其定义为Left的operator()返回类型的引用形式,如果实际上处理的对象的operator=并不是按照常理来声明的,那么这段代码可能就编译不过。这的确是一个很麻烦的事情。实际上,在gcc下,使用typeof关键字可以很容易的得到该类型的operator=的返回类型,就可以让这段代码变得更有通用性。然而为了实现可移植性,我不得不放弃这个诱人的想法。 ,,j=RG_
同理我们可以给constant_t和holder加上这个result_1。 )A+j
*9:6t6x
有了这个result_1,链式操作就简单多了。现在唯一要做的事情就是让所有的functor都重载各种操作符以产生新的functor。假设我们有add和divide两个类,那么 vi.AzO
_1 / 3 + 5会出现的构造方式是: gkn/E}K#
_1 / 3调用holder的operator/ 返回一个divide的对象 bb_jD^
+5 调用divide的对象返回一个add对象。 L$kAe1 V^m
最后的布局是: <!nWiwv
Add ->25$5#
/ \ >^ 1S26
Divide 5 $5AtI$TV_!
/ \ ifCGNvDR
_1 3 <T% hfW
似乎一切都解决了?不。 <`p'6n79
你可以想象一下一个完整的Lambda库,它必然能够重载C++几乎所有的操作符。假设其重载了10个操作符,那么至少会有10个代表这些操作符的functor类。大体上来讲,每一种操作符所对应的functor都应当能够由链式操作产生别的任意一种操作符所对应的functor。(例如:*_1 = 2既是由operator*的functor产生operator=的functor)。可想而知这样一共能产生10*10=100种产生方式。这是对编码的一个大挑战。 7[<sl35
如何简化这个问题呢?我们不妨假定,任意一种操作符的functor,都能够产生任意一种操作符的functor,这样,每一种操作符的functor都拥有一样的产生方案。如果某种转换确实是不合法的(例如:A/B=C无论如何也不可能合法),那么在试图产生新functor的时候会出现编译错误。幸好C++的模版是如果不使用就不编译的,因此这种编译错误不会干扰到正常的使用,这正是我们所要的。 &,kB7r"
OK,我们的方法呼之欲出了。既然所有的functor都具有一样的产生方案,那么不如大家都不要实现,等到最后统一的在所有的functor里面加上这么一系列的产生代码吧。例如,如果要添加从某functor XXX到operator=的functor的产生代码: 8ch~UBq/
9: |K]y
template < typename Right > $YQ&\[pDA
assignment < XXX, typename picker_maker < Right > ::result > operator = ( const KX}dn:;(3
Right & rt) const ok_{8z\#
{ F`}w0=-*(
return assignment < XXX, typename picker_maker < Right > ::result > ( * this , rt); uU!i`8
} :
MmXH&yR
下面对该代码的一些细节方面作一些解释 C>;8`6_!gU
XXX指的是原来的functor的类型,picker_maker<T>是一个类型变换的trait,如果T是一个常量,那么他会返回constant_t<T>,否则返回T本身。 p. ~jo
因此如果该函数声明在assignment的内部,那么就实现了连等,如果声明在的dereference(解引用)的内部,就允许(*A = B)的行为发生。 12DdUPOi
最后,如何把这些函数塞到各个functor的声明里边呢?当然可以用宏,但是。。。大家都知道这样不好。 H+&w