Google C++ Style Guide在C++11普及后的變化


一般比較規范的項目都有一個代碼規范,Google C++ Style Guide(以下簡稱GCSG)是比較流行的C++代碼規范,為什么我會分析它?因為我們現在就在用。

 

C++代碼規范一般有兩個方向,一個方向是很保守,基本把C++降級回c with classes的年代。我記得前幾年我在某公司某項目中時,曾有領導建議代碼規范中不要使用STL。還有個團隊,老大禁用STL,於是組員把VC的STL代碼扒過來改一下名字,比如vector改為Array,map改為Map,begin改為Begin,然后就允許用了。

另一種是偏前衛的方式,boost的應該算是其中的代表。C++之父搞的C++ Core Guidelines也算是這個流派。

 

GCSG算是這兩者的折中,也就是說,在比較“土”的使用者看來,還是比較時尚的。在比較前衛的使用者看來,卻偏保守,即使在業界其他大公司中算比較保守的,比如和Apple LLVMFacebook比起來。

 

前幾年一個毛子程序員就狠狠地吐槽了一把:Why Google Style Guide for C++ is a deal-breaker,原因是這哥們是boost愛好者,自稱boost programmer,你就知道它屬於什么流派了。他在多達6次收到google招聘人員的聯系后怒了,寫文章發泄了一把,還引起了負責人撕逼:The Philosophy of Google's C++ Code

扯得有點遠了……

 

整體來說,GCSG還算是比較實用的,很詳細,覆蓋面很廣,而且還有一個利器cpplint.py來搭配,方便實時自動檢查。

GCSG的另一個好處是更新很及時,自從2008年以來,已經更新了好幾百次。

C++11剛確定的時候,Google C++ Style Guide就做了更新,當時是這么寫的:

只能使用批准的特性,然后來了一句,“目前,只批准了auto”。 

又過去五年了,語言和編譯器都有了很大的進展,C++14標准發布了,C++17標准也在制定中,gcc/clang等的跟進也都比較及時,所以Google也與時俱進地更新了代碼規范。

上周末在手機上把最新版的GCSG看了一遍,發現還是有不少明顯的變化值得講一下的。

閑話不提,下面逐條分析:

舊條款的更新

前向聲明

前向聲明是指一個使用類型時,如果只需要它的指針,引用,返回值,參數,而沒有實際實例化對象時,可以用 class Foo; 這樣的語法,避免包含其定義所在的頭文件。

舊的代碼規范里,鼓勵使用前向聲明,好處是減少依賴加快編譯速度,減少代碼修改時的重新編譯,隱藏實現細節。

在新的規范里,已經改為了盡量避免使用前向聲明,需要時直接包含其定義的頭文件即可。原因:

  • 容易出現不一致,比如引發錯誤
    •  // b.h:

      struct B {};
      struct D : B {};
      
      // good_user.cc:
      #include "b.h"
      void f(B*);
      void f(void*);
      void test(D* x) { f(x); }  // calls f(B*)
  • 妨礙接口升級,比如一個類,原來覺的名字不好或者命名空間不恰當,改為了新名字或者換了命名空間,如果代碼中用了前向聲明,就需要修改所有用到的地方。而如果不用,就可以用typedef或者using之類的方式兼容舊代碼。
  • // 舊代碼:
    class OldClassName {};
    
    // 舊代碼:
    class NewClassName {};
    
    __attribute__((__deprecated("這個類名廢棄了,請改用NewClassName")))
    typedef NewClassName OldClassName; // 兼容舊代碼
  • 對性能有一定的影響。比如類成員本來可以用的對象的,必須用指針,不可避免的帶來動態內存分配開銷。

 

前向聲明在有些時候還是不可避免的,比如兩個類相互引用時。

 

這個改變,除了正確性問題外,還估計跟其分布式編譯越來越快有關。

 

這個問題上,我個人的實踐是

  • 一個項目內,可以使用前向聲明,跨項目的別人的庫,就要避免前向聲明,直接包含頭文件。
  • 值類型,特別是在運行時構造很頻繁類型,不采用前向聲明,比較重而復雜的類,需要對外屏蔽實現細節時,才考慮使用前向聲明。
  • 使用前向聲明隱藏實現時,用pimpl方式,比各個成員都用性能上會好一些。
// 傳統方式:
// my_class.h
// 兩次內存分配,引入了兩個不完整類型Foo, Bar。
class Foo;
class Bar;

