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