C++是一門足夠復(fù)雜的語言.說它"足夠復(fù)雜",是因為C++提供了足夠多編程范式--泛型, 模板, 面向?qū)ο? 異常,等等.順便說說,我已經(jīng)很久沒有跟進C++的最新發(fā)展了(比如C++0x), 所以前面列舉出來的特性應(yīng)該只是C++所有特性的一個部分罷了.C++特性過多很難駕馭好C++的原因之一.另一個原因是C++過于"自作聰明",在很多地方悄無聲息的做了很多事情, 比如隱式的類型轉(zhuǎn)換, 重載, 模板推導(dǎo)等等.而很多時候,這些動作難以察覺,有時候會在你意想不到的地方發(fā)生,即使是熟練的C++程序員也難免被誤傷.(關(guān)于了解C++編譯器自作聰明做了哪些事情, <<深入理解C++物件模型>>是不錯的選擇).
世界上有很多問題, 人們知道如何去解決.但是, 似乎這還不算是最高明的,更高明的做法是學(xué)會避免問題的發(fā)生.而如何避免問題的發(fā)生, 需要經(jīng)驗的積累--曾經(jīng)犯下錯誤,吃一塹長一智,于是知道哪些事情是不該做的或者是不應(yīng)該這么做的.
google C++ code style是google對外公布的一份google內(nèi)部編寫C++的代碼規(guī)范文檔.與其他很多我曾經(jīng)看過的編碼文檔一樣,里面有一些關(guān)于代碼風(fēng)格的規(guī)定,也就是代碼的外觀,這一部分不在這里過多討論,畢竟代碼如何才叫"美觀"是一個見仁見智的話題.在這里專門討論這份文檔中對一些C++特性該如何使用的討論,最后再做一個總結(jié).注意其中的序號并不是文檔中的序號,如果要詳細(xì)了解,可以自己去看這份文檔.
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等)數(shù)據(jù)才行.因為C++標(biāo)準(zhǔn)中沒有明確規(guī)定全局對象的初始化順序, 假設(shè)全局類對象A,B,其中A的初始化依賴于B的值, 那么將無法保證最后的結(jié)果.如果非要使用全局類對象, 那么只能使用指針, 在main等函數(shù)入口統(tǒng)一進行初始化.
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.
文檔規(guī)定, 在類構(gòu)造函數(shù)中對類成員對象做基本的初始化操作, 所有的復(fù)雜初始化操作集中一個比如Init()的函數(shù)中,理由如下:
- 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.
簡單的概括起來也就是:構(gòu)造函數(shù)沒有返回值, 難以讓使用者感知錯誤;假如在構(gòu)造函數(shù)中調(diào)用虛擬函數(shù), 則無法按照使用者的想法調(diào)用到對應(yīng)子類中實現(xiàn)的虛擬函數(shù)(理由是構(gòu)造函數(shù)還未完成意味著這個對象還沒有被成功構(gòu)造完成).
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.
當(dāng)程序員沒有為類編寫一個默認(rèn)構(gòu)造函數(shù)的時候, 編譯器會自動生成一個默認(rèn)構(gòu)造函數(shù),而這個編譯器生成的函數(shù)如何實現(xiàn)(比如如何初始化類成員對象)是不確定的.這樣,假如出現(xiàn)問題時將給調(diào)試跟蹤帶來困難.所以, 規(guī)范要求每個類都需要編寫一個默認(rèn)構(gòu)造函數(shù)避免這種情況的出現(xiàn).
4) Explicit Constructors
Use the C++ keyword explicit for constructors with one argument.
假如構(gòu)造函數(shù)只有一個參數(shù), 使用explicit避免隱式轉(zhuǎn)換,
因為隱式轉(zhuǎn)換可能在你并不需要的時候出現(xiàn).
5) Copy Constructors
Provide a copy constructor and assignment operator only when necessary. Otherwise, disable them with DISALLOW_COPY_AND_ASSIGN.
只有當(dāng)必要的時候才需要定義拷貝構(gòu)造函數(shù)和賦值操作符.
同上一條理由一樣, 避免一些隱式的轉(zhuǎn)換.另一條理由是,"="難以跟蹤,如果真的要實現(xiàn)類似的功能,可以提供比如名為Copy()的函數(shù),這樣子一目了然,不會像賦值操作符那樣可能在每個"="出現(xiàn)的地方出現(xiàn).
6) Operator Overloading
Do not overload operators except in rare, special circumstances.
不要重載操作符.同樣, 也是避免莫名其妙的調(diào)用了一些函數(shù).同上一條一樣, 比如要提供對"=="的重載, 可以提供一個名為Equal()的函數(shù), 如果需要提供對"+"的重載, 可以提供一個名為Add()的函數(shù).
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.
只有在不同的類型表示同樣的信息的時候, 可以使用重載函數(shù).其他情況下,一律不能使用.使用重載, 也可能出現(xiàn)一些隱式出現(xiàn)的轉(zhuǎn)換.所以, 在需要對不同函數(shù)進行同樣操作的時候, 可以在函數(shù)名稱上進行區(qū)分, 而不是使用重載,如可以提供針對string類型的AppendString()函數(shù), 針對int類型的AppendInt()函數(shù),而不是對string和int類型重載Append()函數(shù).另一個好處在于, 在閱讀代碼時,通過函數(shù)名稱可以一目了然.
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!
上面提到的理由中, 我認(rèn)為使用異常最大的害處就是:異常的使用導(dǎo)致了程序無法按照代碼所展現(xiàn)的流程去走的, 比如代碼里面寫了步驟一二三,但是假如有異常出現(xiàn), 這就不好預(yù)知代碼真正步進的步驟了, 在出現(xiàn)問題時, 給調(diào)試和跟蹤帶來困難.
另外, 我更喜歡unix API的設(shè)計.熟悉unix編程的人都知道, unix API基本上都遵守下列規(guī)則:
a) 返回0表示成功, 其他(一般是-1)表示失敗.
b) 在失敗時, 可以根據(jù)errno判斷失敗的原因, 這些在man手冊中都是會清楚的描述.
總結(jié)一下, 這份規(guī)范中規(guī)避的C++特性大致分為以下幾類:
a) 避免使用那些沒有確定行為的特性:如全局變量不能是類對象(初始化順序不確定), 不使用編譯器生成的默認(rèn)構(gòu)造函數(shù)(構(gòu)造行為不確定), 異常(代碼走向不確定).
b) 避免使用那些隱式發(fā)生的操作:如聲明單參數(shù)構(gòu)造函數(shù)為explict以避免隱式轉(zhuǎn)換, 不定義拷貝構(gòu)造函數(shù)避免隱式的拷貝行為, 不使用操作符重載避免隱式的轉(zhuǎn)換
c) 對模棱兩可的特性給予明確的規(guī)定:不使用函數(shù)重載而是定義對每個類型明確的函數(shù).
d) 即使出錯了程序也有辦法知道: 比如不能在類構(gòu)造函數(shù)中進行復(fù)雜的構(gòu)造操作, 將這些移動到類Init()的函數(shù)中.
同時, 這份文檔中描述的大部分C++特性, 都是我之前所熟悉的(除了RTTI之外, 不過這里提到它也是要說明不使用它,另外還提到boost, 不過也是說的要對它"有限制"的使用,比如里面的智能指針).可以看到, 面對這樣一門復(fù)雜同時還在不停的發(fā)展更新特性的語言, google的態(tài)度是比較"保守"的.這與我之前對C++的理解也是接近的, 我一直認(rèn)為C++中需要使用到的特性有基本的面向?qū)ο?STL就夠了(經(jīng)過最近的編碼實踐,我認(rèn)為還得加個智能指針).我對這個"保守"態(tài)度的理解是, 以C++當(dāng)前的應(yīng)用場景來看, 這些特性已經(jīng)足夠, 如果使用其他一些更加復(fù)雜的, 對人的要求提高了, 代碼的可讀性以及以后的可維護性就下降了.
前面說過, 避免問題的出現(xiàn)比解決問題來的更加高明些, 而面對C++這一個提供了眾多特性, google C++ code style給予了明確的規(guī)定, 也就是每個行為, 如果都能做到有明確的動作, 同時結(jié)果也都是可以預(yù)知的, 那么會將出問題的概率最大可能的降低, 即使出了問題, 也容易跟蹤.
上面描述的并不是這份文檔中有關(guān)C++的所有內(nèi)容, 只不過我覺得這些更加有同感些, 詳細(xì)的內(nèi)容, 可以參看這份文檔.都知道google的作品,質(zhì)量有保證, 除了人的素質(zhì)確實高之外, 有規(guī)范的制度保證也是重要的原因, 畢竟只要是人就會犯錯, 為了最大限度的避免人犯錯, 有一份詳盡的代碼規(guī)范, 寫好哪些該做哪些不該做哪些不該這么做, 也是制度上的保證.另外, 假如每個人都能以一個比較高的標(biāo)準(zhǔn)要求自己所寫的代碼, 久而久之, 獲得進步也是必然的結(jié)果.
從這套規(guī)范里面, 我的另一個感悟是, 不論是什么行業(yè), "學(xué)會如何正確的做事情", 都是十分必要的.這個"正確的做事情", 具體到編碼來說, 就是代碼規(guī)范里面提到的那些要求.而除去編碼, 做任何的事情, 使用正確的方式做事, 都是盡可能少的避免錯誤的方法.但是, "錯"與"對"是相對而言的, 沒有之前"錯"的經(jīng)歷, 就不好體會什么叫"對".所以, "如何正確的做事", 說到了最后, 還得看個人的經(jīng)驗積累, 有了之前"錯誤"的經(jīng)歷,才能吃一塹長一智, "錯誤"并不是一無是處的, 只不過, 并不是誰都去嘗試著從中學(xué)習(xí).