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的作品,質量有保證, 除了人的素質確實高之外, 有規范的制度保證也是重要的原因, 畢竟只要是人就會犯錯, 為了最大限度的避免人犯錯, 有一份詳盡的代碼規范, 寫好哪些該做哪些不該做哪些不該這么做, 也是制度上的保證.另外, 假如每個人都能以一個比較高的標準要求自己所寫的代碼, 久而久之, 獲得進步也是必然的結果.
從這套規范里面, 我的另一個感悟是, 不論是什么行業, "學會如何正確的做事情", 都是十分必要的.這個"正確的做事情", 具體到編碼來說, 就是代碼規范里面提到的那些要求.而除去編碼, 做任何的事情, 使用正確的方式做事, 都是盡可能少的避免錯誤的方法.但是, "錯"與"對"是相對而言的, 沒有之前"錯"的經歷, 就不好體會什么叫"對".所以, "如何正確的做事", 說到了最后, 還得看個人的經驗積累, 有了之前"錯誤"的經歷,才能吃一塹長一智, "錯誤"并不是一無是處的, 只不過, 并不是誰都去嘗試著從中學習.