class MyClass {
 public:
  MyClass();
 private:
  Foo* foo_;
  Bar* bar_;
};


// ===================================
// pimpl方式
// 一次內存分配,不引入任何無關符號。
// my_class.h
class MyClass {
 public:
  MyClass();
 private:
  struct Impl;
  std::unique_ptr<Impl> impl_;
};

// my_class.cc
#include "foo.h"
#include "bar.h"

struct MyClass::Impl {
  Foo foo;
  Bar bar;
};

MyClass::MyClass() : impl_(new Impl) {}

-inl.h

舊的規范中,當定義復雜的inline函數或者函數模版時,鼓勵把這部分代碼從頭文件中提取出來,放到單獨的filename-inl.h中。這個實踐在過去很常見,現在不允許了。

嵌套類

舊規范中禁止,新規范中取消了,估計跟不再鼓勵前向聲明有關,因為嵌套類不能前向聲明。

嵌套類可以使接口定義層次化,減少不必要的關注點。

函數重載

舊規范中不鼓勵函數重載,要求函數行為相同時才允許重載,新規范中有所放寬,只要讀代碼時能看比較容易看出調了那個函數,就允許重載。

這個要求依然比較保守,畢竟構造函數天然都是重載的。

默認參數

舊規范中,禁止使用默認參數。新規范中,除了虛函數外,允許使用默認參數,何時使用的決策原則和函數重載的原則一樣。

運算符重載

舊規范中,禁止重載運算符;新規范中,改為了“審慎地”重載運算符。

運算符重載是C++中比較有特色的部分,一棒子打死顯然是過於保守的。

指針和引用的選擇

當一個常量可以是引用也可以是指針時,如何選擇,舊規范中提,新規范中作了規定,這些情況下用指針更合適:

  • 當參數可以是null時
  • 當參數在函數內會被保存下來以后用時

在舊規范中,禁止使用流(iostream),說法是保持一致,只用FILE/printf。新規范中允許了:“恰當地”使用流,保持簡單的方式使用。

所謂簡單的方式使用,就是涉及復雜格式控制時最好不要用,因為不但代碼更啰嗦,還會改變流的狀態。

枚舉值命名

最早的規范規定枚舉值采用全大些下划線分割的方式,2009年以后改為了k開頭,跟大小寫混合的方式,以和宏名做區分。

關於C++11的新條款

auto類型

auto使代碼更清晰時,局部變量鼓勵使用auto,比如迭代器:

// C++03 Style
for (std::map<int, std::string>::iterator i = m.begin(); i != m.end(); ++i) {
  std::cout << i->second;
}

// C++11 Style
for (auto i = m.begin(); i != m.end(); ++i) {
  std::cout << i->second;
}

 

尤其是當訪問map時,特別推薦用auto:

for (const auto& item : some_map) {
  const KeyType& key = item.first;
  const ValType& value = item.second;
  // The rest of the loop can now just refer to key and value,
  // a reader can see the types in question, and we've avoided
  // the too-common case of extra copies in this iteration.
}

 因為很多人可能不知道map的value_type是std::pair<const KeyType, MappedType>而不是std::pair<KeyType, MappedType>,但是當你用后者時,編譯是能通過的,因為存在隱式構造類型轉換。但是轉換后的對象就不再是map中存的那個。

新的函數定義語法

C++11引入了一種新的函數定義語法

auto foo() -> int {
  return 0;
}

規范規定,只有必須使用這種語法才行時,才能用,常規情況下還是要使用普通的方式。

template <typename T, typename U>
<這里填什么類型合適呢??> add(T t, U u) { return t + u; }

// 新語法解決了這個問題
template <typename T, typename U>
auto add(T t, U u) -> decltype(t+u) {
return t + u;
}

右值引用

右值引用允許用於移動構造函數和移動賦值函數以及完美轉發。

C++03中,對象的拷貝構造函數可能是個很大的開銷,代碼風格不鼓勵返回復雜對象。比如

std::vector<int> foo() {
  std::vector<int> v;
  ...
  return v;
}

std::vector<int> v = foo();

盡管編譯器普遍支持(匿名和命名的)返回值優化,但是還是有很多時候這種拷貝不可消除。C++11引入了右值引用,函數重載時,臨時對象優先匹配綁右值引用的版本,這樣的函數知道其參數是臨時對象,就可以把其資源直接“移動”過來,避免拷貝。

C++標准庫組件比如string和stl容器,大范圍支持了基於右值引用的移動構造和賦值,即使你自己的代碼沒有對右值引用做任何處理,很多涉及這些對象拷貝和賦值的場景也自動得到了優化。

