从去年七月到今年一月,我写完了工作后的第一个10,000行C++代码。虽然2015年实习的时候也写过一个暑假的C++,但是之前是游戏工作室,使用C++其实也只是用其中C的子集,甚至可以说是用一个Visual Studio来编译cpp后缀名的c文件。过去半年就大不一样,代码库是真材实料的C++,而且很多还是C++11的风格和语法。所以,我最多算一个0.5起点的C++程序员。

从这半年写的10,000行C++代码里面,我学到的不仅仅是C++语言的语法和特性,这篇文章主要总结一下C++语言相关的收货,另一篇文章整理一下关于软件工程的一些知识:设计、测试、遗留代码、代码审查、版本控制等等。

10k行后的C++感受

开始工作后的第一件事情就是学习C++。不过C++这门语言在C++11之后变化之大,让我确实感受到了Bjarne Stroustrup所说:

C++是一门新的语言。

C++变化巨大,变得越来越复杂,但是也越来越有意思。同时,C++与C的距离也越来越远。面对两种语言当前的差距,我觉得学习今天的C++,确实是没有必要从C开始了:很多C语言的表达方式不被推荐,一些C语言的语言特性在C++中更好的选择。学习C和学习C++确实应该被当做两件独立的事情了。

全新的表达能力

C++11以来,语言变得越来越具有表达性了1。Peter Van Roy在计算机编程范式中曾说道现代程序语言有往“声明式编程(declarative programming)”转变的趋势。从C++的转变来看,确实有这个意思。

比如说range_for这样的新语法,使得循环语句能够直接将关心的对象作为目标变量,而不再需要用下标索引来访问。auto的引入进一步简化了复杂的语法。这样一方面能够让处理内部元素的逻辑更加清晰,另一方面也能减少诸如下标访问非法的错误。

1
2
3
4
for (auto element : collection) {
  // 专心处理关心的对象element。
  // 不用被下标索引等冗余分心。
}

这样的表达方式很早就在Python之类的语言里面有了。不过在语法和思考方式上,这种for循环的写法都和之前的方法不一样,所以经常发现代码的结构和设计都自然地偏向原始的for循环。而一些有C/C++背景的人去写python的时候也时不时会习惯性地用“丑陋的”下标来写循环语句。

1
2
for element in collection:
  # 专心处理对象element。

我之前关于C++的印象就是我需要写很多指令来告诉C++一步一步具体怎么做。而我更喜欢Python这门语言的原因之一也是Python的语言表达性使得我写的代码更短、更易读、更好理解。不过这半年的C++11的体验让我觉得现代的C++还是很令人愉悦的:C++11以来全新的表达能力让我觉得C++表达能力越来越好,表达的内容也更加清晰明快。

要说让我强烈地感受到C++的新的表达能力的强大的,要数Sean Parent一系列关于“更好的代码”的讲座(讲座1讲座2讲座3)和资料,都非常的精彩。当整屏幕的代码最后被两三行的语句所代替的时候,那种美感和满足感真的是难以言表。

写有风格的C++

代码是写给人看的,编译器会把人写的代码编译为机器能看懂的代码。所以,人就专心做好人要做的事情,让编译器去专心做编译器要做的事情。C++是一门可以接触到机器底层的语言,也有很多语言技巧可以提高C++程序的性能,结果这个领域经常出现一些奇技淫巧。提高性能当然是好事,但是看得懂代码的人少,学得懂代码的人更少,这就会影响代码库的维护。虽然其中一些奇技淫巧已经成为了固定的套路(或者说是常用模式),但是还是有很多的技巧晦涩又危险。

这种感觉在我进入现在工作的这个巨大的代码库的时候变得十分强烈。读代码就好像读文章一样。一段简洁清晰的代码能让人很快地理解这段意思和推理这段代码的行为,正如一篇简洁明了的文章能快速传达文章要义,激发读者的想象。而一段晦涩的代码,如一篇晦涩的文章一样,让人不知所云。虽然作家可以尽情地寻找和发挥自己喜欢的风格,但是程序员最好还是追求一种简洁清晰的代码风格为好。毕竟编程也是一种社交活动2,而写软件通常都是靠团队合作。

不同的程序语言通常有不同的风格。比如Python社区就有很好的风格指导3,而具有Python风格的代码被认为是“Pythonic Style”。相比而言,C++的风格就没有那么成型。一方面是因为语言本身特性庞杂、变化多端,另一方面是可能是因为各自为政、各有所需,常常没有一个可以覆盖各个公司/开发者的需求的风格指南。这其中,比较有名的,也被吐槽的比较严重的莫过于Google的C++风格指南了。这份风格之南在过去几年中已经有了不少的改变,但是主要还是针对Google自己的代码库、程序员背景和产品需求来设计的。很多规定对其他开发项目并不合适,看到被吐槽的点,我觉得双方都应该被理解。作为更普适的风格指南,当前C++社区正在制定和书写的C++核心指南可能会比较好。

