http://www.csdn.net/article/2013-01-17/2813778-the-beauty-of-doom3-source-code/2
摘要:Dyad作者、资深C++工程师Shawn McGrathz在空闲时翻看了Doom3的源代码,发出了这样的惊叹:“这是我见过的最整洁、最优美的代码!”“Doom 3的源代码让我对那些优秀的程序员刮目相看。”因此有了本文。
最少模板
id“犯了不少C++的禁忌”,他们重写了所有需要的STD函数。我个人对STD爱恨交织。在Dyad,我调试构建时常使用它来管理动态资源;在发布时又会处理所有的资源,避免使用任何STL函数,以求尽快地加载。STL很不错,因为它提供了快速的通用数据结构;它又很糟糕,因为使用它经常导致代码丑陋不堪,甚至容易出错。例如std::vector<T>类,如果我想迭代每一个元素:
在C++11中要简单些:
但我个人并不喜欢自动化,虽然它简化了代码编写,却导致代码更难阅读,最起码我现在是这么认为的。
STD有的函数、算法甚至非常荒谬,比如要从std::vector中删除一个值:
你必须每次都能拼写正确!id除去了其中所以含糊不清的部分:他们使用自己的通用容器、字符串类等等。他们编写的类比起STL要更加专一,易于理解。id还尽可能地避免使用模板,而且使用自己定制的内存分配器。STD代码里则充斥着无意义的垃圾模板,而且不易于阅读。
C++代码很难写好,所以你需要不断地努力,不相信的话可以去看看Microsoft和GCC的STD代码,这是我见过的最难看的代码!
id通过不滥用泛型就简单地解决了这个问题。他们编写了HashTable<V>和HashIndex类,HashTable强制key类型是const char *,而HashIndex是int->int对。这看起来像是很糟糕的C++实例。他们“本应该”只有一个HashTable类,然后为编写局部特殊化:KeyType = const char *,然后专门 <int, int>。
当然,id的做法完全正确,也保证了代码的优美。
对比更鲜明的是,Hash生成“C++优秀实践”和id做法的比较:
为特定类型专门化:
这样你可以把ComputeHashForType当作HashComputer传给HashTable:
这和我的做法很相近,看起来很聪明,但实际上很难看!因为,如果可选的模板参数很多怎么办?
这种情况下函数定义要更糟:
如果没有代码高亮,我甚至不能区分出方法名!
我也曾看到其它引擎试图通过卸载模板参数规范到无数的typedef,这更糟糕!也许这利于理解,但却导致了本地代码和整个系统逻辑的断层,所以缺乏美感。例如:
以及:
你这样使用两者:
你会产生疑惑:StringHashTable内存分配器——StringAllocator会涉及全局内存吗?这里导致了混淆,于是你又需要返回之前的代码检查(循环)……
Doom的做法和常规C++逻辑完全相反:它尽可能地避免泛型,除非有特别的意义。Doom的HashTable需要生成hash值时怎么办?它只需要调用idStr::GetHash()。
C语言的余韵
虽然我不清楚id团队其他人的出身如何,但John Carmack基本上可以说是开发C应用起家的,id在Quake III之前开发游戏用的都是C语言。我见过很多没有C开发功底的C++程序员,编写代码都有非常重的C++特色,上面过度使用模板的情况只是其中一例,其它还有:
- 过度使用set/get方法
- 使用字符串流
- 过度使用操作符重载
id在以上方面都做得非常完美。
通常很多人会这样创建一个类:
这样不仅浪费行数,还需要花费更多的时间编来写和阅读代码。相比之下:
如果你经常为var自增某个数字n呢?
相比于:
上面的例子明显容易阅读和编写。
id从不使用字符流,字符流通常包含糟糕的操作符重载:<<
例如:
虽然它有很多好处,但是很难看,而且语法也让人讨厌。
id选择printf()来代替,这样也易于阅读理解。我同意这样的决定。
另一方面,Doom还尽量避免操作符重载。虽然操作符重载是非常优秀C++特性,但没有操作符重载也就没有歧义,更便于编写和阅读。
横向空间
这是我从Doom的代码中最大的收获,原来我是这样编写代码的:
根据Doom3的编码标准,始终使用相对于4个空格的tab,水平对齐其中所有类的定义:
他们很少在类的定义中嵌入内联函数,我看到的唯一一次是代码和函数声明写在了同一行,这种做法有点不符合规范。这种类定义的组织方式非常容易解析,不过需要更多的时间来编写。
我讨厌多余的代码编写,但这种情况下,我只需要这次稍微多做一点工作,其他程序员在之后接手时就可以省下很多功夫。相信这里的Doom3编程规范能够帮助你理解其代码之美。(有网友称Google的C++编程规范与其也有很多相似之处。)
方法名
我认为Doom在方法名方面缺乏规范,我个人会尽可能地以动词开头命名方法:
比这样要好得多:
以下是John Carmack本人的回复:
从某些角度来看,我认为Quake3的代码更加整洁,算是我C语言代码的风格的一次进化,而非C++风格的第一次迭代。当然也可能因为总代码行数更少,或者是因为我已经10年没看过它的代码引起的错觉。我认为“好的C++”在可读性方面比“好的C语言”更好,其它方面大体相同。
我开始掌握C++是在Doom3开发的时候——在这之前,我有丰富的C语言编程经验,因为NeXT Objective-C编程的原因也有OOP(面向对象编程)背景,因此在使用C++的时候并没有对其使用和习惯进行适当针对性的研究。现在回想起来,真希望提前看过Effective C++这样的教程。团队里其他程序员虽然之前有C++编程经验,但基本上也是按照我选择和设置的风格在编程。
很多年来,我一直怀疑模板,一直在克制地使用它,不过最终确定自己更喜欢强类型,而非充满奇怪的代码的头文件。关于STL的争论在id内部一直没有停息,显得很有生气。回想Doom3开始开发的时候,使用STL基本上算不得好主意,直到现在,即使是在游戏中我们也仍然在争论这件事。
关于const,我直到现在基本上还是一个nazi,我会斥责每一个不尽可能常量化变量和参数的程序员。
我现在的风格主要是在向函数式编程靠近,这样可以舍去很多旧习,逐渐远离一些OOP的方向。
关于C++函数式编程John Carmack写过一篇《Functional Programming in C++》值得一读!《程序员》对这篇文章做过编译。
附:id-Software源代码网站(GitHub)|Doom3源代码(GitHub)|Doom3 BFG源代码(GitHub)