我爱电脑技术论坛's Archiver

不和人说话 发表于 2008-4-6 08:20

瘦身前后——兼谈C++语言进化

 前一阵子写了一篇文章,提到语言进化的职责之一,就是去除语言中的tricks(职责之二是去除非本质复杂性)。*b1}.z8Gtu

O ~ T7a+M   常看我blog的朋友肯定记得我曾写过的boost源码剖析系列。本来这个系列是打算成书的,但随着对C++的认识发生了一些转变,对语言级技术的热衷逐渐消退,再回过头来看boost库中的一些组件,发现原本觉得很有写的必要的东西顿时消失了。Scott Meyers的主页上也列有一个写Boost Under The Hood的计划,一直也不见成文,兴许也有类似的原因。a Z.F:I0~-z.k

},E~s~:M3Y/A   一门语言应该是“Make simple things simple, make complex things possible”的。当我们用语言来表达思想的时候,这门语言应该能够提供这样的能力:即让我们能够最直接地表达我们的意思,多一分则太多,少一分则太少,好比古人形容美女:增一分则太肥,减一分则太瘦。
.mg_.D5a0G~ f;T.N5W D(fh
  这个问题上,有一个我认为是广泛的误解,就是“KISS便意味着要精简语言,并避免在编码中使用‘高阶’语言特性”。对此有一句话我觉得说得好:你不能通过从一门语言中去掉东西来增加表达力。高阶特性是一面利刃,用得不好固然伤了自己,但这并不表明就没有用。任何东西都是在它真正适用的地方适用,霸王硬上弓的话弓断弦崩反而伤及自身。所以,仅仅因为高阶特性容易误用(而且高阶特性的确也容易吸引人去用且容易误用,不过这是另一个问题),就断然在任何地方都不用并宣称这样才是KISS的话,便因噎废食了。举个例子,高阶函数是有用的,如果在真正需要高阶函数的地方不用高阶函数,那不是KISS,只能让解决方案(或者更确切地说,workaround)更复杂。lambda函数是有用的,但如果在真正需要lambda的地方不使用lambda,也只能导致更复杂更不直观的workarounds。OOP是有用的,但如果你的程序本来就只是简单的“数据+操作”你偏要硬上OOP的话,不仅多了编码时间,而且还降低程序的可见度和可维护性,后者就意味着项目的money。拿C++来说,这是一个广为诟病的问题。C++的偏向底层的应用领域决定了有不少地方使用C++其实就是“数据+操作”,然而很多人却因为用的是C++编译器,便忍不住去使用高级特性,结果把本来简单的事情复杂化——我自己就有不少次这样的经历:用了一大堆类之后,做完了回过头来再看,这些类都干嘛来着?需要吗?最关键的就是要清楚自己做的是什么事情,以及什么工具才是对你所做的事情最适合的。
c#j$Vp@&Z /i K3C\-T^*x^@;d$YI8b
  说到这里不妨顺便说说另一个误解:“如果我反正用不着C++里面的高级特性,那还不如用C罢了”,鉴于C/C++的应用领域,的确有不少地方是可以用C++的C部分完成得很好的,所以这个误解被传播得还是蛮广泛的。这里的一个微妙的忽视在于:用C的话,你就用不到许多很好的C++库了。用C++的话,你完全可以在你自己的编码中不使用高阶特性(说实话,这需要清醒的头脑和丰富的经验,以及克制能力),但你还是可以利用众多的C++库来简化你的工作的:如果一个transform明明可以搞定的你偏要写一个for出来难道能叫KISS?如果一个vector就能避免绝大多数内存管理漏洞和简化内存管理工作你偏偏要手动malloc/free那能叫KISS(我见过不少用C++编码却到处都是malloc/free的)?如果最直接的方式是gc你偏偏要绕一大堆弯子才能保证正确释放那也不叫KISS(等C++09吧)。如果一个for_each(readdir_sequence(".", readdir_sequence::files), ::remove);能搞定的你偏要写:
&q.I9ia:n}2I (V!^'_v9uLS
// in C
-q,]Sp#L
aCf0C~fU _!G X DIR* dir = opendir(".");7p[S]8W1Re
zVL!L ~V O(p
if(NULL != dir),A wry4W1Y{ wSjo

rSoJDiAK` {z9yO1~N

`4SZ }m@f2K+` struct dirent* de;+Nk-vA;^2AX

;QN1W,py|rSN7A for(; NULL != (de = readdir(dir)); )
6f)c?$Q.\bT0F0],SD?#D q 6Vfc&x E2`#N
{?]x@ W3G(N[)G
/i5E5e.nn5G9Oqbg
struct stat st;
!Q0O2y#{7|}
xL"J0]a5@H7S if( 0 == stat(de->d_name, &st) &&
N&l"X2Vj"yU)t
#a-F~/y9y5n%Z hUL&~4V S_IFREG == (st.st_mode & S_IFMT))Kq5A~-q%P B
3b2ivW"`1w
{
${2|E e7U!]
J0qk)jN0G+nk,N.X:R!OGWJ remove(de->d_name);0FbJ*yhX1z:fQ
4i6P ~)zJ v
} RE2Gx"KR6p l-{