由于工作的需要,我自然需要先学习和理解我们自己的风格指南。通读全篇文章倒不用花费很长的时间,但是真正要在日常的代码书写中跟着要求走却是一件不容易的事情。更别说在指南之外还有160多个建议,我这10,000行代码的经验也就学到了个零头。写完了10,000行代码以后,注意了代码审阅中所有的知识点,还是经常被C++可读性学习项目的评审人轻易地指出20来个修改建议。

最感兴趣的两个专题

写完10,000行以后,我现在最想好好搞清楚的是两个专题:生命周期(lifetime)和资源所有权(resource ownership)。

生命的周期也就是一个对象从“生”到“死”的时间线。C++里面有全局变量,局部变量,静态变量,类内部静态与非静态成员变量,动态构造的指针变量等等,每一种变量都有自己的逻辑和生命周期。即使是同一类的变量,生命周期也有先后顺序。

对生命周期感兴趣主要是因为在写过的代码里面出现的几次重大问题都和变量生命周期没有搞清楚有关。比如没有搞清楚变量构造和析构的先后顺序,导致非法访问已经释放的空间。又比如没有搞清楚局部静态变量的构造时间点,导致提前访问了还没有分配的空间。更不要说面对网络通讯和多线程计算之类的问题的时候,我现在也很难理清各种变量产生和消亡的时间点,通常就想放弃推理,交给同事来审阅代码。这一部分,接下来要好好学习一下。

资源所有权跟生命周期息息相关。除了只能在对象存在的时候才能安全地访问对象所持有的资源(比如内存)中的数据之外,还要推理动态分配资源的所有权的转移问题。比如“智能指针(smart pointers)”这一类抽象类型的引入,就强调了指针对其指向的内存的所有权的管理。std::unique_ptr是相对比较好理解的,所以也在我们的风格之南中被高度推荐,而std::shared_ptr之类对于内存所有权的管理需要计算和跟踪,对开发者来说推理难度增大,所以经常不被推荐4。除此之外,还有移动语意(move semnatics)、返回值优化(return value optimization)之类的语法概念,都使得资源所有权的推理变得更加复杂。

C++11以来似乎越来越注重如何安全地管理资源,因此也更新了以上一系列的语法概念。作为初学者,我觉得学起来确实还挺难的。主要是刚开始接触这些概念的时候,感觉非常抽象。只有多写,多经历一些实际的例子之后,可能才能习惯。10,000行代码,肯定是远远不够的。

难点概念和特性

10,000行代码毕竟只是一个开始,很多C++的语言概念和特性我都还似懂非懂。希望自己能够在写完20,000行前理解这些语言特性。

  1. 右值(Rvalue)

  2. 移动和复制语意(move and copy semantics)

  3. 资源获取即初始化(Resource Acquisition Is Initialization, RAII)

  4. 运行时类型信息(Runtime Type Information, RTTI)

  5. 参数依赖查找(Argument-Dependent Lookup, ADL)

  6. 线程与并发(Thread & Concurrency)

  7. 回调(Callbacks)与std::function

除此之外,我在阅读中第一次体会到很多常用的数据,比如时间和字符串,处理起来原来是异常的复杂。之前编程只觉得字符串处理非常繁琐。后来才知道,时间的计算和处理更加困难,边界条件多得数不过来。想一想时区的更变,各个国家各个文化的日历,以及著名的千年虫和2038问题5。有时候很庆幸我不负责写这些底层逻辑,只是用写好的时间库就可以了。

接下来的路

10,000行C++仅仅只是个开始。我所使用过的C++语言特性还只是非常少的一部分。C++的语言学习还有很长的路要走。希望在20,000行Code之后能够写出初具风格的C++代码,以及对一些比较重要的C++语言特性和概念有比较好的把握。


  1. Expressive这个说法是我从博客“Fluent C++”第一次学到的。可能我有点过时了,但是这个说法确实让我眼前一亮。 ↩︎

  2. Github不就被戏称为全球最大同性交友网站么。 ↩︎

  3. 比如纲领性的The Zen of Python↩︎

  4. 开发者对于某项语法的理解难度通常是我们风格指南考虑是否推荐这项语法的因素,这也是被很多人吐槽的一点。不过说实话,我们这很多码农可能是我这样的C++普通玩家,要让大多数码农都能写出比较正确的代码,我觉得这是可以理解的。 ↩︎

  5. 2038问题也跟《斯坦因之门》紧密相关啊。 ↩︎