代码之美——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清空DataGridView中的数据 分类: DataGridView 2014-05-19 20:56 180人阅读 评论(0) 收藏
我们一般要把dgv情况,一般用: DataTable dt = (DataTable)dgvData.DataSource; dt.Rows.Clear(); dgvData.DataSource = ...
- Android 官方命令深入分析
原文:www.libgdx.cn Android SDK包括了多种工具来帮助你创建基于Android平台的移动应用.这些工具一般分成两类:SDK 工具和 platform 工具. SDK 工具是独立的 ...
- PHP运行出现Notice : Use of undefined constant 的解决方法【已测】
关闭 PHP 提示的方法 搜索php.ini:error_reporting = E_ALL改为:error_reporting = E_ALL & ~E_NOTICE还有个不是办法的办法就是 ...
- SQL Server 2008 常见异常收集(持续更新)
写在前面: 最近,在使用SQL Server 2008时,出现了不少问题.发现,很多问题都是以前碰见过的,并且当时也寻找到了解决方法(绝大部分来源于“百度”与“Google”),只是时间一长,又忘记了 ...
- 由 argv引出的main参数 分类: C/C++ 2014-11-08 18:00 154人阅读 评论(0) 收藏
我们经常用的main函数都是不带参数的.因此main 后的括号都是空括号.实际上,main函数可以带参数,这个参数可以认为是 main函数的形式参数.C语言规定main函数的参数只能有两个, 习惯上这 ...
- VirtualBox 运行失败
运行 VirtualBox --help 安装 VirtualBox 后 运行 报错内核没加载问题 需要设置环境变量 内核加载的环境变量 export KERN_DIR=/usr/src/kernel ...
- android ScrollView 充满屏幕
android:fillViewport=true ScrollView下面的组件如果有android:layout_height="fill_parent"或android:la ...
- MS SQL 性能优化
http://blog.csdn.net/dba_huangzj/article/details/50455543
- python-gdb
https://blog.log4d.com/2013/11/python-gdb/ https://wiki.python.org/moin/DebuggingWithGdb
- iOS--为视图添加阴影
iOS–为视图添加阴影 情况一:视图添加圆角,在添加阴影 //阴影视图 self.viewShadow = [[UIView alloc]initWithFrame:CGRectMake(0, 0, ...