1. 程式人生 > >解讀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 explicitfor 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的作品,質量有保證, 除了人的素質確實高之外, 有規範的制度保證也是重要的原因, 畢竟只要是人就會犯錯, 為了最大限度的避免人犯錯, 有一份詳盡的程式碼規範, 寫好哪些該做哪些不該做哪些不該這麼做, 也是制度上的保證.另外, 假如每個人都能以一個比較高的標準要求自己所寫的程式碼, 久而久之, 獲得進步也是必然的結果.

從這套規範裡面, 我的另一個感悟是, 不論是什麼行業, "學會如何正確的做事情", 都是十分必要的.這個"正確的做事情", 具體到編碼來說, 就是程式碼規範裡面提到的那些要求.而除去編碼, 做任何的事情, 使用正確的方式做事, 都是儘可能少的避免錯誤的方法.但是, "錯"與"對"是相對而言的, 沒有之前"錯"的經歷, 就不好體會什麼叫"對".所以, "如何正確的做事", 說到了最後, 還得看個人的經驗積累, 有了之前"錯誤"的經歷,才能吃一塹長一智, "錯誤"並不是一無是處的, 只不過, 並不是誰都去嘗試著從中學習.