0s+Ln`rT-g }E Nf xD7T7l#SE,z
+~ zo,[&f3T3@)rXQ
 closedir(dir);^ L0o,F(@A
7c:dO)p8N+A[
} ,eZ7j `%f
7oc/[$j'? l9L
  那能叫KISS?
)d)m3v*EF"jl@;r4\ }F-LD$x1et~
  总之还是那句话:明确知道你想要表达的是什么并用最简洁(在不损害容易理解性的前提下)的方式去表达它。但我认为,最KISS不代表最原始。pUM*~3@\]Wx
!Re(S7~|
  进化——两个例子_WC$K~nf7H

] g:VCs gy7P*_   先举一个平易近人的例子(Walter Bright——D语言发明者——曾在他的一个presentation中使用这个例子),如果我们想要遍历一个数组,在C里面我们是这么做(或者用指针,不过指针有指针自己的问题):F PB-N5a^u;_#S
1Q m"h0j(p5_4in m.D2{T
int arr[10];
'I!J7Dt/r-j[?1z`bT i
6G Jzzy9Y!~ … // initialize arr
5@"ZC WIL
&? b:iGk Y/o for(int i = 0; i < 10; ++i)
w7Q;J4]R_5{+_ bal{"h
{
z/jF6YM,F&y
&C F Ip_v [/n int value = arr[i];]$HgcUf*B

2Y6]5GU7WG ^Q7g yEk7pV/X,K

[0x0p&w)?4aHp8c f printfkR-gf9N)k
t.u.X;Y)@
}
1ih/r]h @5niPFW
D&a2j6{'u5P   这个貌似简单的循环其实有几个主要的问题:
Q%a&wB@)]b&p |6A])p~ epL fH.F
  1. 下标索引不应该是int,而应该是size_t,int未必能足够存放一个数组的下标。'p'P \(T7_i+Lx
O9l*p4Y4v,t
  2. value的类型依赖于arr内元素的类型,违反DRY,如果arr的类型改变为long或unsigned,就可能发生截断。`0v Y]#\s-| c
,kH@@:~[ ])v i
  3. 这种for只能对数组工作,如果是另一个自定义容器就不行了。;kSo%N7h n9c
y3V#a` O-Aj
  在现代C++里面,则是这么做:buSR&y E3}Q(`4U)l!`
+tr'ZOAU R(W-WPlp
for(std::vector<int>::iterator
9aP1Sx"p1w2F%d/e6U
9[-G n ] @:G iter = v.begin(); CMW"@8jF#j

#aG*v J j8\n(k iter != v.end(); OM[,yf#rk*j
c7eq}e xm
++iter) {
Px L!a u_T7z:x j 9G+dD*SNO"s

{p4Y,n2hG)K5v
\1c6fv W-?!s$p } k P`,U,x\$A

"z9~F;@SL   其实最大的问题就是一天三遍的写,麻烦。for循环的这个问题上篇讲auto的时候也提到。
s+np h(K8c#].J
Co A7]UBsF%SF   Walter Bright然后就把D里面支持的foreach拿出来对比(当然,支持foreach的语言太多了,这也说明了这个结构的高效性)。
*SMO.T(MF's Z
n a7nJ9HOi0G foreach(i; v) {
%? R8\I?8k Q
xi p%NUjU+uj8zA/f)TM-N G
9o/KTq|FB5@m
}
v(c^,R3hH8o \+r
X3x6?Tg   不多不少,刚好表达了意思:对v中的每个元素i做某某事情。/ZI8~e(t

!Q;~,Z|.s;Vbk   这个例子有人说太Na?ve了,其实我也赞成,的确,每天不知道有多少程序员写下一个个的循环结构,究竟有多少出了上面提到的三个问题呢?最大的问题恐怕还是数组越界。此外大家也都亲身体验过违反DRY原则的后果:改了一处地方的类型,编译,发现到处都是类型错误,结果一通“查找——替换”是免不了的了,谁说程序员的时间是宝贵的来着?.MA8R3] \S8`8@)U
eLx!nW#e6mGc [
  既然这个例子太Nave,那就说一个不那么Nave的。Java为什么要加入closure?以C++STL为例,如果我们要:
