【zz】解读google C++ code style谈对C++的理解


解读google C++ code style谈对C++的理解C++是一门足够复杂的语言.说它"足够复杂",是因为C++提供了足够多编程范式–泛型, 模板, 面向对象, 异常,等等.顺便说说,我已经很久没有跟进C++的最新发展了(比如C++0x), 所以前面列举出来的特性应该只是C++所有特性的一个部分罢了.C++特性过多很难驾驭好C++的原因之一.另一个原因是C++过于"自作聪明",在很多地方悄无声息的做了很多事情, 比如隐式的类型转换, 重载, 模板推导等等.而很多时候,这些动作难以察觉,有时候会在你意想不到的地方发生,即使是熟练的C++程序员也难免被误伤.(关于了解C++编译器自作聪明做了哪些事情, <<深入理解C++物件模型>>是不错的选择).

世界上有很多问题, 人们知道如何去解决.但是, 似乎这还不算是最高明的,更高明的做法是学会避免问题的发生.而如何避免问题的发生, 需要经验的积累–曾经犯下错误,吃一堑长一智,于是知道哪些事情是不该做的或者是不应该这么做的.

google C++ code style是google对外公布的一份google内部编写C++的代码规范文档.与其他很多我曾经看过的编码文档一样,里面有一些关于代码风格的规定,也就是代码的外观,这一部分不在这里过多讨论,毕竟代码如何才叫"美观"是一个见仁见智的话题.在这里专门讨论这份文档中对一些C++特性该如何使用的讨论,最后再做一个总结.注意其中的序号并不是文档中的序号,如果要详细了解,可以自己去看这份文档.

1) Static and Global Variables
Static or global variables of class type are forbidden: they cause hard-to-find bugs due to indeterminate order of construction and destruction.google明确禁止全局对象是类对象, 只能是所谓POD(Plain Old Data,如int char等)数据才行.因为C++标准中没有明确规定全局对象的初始化顺序, 假设全局类对象A,B,其中A的初始化依赖于B的值, 那么将无法保证最后的结果.如果非要使用全局类对象, 那么只能使用指针, 在main等函数入口统一进行初始化.

2) Doing Work in Constructors
In general, constructors should merely set member variables to their initial values. Any complex initialization should go in an explicit Init() method. 文档规定, 在类构造函数中对类成员对象做基本的初始化操作, 所有的复杂初始化操作集中一个比如Init()的函数中,理由如下:

  • There is no easy way for constructors to signal errors, short of using exceptions (which are forbidden).
  • If the work fails, we now have an object whose initialization code failed, so it may be an indeterminate state.
  • If the work calls virtual functions, these calls will not get dispatched to the subclass implementations. Future modification to your class can quietly introduce this problem even if your class is not currently subclassed, causing much confusion.
  • If someone creates a global variable of this type (which is against the rules, but still), the constructor code will be called before main(), possibly breaking some implicit assumptions in the constructor code. For instance, gflags will not yet have been initialized.
简单的概括起来也就是:构造函数没有返回值, 难以让使用者感知错误;假如在构造函数中调用虚拟函数, 则无法按照使用者的想法调用到对应子类中实现的虚拟函数(理由是构造函数还未完成意味着这个对象还没有被成功构造完成).

3) Default Constructors
You must define a default constructor if your class defines member variables and has no other constructors. Otherwise the compiler will do it for you, badly. 当程序员没有为类编写一个默认构造函数的时候, 编译器会自动生成一个默认构造函数,而这个编译器生成的函数如何实现(比如如何初始化类成员对象)是不确定的.这样,假如出现问题时将给调试跟踪带来困难.所以, 规范要求每个类都需要编写一个默认构造函数避免这种情况的出现.

4) Explicit Constructors
Use the C++ keyword explicit for constructors with one argument.假如构造函数只有一个参数, 使用explicit避免隐式转换, 因为隐式转换可能在你并不需要的时候出现.

5) Copy Constructors
Provide a copy constructor and assignment operator only when necessary. Otherwise, disable them with DISALLOW_COPY_AND_ASSIGN.只有当必要的时候才需要定义拷贝构造函数和赋值操作符. 同上一条理由一样, 避免一些隐式的转换.另一条理由是,"="难以跟踪,如果真的要实现类似的功能,可以提供比如名为Copy()的函数,这样子一目了然,不会像赋值操作符那样可能在每个"="出现的地方出现.

6) Operator Overloading
Do not overload operators except in rare, special circumstances.不要重载操作符.同样, 也是避免莫名其妙的调用了一些函数.同上一条一样, 比如要提供对"=="的重载, 可以提供一个名为Equal()的函数, 如果需要提供对"+"的重载, 可以提供一个名为Add()的函数.

7) Function Overloading
Use overloaded functions (including constructors) only in cases where input can be specified in different types that contain the same information. Do not use function overloading to simulate default function parameters.只有在不同的类型表示同样的信息的时候, 可以使用重载函数.其他情况下,一律不能使用.使用重载, 也可能出现一些隐式出现的转换.所以, 在需要对不同函数进行同样操作的时候, 可以在函数名称上进行区分, 而不是使用重载,如可以提供针对string类型的AppendString()函数, 针对int类型的AppendInt()函数,而不是对string和int类型重载Append()函数.另一个好处在于, 在阅读代码时,通过函数名称可以一目了然.