如果掌握了右值引用,這條規范就允許你針對自定義類做移動構造和賦值優化,從而進一步提高代碼性能。

 

大括號初始化語法

鼓勵使用,能簡化代碼

auto p = new vector<string>{"foo", "bar"};

// A map can take a list of pairs. Nested braced-init-lists work.
map<int, string> m = {{1, "one"}, {2, "2"}};

// A braced-init-list can be implicitly converted to a return type.
vector<int> test_function() { return {1, 2, 3}; }

// Iterate over a braced-init-list.
for (int i : {-1, -2, -3}) {}

// Call a function using a braced-init-list.
void TestFunction2(vector<int> v) {}
TestFunction2({1, 2, 3});
A user-defined type can also define a constructor and/or assignment operator that take std::initializer_list<T>, which is automatically created from braced-init-list:

 

constexpr

鼓勵使用

在C++中,const關鍵字實際有兩種含義:

const int N = 100;    // 編譯期間常量,可以做數組緯度,可以做模板非類型參數。
const int N = rand(); // 運行期常量,不能進行上述用途,只能保證不能被修改。
int a[N];             // 第一種定義OK,第二種編譯出錯。

C++11中,引入了constexpr關鍵字,用來定義“真正”的常量,可以確保是編譯期間就能確定的。

在C++03中,const能用於函數,但是返回的不是編譯期常量。

const int size() {
  return 1000;
}
const int Size = size(); // Size不是編譯期常量

constexpr不但可以用於常量,還能用於函數。

constexpr int size() {
  return 1000;
}
const int Size = size(); // Size是編譯期常量
int a[Size]; // OK

 

nullptr

鼓勵使用nullptr代替NULL

好處:有類型,可重載。

這段代碼在C++03中會引發編譯錯誤,因為NULL實際定義為0(gcc的NULL定義為(__null),不影響這里的行為)。

void f(int);
void f(void*);
f(NULL);

C++11中,用nullptr就沒有歧義

f(nullptr); // 調 f(void*)

由於nullptr是有類型的,在某些情況下用於重載:

template <template T>
class shared_ptr {
 public:
  constexpr shared_ptr(std::nullptr_t);
  explicit shared_ptr(T*);
};

constexpr shared_ptr<int> EMPTY(nullptr);

 

 

sizeof

舊規范中說,盡量用sizeof(變量名)而不是sizeof(類型名),因為這樣當類型改變時,可以避免改動多處。新規范對此作了進一步的完善,規定當代碼跟具體的某個變量無關的場合時,還是要用sizeof(類型的):

if (raw_size < sizeof(int)) {
  LOG(ERROR) << "compressed record not big enough for count: " << raw_size;
  return false;
}

 

lambda表達式

恰當使用lambda表達式。lambda在結構復雜的代碼中,可以減少回調函數的定義,使STL中基於謂詞的各種算法(foreach, find_if等)真正好用起來。

 

override關鍵字

鼓勵使用

用C++的人都遇到過這種情況,基類定義了一個虛函數,我們在派生類中覆蓋了這個虛函數,結果由於失誤,簽名沒弄對

class Shape {
 public:
  virtual void Rotate(double radians) = 0;
};

class Circle : public Shape {
 public:
  virtual void Rotate(float radians);    // 錯誤情況1:類型搞錯了
  virtual void Rotete(double radians);   // 錯誤情況2:函數名寫錯了
};

這兩種錯誤情況都會導致虛函數沒有真正被覆蓋,運行時才能發現。

針對第一種情況,gcc有個編譯警告選項,-Woverride-virtual,能發現大多數錯誤。

第二中情況就麻煩了,畢竟編譯器不會替我們做拼寫檢查,一種不完善的方案是,在基類中把虛函數聲明成純虛函數,但是不是所有的場合都適合把虛函數定義為純虛函數。

C++11引入了override關鍵字,來解決這個問題:

class Circle : public Shape {
 public:
  void Rotate(double radians) override;
};

override關鍵字表明這個函數是覆蓋基類中同簽名的函數,如果基類中不存在同簽名的函數,編譯期間就會報錯。

總結

GCSG對C++11的新特性做了不少有價值的分析,完善了新的規范,另外隨着C++語言新風格的普及,保守程度也下降了不少。

整體看來,GCSG是一個成熟,比較靠譜,容易實施,與時俱進的代碼規范,還是很實用的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM