代码之美——Doom3源代码赏析2
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)
代码之美——Doom3源代码赏析2的更多相关文章
- 代码之美——Doom3源代码赏析1
http://www.csdn.net/article/2013-01-17/2813778-the-beauty-of-doom3-source-code/1 摘要:Dyad作者.资深C++工程师S ...
- java代码之美(14)---Java8 函数式接口
Java8 函数式接口 之前写了有关JDK8的Lambda表达式:java代码之美(1)---Java8 Lambda 函数式接口可以理解就是为Lambda服务的,它们组合在一起可以让你的代码看去更加 ...
- java代码之美(15)---Java8 Function、Consumer、Supplier
Java8 Function.Consumer.Supplier 有关JDK8新特性之前写了三篇博客: 1.java代码之美(1)---Java8 Lambda 2.java代码之美(2)---Jav ...
- 【OpenCV入门教程之二】OPENCV3 开源之美 — 编译源代码、配置opencv_contrib
为什么要配置opencv_contrib? opencv3.0版本 功能更加模块块,一些功能模块不够完善,等足够完善在merge到主分支中,而我们图像识别中要用到的SIFT等算法被封装在xfeactu ...
- java代码之美(11)---java代码的优化
java代码的优化 随着自己做开发时间的增长,越来越理解雷布斯说的: 敲代码要像写诗一样美.也能理解有一次面试官问我你对代码有洁癖吗? 一段好的代码会让人看就像诗一样,也像一个干净房间会让人看去很舒服 ...
- java代码之美(12)---CollectionUtils工具类
java代码之美(12)---CollectionUtils工具类 这篇讲的CollectionUtils工具类是在apache下的, 而不是springframework下的CollectionUt ...
- java代码之美(10)---Java8 Map中的computeIfAbsent方法
Map中的computeIfAbsent方法 Map接口的实现类如HashMap,ConcurrentHashMap,HashTable等继承了此方法,通过此方法可以在特定需求下,让你的代码更加简洁. ...
- java代码之美(9)---guava之Lists、Maps
guava之Lists.Maps 谷歌提供了guava包里面有很多的工具类,Lists和Maps集合工具,集合操作做了些优化提升. 1.概述 1.静态工厂方法 (1)Guava提供了能够推断范型的静态 ...
- java代码之美(8)---guava字符串工具
guava字符串工具 在java开发过程中对字符串的处理是非常频繁的,google的guava工具对字符串的一些处理进行优化,使我们开发过程中让自己的代码看去更加美观,清爽. 一.Joiner 根据给 ...
随机推荐
- winform 猜猜看 分类: WinForm 2014-08-21 14:12 267人阅读 评论(0) 收藏
说明: 1>窗体应用程序. 2>一个窗体(Form1),一个按钮(btnStart),一个文本(labTime) 3>截图: 4>代码如下: using System; usi ...
- MediaInfo源代码分析 4:Inform()函数
我们来看一下MediaInfo中的Inform()函数的内部调用过程 首先Inform()函数封装了MediaInfo_Internal类中的Inform()函数 //返回文件信息 String Me ...
- java不求有功,但求无过—异常处理
在程序开发中,错误往往有两种.一种是编译时出现的错误,该种错误比較easy发现.还有一种是执行时出现的错误,该种错误是开发者比較头疼的.异常就是一个执行时的错误,比如,除数为0 ,数组越界等. 异常处 ...
- iOS-设置状态栏白色以及覆盖状态栏
iOS-设置状态栏白色以及覆盖状态栏 将状态栏设置为白色 首先, 在info.plist中添加一个标记. View controller–based status bar appearance键值设置 ...
- object C—类中函数的调用
Object C-类中函数的调用 创建,三个类.然后,在代码中调用相同名字的函数.观察他们的调用次序. @interface test : NSObject - (void)print; @end @ ...
- web前端:js
内嵌样式<script></script> alert(“123”)弹出对话框 document.write(“test”) 引入方式 <title></ti ...
- javascript-图片横向无缝隙滚动
<style type="text/css"> <!-- ul,li,div{margin:0; padding:0; font-size:12px;} #dem ...
- 多列的行列转换(PIVOT,UNPIVOT)
形式1 形式2 形式3 有时候可能会有这样的需求: 将一张表的所有列名转做为数据的一列数据,将一列数据作为整张表的列名 当列比较多时,只用PIVOT是解决不了的,经过研究,需要将UNPIVOT 和 P ...
- 利用linq快速判断给定数字是否包含在某个段范围内
一.需求: 知道某段范围0x0020~0x007F0x00A0~0x017F0x01A0~0x01CF0x01F0~0x01FF0x0210~0x021F0x1EA0~0x1EFF给定一个值,快速判断 ...
- 对UIImage进行的一些操作
1.生成指定宽高的UIImage对象(oldImage为原始图片对象,newImage为操作后的图片对象) // 参数1:图片的尺寸 参数2:是否透明(没看出YES和NO有什么区别) 参数3:缩放(1 ...