A ]b\ O/x;F
R9h,PJx+MS,V transform(v1.begin(), v1.end(), v2.begin(), v3.begin(), _1 + _2);V)Hv(c:l-Q)F)~4W!~

HIW4Y;}K8L   也就是说将v1和v2里面的元素对应相加然后放到v3当中去。这里用了boost.lambda,但大家都知道boost.lambda又是一个经典的鸡肋。_1 + _2还算凑活,一旦表达式复杂了,或者其中牵涉到对其它函数的调用了,简直就是一场噩梦,比如说我们想把v1和v2中相应元素这样相加:f(_1) + f(_2),其中f是一个函数或仿函数,可以做加权或者其它处理,那么我们可以像下面这样写吗:Og#H$W0y^#ZKj
3KHq"g,|*T2a;F
transform(…, f(_1) + f(_2));
6p ]C1z&q9Af
o hq{-RPbjl$U2B#w   答案是不行,你得这样写:+ZjAJ wj^'m/s

xx7TRK3?[u transform(…, *Osa;`0Rbf0_

h#zF(Gih1X\(} boost::bind(std::plus<int>(), boost::bind(f, _1), boost::bind(f, _1))
M1F0i"[!n9u9sK ;M4c,E h|
);
"iN A-K0uZ Q2L0ruY#]
  Lisper们笑了,Haskeller们笑了,就连Javaer们都笑了。It’s not even funny! 这显然违反了“simple things should be simple”原则。
Bgd8?IF p YfI#}
  如果不想卷入C++ functional的噩梦的话,你也可以这么写:c#nu5ABIT,U$l

)b hp)@%Dn!Uu struct Op%j9r{"^UR9qtnK1u
7x-q%a P3D$Q ]/w
{ gp,L+a9Fz

g-dj U(l r.Xp@ int operator()(int a1, int a2) { return f(a1) + f(a2); }(@:]r{q&m p
;g` q8U/c,N~6w
}; E)d6e-z"u,v~
xT%u!vF1g R7n!KI
transform(…, Op()); a%x3Q ^v

9F k W9O f6N   稍微好一点,但这种做法也有很严重的问题。
r%U@%z0k*w -_*}kPvW7{
  为什么Java加入closure,其实还是一个语法问题。从严格意义上,Java的anonymous class已经可以实现出一样的功能了,正如C++的functor一样。然而,代码是给人看的,语言是给人用来写代码的,代码的主要代价在维护,维护则需要阅读、理解。写代码的人不希望多花笔墨来写那些自己本不关心的东西,读代码的人也希望“所读即所表”,不想看到代码里面有什么弯子,最好是自然语言自然抽象才好呢。
7R/f4Jw"A(i#N;G
3P9k3t0g4Z6}6s   所以,尽管closure是一颗语法糖,但却是一颗很甜很甜的糖,因为有了closure你就可以写:
`r3QKh1T
j4U5Z+Q&C9M#s i$a4e transform(…, <>(a1, a2){ f(a1) + f(a2) });ppR ?%\2BZ

O#b+bRg Simple things should be simple!  
/o8fpL G
To1D&Hb   此外,closure最强大的好处还是在于对局部变量的方便的引用,设想我们想要创建的表达式是:-qim@8F"R4dI'h

)a%D'emM |s z0p int weight1 = 0.3, weight2 = 0.6;6B;J(O8M&Ps

&^t}v@6~DU*? transform(…, f(_1)*weight1 + f(_2)*weight2);
O-R3_Smx6k ,@Q1ht Rf;LwO
  当然,上面的语句是非法的,不过使用closure便可以写成:(R(X2hz+qB%U%{

X@m |xyN int weight1 = 0.3, weight2 = 0.6;B1~*po3wq
"`d(ME/A1@"u7UZ
transform(…, <&>(_1, _2){ f(_1)*weight1 + f(_2)*weight2 } );
'T9q%c!R!]5Rq]
'x|Y4ak-w[~   用functor class来实现同样的功能则要麻烦许多,一旦麻烦,就会error-prone,一旦error-prone,就会消耗人力,而人力,就是金钱。7t-Q!ljq/k

$s[.LHL   C++09也有希望加入lambda,不过这是另一个话题,下回再说。
AwZ yZ U+g{~2P UZ
The Real Deal——variadic templates

不和人说话 发表于 2008-4-6 08:21

 C++的callback类,google一下,没有一打也有半打。其中尤数boost.function实现得最为灵活周到。然而,就在其灵活周到的接口下面,却是让人不忍卒读的实现;03年的时候我写的第一篇boost源码剖析就是boost.function的,当时还觉得能看懂那样的代码牛得不行...话说回来,那篇文章主要剖析了两个方面,一个是它对不同参数的函数类型是如何处理的,第二个是一个type-erase设施。其中第一个方面就占去了大部分的篇幅。l|(AiqB @U/]
2x e-?|q{;V*g9XUCV
  简而言之,要实现一个泛型的callback类,就必须实现以下最常见的应用场景:
q}T q{X NXiC,Y
function<int(int, int)> caller = f;QU/w W,Vf)gs5LY
s2rrl)v6ZD
int r = caller(1, 2); // call f
N$rB [ Y#}:Z@ H"O?6b4h'^.H.y$U9s:Q
  为此function类模板里面肯定要有一个operator(),然而,接下来,如何定义这个operator()就成了问题:
%UR8K)\q:kg w2zW}%eLST]
template<Signature>
]&n P5v EM+P;m
v~d{m^:G9~Z+r class function,{g#h3v1eW'adG-]!x4d

L6Bzj dJ sf {
^Y'qwU1_9y s*D;L3f6M
operator()(???);
*yM,VX @(lV
nTp'X_YQ };
|+z|7V*t%M
\CvJp   ???处填什么?返回值处的???可以解决,用一个traits:typename result_type<Signature>::type,但参数列表处的???呢?b_jB!?I AP
(AUk rgyI&K
  boost采用的办法也是C++98唯一的办法,就是为不同参数个数的Signature进行特化:#i8_spk2_
R o]VF~&\N
template<typename R, typename T1>
m!?'L[RJQ1J
t$g%ARo class function<R(T1)>)N-t upC0g
(Rz2z TMYPr.c
{
8bM*C p5c d/Fc 6G{I$mV,o
R operator()(T1 a1);
(\#R?Um [(R1k2aeSt0Z
};
Ca9o+QX*vF %Eb5I.o.D
template<typename R, typename T1, typename T2>2iy}J9|At

l+I0E q1] class function<R(T1, T2)>#[T \%m*e+P
x4o6|!y"|A:YI
{(bJs \G*R

A O dF^(r)D`0[#B!Cc R operator()(T1 a1, T2 a2);
PV+X8n&r"r1G3oQ(d
+W-C3E"n@g u&?3Dm1c0Y };
G!p N$l8O.O
7} U|2i[o,{| template<typename R, typename T1, typename T2, typename T3>V6{;YMilO0\
~T1Tp yg&[v
class function<R(T1, T2, T3)>
~h&z&B(ad|
6fzs#Du$EM {
} j.}(T fsY9bH l.Du;rh/QC6l#E
R operator()(T1 a1, T2 a2, T3 a3);
'rh@-aQp`
,np ~*_y,@:J };
HJ/C$HBtx7{
K7H:S1U5go
,`'V"w`*bL#L:EH … // 再写下去页宽不够了,打住…
` w e4Q F /?9\LHJm&u
S2P#MU+Eg
  如此一共N(N由一个宏控制)个版本。
9i/vL:{M Ym.k}#Z
  这种做法有两个问题:一,函数的参数个数始终还是受限的,你作出N个特化版本,那么对N+1个参数的函数就没辙了。boost::tuple也是这个问题。二,代码重复。每个特化版本里面除了参数个数不同之外基本其它都是相同的;boost解决这个问题的办法是利用宏,宏本身的一大堆问题就不说了,你只要打开boost.function的主体实现代码就知道有多糟糕了,近一千行代码,其中涉及元编程和宏技巧无数,可读性可以说基本为0。好在这是个标准库(boost.function将加入tr1)不用你维护,如果是你自己写了用的库,恐怕除了你谁也别想动了。所以第二个问题其实就是可读性可维护性问题,用Matthew Wilson的说法就是可发现性和透明性的问题,这是一个很严重的问题,许多C++现代库因为这个问题而遭到诟病。
SQwL1Vu NfX lVxV8?}0K"l)|bU
  现在,让我们来看一看加入了variadic templates之后的C++09实现:
9[O'N2j9jo0RF 9ax8k&a/r%v!r4?#@V
template<typename R, typename... Args>+D0rnyUJb m)Q
&| [~$Ej
struct invoker_base { 6G1R cBOv9C4_
6zV7aIh!E.q
virtual R invoke(Args...) = 0;
+e4d:K9~w F TD1mw];~t
virtual ~invoker_base() { }
C a;{V7gp)s;r6T &u6xh1F-l*b,i:kWj|x
};I&Z$V(Y&sDXR e8X

o+IAA/}*L)dU template<typename F, typename R, typename... Args>
oglBx/q 5p#Z2v `AD)k}5X
struct functor_invoker : public invoker_base<R, Args...> +dqV.x T/i!_%q+~

V+x(YotD&J {
8G_p1o1H2~ ,~~H `Wtt
explicit functor_invoker(F f) : f(f) { }*@t,nn+}NP2l

7n"E?#B] G(sE&Z R invoke(Args... args) { return f(args...); }
o.|*v v4sP%^ ND[E$P;p_]
private:
,bjJ)e8X-[.L-EQD7|3z /D/DAz Ae
F f;
o:{3J\-d
Fs|(lkY };
l;_p"\jO7pD ']:v!Z-X$m1t,~7_
template<typename Signature>N.Bw~n5j#l,r

|Y:jU@ class function;
\:I:Dw Y,E A F%x7d+~'u
template<typename R, typename... Args>
j&^,hQsD
5Ly$G/C9b\Y| class function<R (Args...)> t+v D U A}Dc+U
{
\\4S+Vw~*V/Rv
%pV9b ]]Ua public:O{{~2G+I!o!s
bAAc CVo#tF
template<typename F>:w@`tWX0@x!w

V|d_HH function(F f) : invoker(0)
U Hr'W F%g1Y$Cu 9xpjzXB6O
{
P}0r&iE'GHp 5az{A~l`b
invoker = new functor_invoker<F, R, Args...>(f);7y2t)Qf;v k
5E qE%N#j0Ye
}
\M;~%Z G5y$Sr R operator()(Args... args) const
Ndu.g/B4]&|A2re {
~/tk#@#XQ
4JM:dkLa%Mp return invoker->invoke(args...);;^ r/_m3TOoh
5o ge2tfM`
}2?[cd%zPc
private:3DN7HMCF

7JJ} ~m \}0S n*?H invoker_base<R, Args...>* invoker; P8W d:TW1og)h
JP|TM$OE m
};
0i*G$Ia({ ] C7N
?z~]*v-L3^8rl"yU5{   整个核心实现就这些!一共才36行!加上析构函数拷贝构造函数等边角料一共也就70行!更重要的是,整个代码清晰无比,所有涉及到可变数目个模板参数的地方都由variadic templates代替。“Args…”恰如其分的表达了我们想要表达的意思——多个参数(数目不管)。与C++98的boost.function实现真是天壤之别!1S,a%xe+r

aRN!f!E;?F8TXLZF   这里function_invoker是用的type-erase手法,具体可参见我以前写的boost.any源码剖析,或上篇讲auto的,或《C++ Template Metaprogramming》(内有元编程慎入!)。type-erase手法是像C++这样的弱RTTI支持的语言中少数真正实用的手法,某种程度上设计模式里面的adapter模式也是type-erase的一个变种。
p!U8TAB^C 9VA1|Y`dK h&c"O Y
  如果还觉得不够的话,可以参考variadic-templates的主页,上面的variadic templates proposal中带了三个tr1实现,分别是tuple,bind,function,当然,variadic-templates的好处远远不仅仅止于这三个实现,从本质上它提供了一种真正直接的表达意图的工具,完全避开了像下面这种horrible的workaround:
_3Dv3[2N;D)~"LL B (kP(Y3b~^"Os
template<class T1>
.lVq)dPM8["~W isviN$C3c6P4g
cons(T1& t1, const null_type&, const null_type&, const null_type&,P {qu*dD

3u(o0s9EFS:ct0}u const null_type&, const null_type&, const null_type&,2at;\el RQqI3~R

e4T$P iex const null_type&, const null_type&, const null_type&)gz k%pDp)H2N

8TrMirM1H2bH$` : head (t1) {} u;i6?:[;e+z

l*n M]X&lJV   tuple的C++98实现,代码近千行。利用variadic-templates实现,代码仅百行。
;}6[o@*Oau ` @
p!]`x5^"x   和这种更horrible的workaround:\e#@`-C,K
0H2m0bk(w
template<class R, class F, class A1, class A2, class A3, class A4, class A5, class A6>.yYWDA

h.K}v6h_6X _bi::bind_t<R, F, typename _bi::list_av_6<A1, A2, A3, A4, A5, A6>::type>
#T'n'C f,T{&S6E%j
yiA"L8G BOOST_BIND(boost::type<R>, F f, A1 a1, A2 a2, A3 a3, A4 a4, A5 a5, A6 a6)*?"| iN!oD n,Z2Y

(Co.B#A c {!_-m:i+A2C2ggw4G
r)~9n6x"lW y h[
typedef typename _bi::list_av_6<A1, A2, A3, A4, A5, A6>::type list_type;xJS{?5z1w.J

~b%}|*vs return _bi::bind_t<R, F, list_type>(f, list_type(a1, a2, a3, a4, a5, a6));%@u2}X}VQ
(bFwZ9f K]
} `s7X:VgeE m/WU

d2Kt~]   小小的boost.bind,实现代码逾两千行,其间重复代码无数。用了variadic-templates,实现不过百行。
$@Yw/?#aN '{jGUNE/rK&B
  BTW. variadic templates在C++大会上一次性几乎全数投票通过。lambda能不能进标准则要看几个提案者的工作。目前还没有wording出来。不过只要出了wording想必也会像variadic templates那样压倒性通过的。

页: [1]

Powered by Discuz! Archiver 6.1.0  © 2001-2007 Comsenz Inc.