瘦身前后——兼谈C++语言进化
前一阵子写了一篇文章,提到语言进化的职责之一,就是去除语言中的tricks(职责之二是去除非本质复杂性)。*b1}.z8Gt u常看我blog的朋友肯定记得我曾写过的boost源码剖析系列。本来这个系列是打算成书的,但随着对C++的认识发生了一些转变,对语言级技术的热衷逐渐消退,再回过头来看boost库中的一些组件,发现原本觉得很有写的必要的东西顿时消失了。Scott Meyers的主页上也列有一个写Boost Under The Hood的计划,一直也不见成文,兴许也有类似的原因。a Z.F:I0~-z.k
一门语言应该是“Make simple things simple, make complex things possible”的。当我们用语言来表达思想的时候,这门语言应该能够提供这样的能力:即让我们能够最直接地表达我们的意思,多一分则太多,少一分则太少,好比古人形容美女:增一分则太肥,减一分则太瘦。
f;T.N5W D(fh
这个问题上,有一个我认为是广泛的误解,就是“KISS便意味着要精简语言,并避免在编码中使用‘高阶’语言特性”。对此有一句话我觉得说得好:你不能通过从一门语言中去掉东西来增加表达力。高阶特性是一面利刃,用得不好固然伤了自己,但这并不表明就没有用。任何东西都是在它真正适用的地方适用,霸王硬上弓的话弓断弦崩反而伤及自身。所以,仅仅因为高阶特性容易误用(而且高阶特性的确也容易吸引人去用且容易误用,不过这是另一个问题),就断然在任何地方都不用并宣称这样才是KISS的话,便因噎废食了。举个例子,高阶函数是有用的,如果在真正需要高阶函数的地方不用高阶函数,那不是KISS,只能让解决方案(或者更确切地说,workaround)更复杂。lambda函数是有用的,但如果在真正需要lambda的地方不使用lambda,也只能导致更复杂更不直观的workarounds。OOP是有用的,但如果你的程序本来就只是简单的“数据+操作”你偏要硬上OOP的话,不仅多了编码时间,而且还降低程序的可见度和可维护性,后者就意味着项目的money。拿C++来说,这是一个广为诟病的问题。C++的偏向底层的应用领域决定了有不少地方使用C++其实就是“数据+操作”,然而很多人却因为用的是C++编译器,便忍不住去使用高级特性,结果把本来简单的事情复杂化——我自己就有不少次这样的经历:用了一大堆类之后,做完了回过头来再看,这些类都干嘛来着?需要吗?最关键的就是要清楚自己做的是什么事情,以及什么工具才是对你所做的事情最适合的。
/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);能搞定的你偏要写:
(V!^'_v9uLS
// in C
DIR* dir = opendir(".");7p[S]8W1Re
zVL!L ~V O(p
if(NULL != dir),Awr y4W1Y{ wSjo
{z9yO1~N
struct dirent* de;+Nk-vA;^2AX
for(; NULL != (de = readdir(dir)); )
6Vfc&x E2`#N
{?]x@ W3G(N[)G
/i5E5e.nn5G9Oq b g
struct stat st;
if( 0 == stat(de->d_name, &st) &&
S_IFREG == (st.st_mode & S_IFMT))Kq5A~-q%P B
3b2ivW"`1w
{
remove(de->d_name);0FbJ*yhX1z:fQ
4i6P~)zJv
} RE2Gx"KR6p l-{
}E NfxD7T7l#SE,z
+~ zo,[&f3T3@)rXQ
closedir(dir);^L0o,F(@A
7c:dO)p8N+A[
} ,eZ7j `%f
7oc/[$j'? l9L
那能叫KISS?
}F-LD$x1et~
总之还是那句话:明确知道你想要表达的是什么并用最简洁(在不损害容易理解性的前提下)的方式去表达它。但我认为,最KISS不代表最原始。pUM*~3@\]Wx
!Re(S7~|
进化——两个例子_WC$K~nf7H
先举一个平易近人的例子(Walter Bright——D语言发明者——曾在他的一个presentation中使用这个例子),如果我们想要遍历一个数组,在C里面我们是这么做(或者用指针,不过指针有指针自己的问题):F PB-N5a^u;_#S
1Q m"h0j(p5_4in m.D2{T
int arr[10];
… // initialize arr
for(int i = 0; i < 10; ++i)
bal{"h
{
int value = arr[i];]$HgcUf*B
… yEk7pV/X,K
printfkR-gf9N)k
t.u.X;Y)@
}
这个貌似简单的循环其实有几个主要的问题:
|6A])p~ epL fH.F
1. 下标索引不应该是int,而应该是size_t,int未必能足够存放一个数组的下标。'p'P \(T7_i+L x
O9l*p4Y4v,t
2. value的类型依赖于arr内元素的类型,违反DRY,如果arr的类型改变为long或unsigned,就可能发生截断。`0vY]#\s-| c
,kH @@:~[])vi
3. 这种for只能对数组工作,如果是另一个自定义容器就不行了。;kSo%N7h n9c
y3V#a` O-A j
在现代C++里面,则是这么做:buSR&yE3}Q(`4U)l!`
+tr'ZOAU R(W-W Plp
for(std::vector<int>::iterator
iter = v.begin(); C MW"@8jF#j
iter != v.end(); OM[,yf#rk*j
c7eq}e xm
++iter) {
9G+dD*SNO"s
…
} k P`,U,x\$A
其实最大的问题就是一天三遍的写,麻烦。for循环的这个问题上篇讲auto的时候也提到。
Walter Bright然后就把D里面支持的foreach拿出来对比(当然,支持foreach的语言太多了,这也说明了这个结构的高效性)。
foreach(i; v) {
…+uj8zA/f)TM-N G
9o/KTq|FB5@m
}
不多不少,刚好表达了意思:对v中的每个元素i做某某事情。/ZI8~e(t
这个例子有人说太Na?ve了,其实我也赞成,的确,每天不知道有多少程序员写下一个个的循环结构,究竟有多少出了上面提到的三个问题呢?最大的问题恐怕还是数组越界。此外大家也都亲身体验过违反DRY原则的后果:改了一处地方的类型,编译,发现到处都是类型错误,结果一通“查找——替换”是免不了的了,谁说程序员的时间是宝贵的来着?.MA8R3] \S8`8@)U
eLx!nW#e6mGc [
既然这个例子太Nave,那就说一个不那么Nave的。Java为什么要加入closure?以C++STL为例,如果我们要:
transform(v1.begin(), v1.end(), v2.begin(), v3.begin(), _1 + _2);V)Hv(c:l-Q)F)~4W!~
也就是说将v1和v2里面的元素对应相加然后放到v3当中去。这里用了boost.lambda,但大家都知道boost.lambda又是一个经典的鸡肋。_1 + _2还算凑活,一旦表达式复杂了,或者其中牵涉到对其它函数的调用了,简直就是一场噩梦,比如说我们想把v1和v2中相应元素这样相加:f(_1) + f(_2),其中f是一个函数或仿函数,可以做加权或者其它处理,那么我们可以像下面这样写吗:Og#H$W0y^#ZKj
3KH q"g,|*T2a;F
transform(…, f(_1) + f(_2));
答案是不行,你得这样写:+ZjAJ wj^'m/s