8) Exceptions
We do not use C++ exceptions.不使用异常.理由如下:
  • When you add a throw statement to an existing function, you must examine all of its transitive callers. Either they must make at least the basic exception safety guarantee, or they must never catch the exception and be happy with the program terminating as a result. For instance, if f() calls g() calls h(), and h throws an exception that f catches, g has to be careful or it may not clean up properly.
  • More generally, exceptions make the control flow of programs difficult to evaluate by looking at code: functions may return in places you don’t expect. This results maintainability and debugging difficulties. You can minimize this cost via some rules on how and where exceptions can be used, but at the cost of more that a developer needs to know and understand.
  • Exception safety requires both RAII and different coding practices. Lots of supporting machinery is needed to make writing correct exception-safe code easy. Further, to avoid requiring readers to understand the entire call graph, exception-safe code must isolate logic that writes to persistent state into a "commit" phase. This will have both benefits and costs (perhaps where you’re forced to obfuscate code to isolate the commit). Allowing exceptions would force us to always pay those costs even when they’re not worth it.
  • Turning on exceptions adds data to each binary produced, increasing compile time (probably slightly) and possibly increasing address space pressure.
  • The availability of exceptions may encourage developers to throw them when they are not appropriate or recover from them when it’s not safe to do so. For example, invalid user input should not cause exceptions to be thrown. We would need to make the style guide even longer to document these restrictions!
上面提到的理由中, 我认为使用异常最大的害处就是:异常的使用导致了程序无法按照代码所展现的流程去走的, 比如代码里面写了步骤一二三,但是假如有异常出现, 这就不好预知代码真正步进的步骤了, 在出现问题时, 给调试和跟踪带来困难.
另外, 我更喜欢unix API的设计.熟悉unix编程的人都知道, unix API基本上都遵守下列规则:
a) 返回0表示成功, 其他(一般是-1)表示失败.
b) 在失败时, 可以根据errno判断失败的原因, 这些在man手册中都是会清楚的描述.

总结一下, 这份规范中规避的C++特性大致分为以下几类:
a) 避免使用那些没有确定行为的特性:如全局变量不能是类对象(初始化顺序不确定), 不使用编译器生成的默认构造函数(构造行为不确定), 异常(代码走向不确定).
b) 避免使用那些隐式发生的操作:如声明单参数构造函数为explict以避免隐式转换, 不定义拷贝构造函数避免隐式的拷贝行为, 不使用操作符重载避免隐式的转换
c) 对模棱两可的特性给予明确的规定:不使用函数重载而是定义对每个类型明确的函数.
d) 即使出错了程序也有办法知道: 比如不能在类构造函数中进行复杂的构造操作, 将这些移动到类Init()的函数中.

同时, 这份文档中描述的大部分C++特性, 都是我之前所熟悉的(除了RTTI之外, 不过这里提到它也是要说明不使用它,另外还提到boost, 不过也是说的要对它"有限制"的使用,比如里面的智能指针).可以看到, 面对这样一门复杂同时还在不停的发展更新特性的语言, google的态度是比较"保守"的.这与我之前对C++的理解也是接近的, 我一直认为C++中需要使用到的特性有基本的面向对象+STL就够了(经过最近的编码实践,我认为还得加个智能指针).我对这个"保守"态度的理解是, 以C++当前的应用场景来看, 这些特性已经足够, 如果使用其他一些更加复杂的, 对人的要求提高了, 代码的可读性以及以后的可维护性就下降了.

前面说过, 避免问题的出现比解决问题来的更加高明些, 而面对C++这一个提供了众多特性, google C++ code style给予了明确的规定, 也就是每个行为, 如果都能做到有明确的动作, 同时结果也都是可以预知的, 那么会将出问题的概率最大可能的降低, 即使出了问题, 也容易跟踪.

上面描述的并不是这份文档中有关C++的所有内容, 只不过我觉得这些更加有同感些, 详细的内容, 可以参看这份文档.都知道google的作品,质量有保证, 除了人的素质确实高之外, 有规范的制度保证也是重要的原因, 毕竟只要是人就会犯错, 为了最大限度的避免人犯错, 有一份详尽的代码规范, 写好哪些该做哪些不该做哪些不该这么做, 也是制度上的保证.另外, 假如每个人都能以一个比较高的标准要求自己所写的代码, 久而久之, 获得进步也是必然的结果.

从这套规范里面, 我的另一个感悟是, 不论是什么行业, "学会如何正确的做事情", 都是十分必要的.这个"正确的做事情", 具体到编码来说, 就是代码规范里面提到的那些要求.而除去编码, 做任何的事情, 使用正确的方式做事, 都是尽可能少的避免错误的方法.但是, "错"与"对"是相对而言的, 没有之前"错"的经历, 就不好体会什么叫"对".所以, "如何正确的做事", 说到了最后, 还得看个人的经验积累, 有了之前"错误"的经历,才能吃一堑长一智, "错误"并不是一无是处的, 只不过, 并不是谁都去尝试着从中学习.