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!