GoogleCpp風格指南 5) 其他特性_part2

5.10 前置自增和自減 Preincrement and Predecrement

Tip 對於迭代器iterator和其他模板對象template object使用前綴形式(++i)的自增, 自減運算符;

定義: 

對於變量在自增(++i 或 i++)或自減(--i 或 i--)後, 表達式的值沒有被用到的情況下, 需要確定到底是使用前置還是後置的自增(自減);

優點: 

不考慮返回值的話, 前置pre自增(++i)通常要比後置post自增(i++)效率更高; 因爲後置自增(自減)需要對錶達式的值 i 進行一次拷貝; 如果i是迭代器或其他非數值 non-scalar類型, 拷貝的代價是比較大的; 既然兩種自增方式實現的功能一樣, 爲什麼不總是使用前置自增呢?

缺點: 

在C開發中, 當表達式的值未被使用時, 傳統的做法還是使用後置自增, 特別是在 for循環中; 有些人覺得後置自增更加易懂, 因爲這很像自然語言, 主語subject(i)在謂語動詞precede(++)前; 

[C語言中沒有class類型, 基本上POD不必在意前置或後置的效率區別]

結論: 

對簡單數值scalar(非對象non-object), 兩種都無所謂; 對迭代器和模板類型, 使用前置自增(自減);


5.11 const的使用 Use of const

Tip 強烈建議在任何可能的情況下都要使用 const; [Add] c++11中, constexpr對於某些const使用情況是更好的選擇; [http://en.cppreference.com/w/cpp/language/constexpr] <<<

定義: 

在聲明的變量或參數前preceded加上關鍵字 const用於指明變量值不可被篡改(如 const int foo); 爲類中的函數加上 const限定符qualifier表明該函數不會修改類成員變量的狀態(如 class Foo { int Bar(char c) cosnt; }; )

優點: 

大家更容易理解如何使用變量; 編譯器可以更好地進行類型檢測; 相應地, 也能生成更好的代碼; 人們對編寫正確的代碼更加自信, 因爲他們知道所調用的函數被限定了能或不能修改變量值; 即使是在無鎖的多線程編程中without locks in multi-threaded, 人們也知道什麼樣的函數是安全的;

缺點: 

const是入侵性viral的: 如果你向一個函數傳入 const變量, 函數原型聲明中也必須對應 const參數(否則變量需要 const_cast類型轉換), 在調用庫函數是顯得尤其麻煩;

結論:

const變量, 數據成員, 函數和參數爲編譯時類型檢測增加了一層保障: 便於儘早發現錯誤; 因此, 我們強烈建議在任何可能的情況下使用 const;

- 如果函數不會修改傳入的引用或指針類型參數, 該參數應聲明爲const;

- 儘可能將函數聲明爲const; 訪問函數幾乎總是const; 其他不會修改任何數據成員, 沒有調用非const函數, 不會返回數據成員非const指針或引用的函數也應聲明成const;

- 如果數據成員在對象構造之後不再發生變化, 可將其定義爲const;

[Remove] 然而, 也不要發瘋似的使用const; 像 const int* const* const x; 就有些過了, 雖然它非常精確地描述了常量 x; 關注真正有幫助一樣的信息: 前面的例子寫成 const int** x就夠了; [內容不可變] <<<

關鍵字 mutable可以使用, 但是在多線程中是不安全的, 使用時首先要考慮線程安全性;


const的位置 Where to put the const:

有人喜歡 int const *foo形式, 不喜歡 const int* foo; 他們認爲前者更一致因此可讀性也更好: 遵循了const總位於其描述的對象之後的原則; 但是一致性原則不適用於此, 由於多數const表達式只有一個const, 而且應用的是一個值, 很少有深層嵌套的指針表達式few deeply-nested pointer expressions; "不要過度使用"的聲明可以取消大部分你原本想保持的一致性; 將const放在前面才更易讀, 因爲在自然語言中形容詞adjective(const)是在名詞noun(int)之前;

這是說, 我們提倡但不強制const在前; 但要保持代碼的一致性; (譯註, 就是不要在一些地方把const寫在類型前面, 在其他地方又寫在後面, 要確定一種寫法, 然後保持一致);


[Add]

使用constexpr Use of constexpr

C++11中, 使用constexpr來定義true的常量或確保常量初始化constant initialization; 

定義:

一些變量可以被聲明爲constexpr, 表明變量是true的常量, e.g. 在編譯/鏈接時是固定的; 一些函數和cotr可以被聲明爲constexpr, 讓它們可以在定義一個constexpr變量時被使用;

優點:

使用constexpr定義浮點數floating-point表達式常量, 而不是字面量literal定義, 用戶定義的類型的定義和函數調用function call的常量的定義;

缺點:

把某些東西定義爲constexpr可能會在之後導致一些遷移migration問題, 或許不得不回退回去downgraded; 目前的對於constexpr函數和ctor的限制規定restriction可能會在這些定義中產生些隱晦的替代方案workaround;

結論:

constexpr定義可以在一些接口的const部分給出健壯的規格robust specification; 使用constexpr來指定真實常量true constants以及支持函數的定義; 使用constexpr來防止複雜的函數定義; 不要使用constexpr來強制內聯 inline; [http://stackoverflow.com/questions/14391272/does-constexpr-imply-inline ]

<<<


5.12 整型 Integer Types

Tip C++內建整型中, 僅使用 int; 如果程序中需要不同大小的變量, 可以使用 <stdint.h>中長度精確precise-width的整型, 如 int16_t; [http://www.cplusplus.com/reference/cstdint/ ]

[Add] 如果你的變量表示了一個可以變大或者等於2^31(2GiB)的值, 使用一個64-bit類型, 比如int64_t; 記住即使你的值對於int來說不會過大, 它還是可能在一些中間計算中需要一個更大的類型; 如果不確定, 就選用更大的類型; <<<

定義: 

C++沒有指定整型的大小; 通常人們假定 short是16位, int是32位, long是32位, long long是64位; (bits)

優點: 

保持聲明統一性Uniformity;

缺點: 

C++中整型大小因編譯器和體系結構architecture的不同而不同;

結論:

<stdint.h>定義了 int16_t, uint32_t, int64_t等整型, 在需要確保guarantee 整型大小時可以優先preference使用它們代替 short, unsigned long long等; 在C整型中, 只使用 int; 在合適的情況下, 推薦使用標準類型如 size_tptrdiff_t;

如果已知整數不會太大, 我們常常會使用 int, 如循環計數loop counter; 在類似的情況下使用原生類型plain old int; 你可以認爲int至少爲32位, 但不要認爲它會多於32位; 如果需要64位整型, 用 int64_t或 uint64_t;

對於大整數, 使用 int64_t;

不要使用 uint32_t等無符號整型, 除非有正當valid 理由, 比如在表示一個位組bit pattern而不是一個數值, 或是需要定義二進制補碼溢出overflow modulo 2^N; 尤其是不要爲了指出數值永遠不會爲負, 而使用無符號類型; 相反, 你應該用斷言來保護數據;

<<<

[Add] 如果你的代碼是一個返回大小的容器, 確保使用一個可適應accommodate任何使用的可能性的數據類型; 不確定的時候使用一個更大的類型而不是較小的類型;

當轉化整型的時候要小心; 整型轉換和提升promotion會引起一些反直覺non-intuitive的行爲; 

<<<


關於無符號整數 On Unsigned Integers:

有些人, 包括一些教科書作者, 推薦使用無符號整型表示非負數; 這種做法試圖達到自我文檔化self-documentation; 但是在C語言中, 這一優點被由其導致的bug所淹沒outweighed; 看看下面的例子:

1
for (unsigned int i = foo.Length()-1; i >= 0; --i) //...

上述循環永遠不會退出! 有時gcc會發現該bug並報警, 但大部分情況下都不會; 類似的bug還會出現在比較有符號變量和無符號變量時; 主要是C的類型提升機制會導致無符號類型的行爲出乎你的意料; 

因此, 使用斷言來指出變量爲非負數, 而不要使用無符號類型;


5.13 64位下的可移植性 64-bit Portability

Tip 代碼應該對64位和32位系統友好; 處理打印printing, 比較comparison, 結構體對齊structure alignment時應該切記;

- 對於某些類型, printf()的指示符在32位和64位系統上可移植性不是很好; C99標準定義了一些可移植的格式化指示符; 不幸的是, MSVC7.1並非全部支持; 而且標準中也有所遺漏, 所以有時我們不得不自己定義一個醜陋的版本(頭文件inttype.h仿標準風格):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// printf macros for size_t, in the style of inttypes.h
#ifdef _LP64
#define __PRIS_PREFIX "z"
#else
#define __PRIS_PREFIX
#endif
 
// Use these macros after a % in a printf format string
// to get correct 32/64 bit behavior, like this:
// size_t size = records.size();
// printf("%"PRIuS"\n", size);
#define PRIdS __PRIS_PREFIX "d"
#define PRIxS __PRIS_PREFIX "x"
#define PRIuS __PRIS_PREFIX "u"
#define PRIXS __PRIS_PREFIX "X"
#define PRIoS __PRIS_PREFIX "o"

check table:

Type DO NOT use DO use Notes
void * (or any pointer) %lx             %p              
int64_t             %qd%lld             %"PRId64"              
uint64_t             %qu%llu%llx             %"PRIu64"%"PRIx64"              
size_t             %u             %"PRIuS"%"PRIxS"             C99 specifies %zu            
ptrdiff_t             %d             %"PRIdS"             C99 specifies %td            


Note PRI*宏會被編譯器擴展concatenated爲獨立字符串; 因此如果使用非常量的格式化字符串, 需要將宏的值而不是宏名插入格式中; 使用PRI*宏同樣可以在 %後包含長度指示符,etc; 例如: printf("x = %30"PRIuS"\n", x); 在32位Linux將被展開爲: printf("x = %30" "u" "\n", x); 編譯器當成: printf("x = %30u\n", x);處理; (譯註: 這在MSVC6.0上不行, VC6編譯器不會自動把引號間隔的多個字符串連接成一個長字符串);

- 記住 sizeof(void *) != sizeof(int); 如果需要一個指針大小的整數要用 intptr_t; [不同編譯器, 系統不一樣]

- 要非常小心地對待結構體對齊, 尤其是要持久化存儲到磁盤上的結構體; (譯註: 持久化--將數據按字節流順序保存在磁盤文件或數據庫中); 在64位系統中, 任何含有 int64_t/uint64_t成員的類/結構體, 缺省都以8字節在結尾對齊; 如果32位和64位代碼要共用持久化在磁盤上的結構體; 需要確保兩種體系結構下的結構體對齊一致;(packed) 大多數編譯器都允許調整結構體對齊; gcc中可使用 __attribute__((packed)); MSVC則提供了 #pragma pack()和 __deslspec(align());

(譯註: 解決方案的項目屬性欄裏也可以直接設置) [VS的項目--solution] 

[http://en.wikipedia.org/wiki/Data_structure_alignment ]

- 創建64位常量時使用 LL或 ULL作爲後綴suffixes, 如:

1
2
int64_t my_value = 0x123456789LL;
uint64_t my_mask = 3ULL << 48;

- 如果你確實需要32位和64位系統具有不同代碼, 可以使用 #ifdef _LP64指令在代碼變量中區分 32/64位代碼; (儘量不要這麼做, 如果非用不可, 儘量使修改局部化);

[http://stackoverflow.com/questions/685124/how-to-identify-a-64-bit-build-on-linux-using-the-preprocessor 

1
2
3
#if defined(__LP64__) || defined(_LP64)
#define BUILD_64   1
#endif

]


5.14 預處理宏 Preprocessor Macros

Tip 使用宏時要非常謹慎, 儘量以內聯函數, 枚舉和常量替代之;

宏意味着你和編譯器看到的代碼是不同的; 這可能會導致異常行爲, 尤其因爲宏具有全局作用域

值得慶幸的是, C++中, 宏不像在C中那麼必不可少; 以往用宏展開性能關鍵performance-critical的代碼, 現在可以用內聯函數替代; 用宏表示的常量可以被 const變量代替; 用宏"縮寫"長變量的別名abbreviate可被引用代替reference; [以及typedef]; 用宏進行條件編譯...這個, 千萬別這麼做, (#define防止頭文件重複包含當然是個特例) 會令測試更加痛苦;

宏可以做一些其他技術無法實現的事情, 在一些代碼庫(尤其是底層庫中)可以看到宏的某些特性(如用#字符串化stringifying, 用##連接concatenation等); 但在使用前, 仔細考慮一下能不能不使用宏達到同樣的目的; [Hack: private和public]

下面給出的用法模式可以避免使用宏帶來的問題; 如果要用宏, 儘可能遵守:

- 不要在 .h文件中定義宏;

- 在馬上要使用時才進行 #define, 使用完要立即 #undefine;

- 不要只是對已經存在的宏使用 #undef, 選擇一個不會衝突的獨特名稱;

- 不要試圖使用展開後會導致C++構造不穩定的宏, 否則至少要附上文檔說明其行爲; [不要在構造相關代碼使用宏?]

- 最好不要使用 ##來產生 function/class/variable的名字;


5.15 0 and nullptr/NULL

Tip 整數用 0, 實數用 0.0, 指針用 NULL或nullptr, 字符chars(串)用 '\0'; 

整數用0, 實數用0.0, 這一點毫無爭議controversial;

對於指針(地址值), 到底是使用0還是NULL/nullptr,

[Remove]Bjarne Stroustrup建議使用最原始的0; 我們建議使用看上去像是指針的 NULL;  [C++11: nullptr]<<<

[Add] 對允許C++11的項目, 使用nullptr, C++03項目使用NULL, 看起來比較像個指針; <<<

事實上一些C++編譯器(如gcc4.1.0)對 NULL進行了特殊的定義, 可以給出有用的警告信息, 尤其是 sizeof(NULL) sizeof(0)不相等的情況;   

字符(串)用 '\0', 不僅類型正確而且可讀性好; [http://bbs.csdn.net/topics/390615761]


5.16 sizeof

Tip 儘可能用 sizeof(varname)代替 sizeof(type);

使用 sizeof(varname)是因爲當代碼中變量類型改變時會自動更新; 某些特定情況下 sizeof(type)或許有意義, 比如管理external或internal數據類型變量的代碼, 而沒有方便的C++類型; 但還是要儘量避免, 因爲它會導致變量類型改變後不能同步;

1
2
Struct data;
memset(&data, 0, sizeof(data));

WARNING 

1
memset(&data, 0, sizeof(Struct));

[Add]

Other

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

<<<


[Add]

auto

使用auto來避免類型名字雜亂clutter; 當有助於可讀性的時候繼續使用明顯的manifest類型聲明, 除了本地local變量之外不要使用auto;

定義:

C++11中, 一個由auto指定類型的變量會給出符合初始化它的表達式的類型; 可以使用auto來用copy初始化initialize它, 或者綁定bind一個引用;

[http://en.cppreference.com/w/cpp/language/auto]

1
2
3
4
vector<string> v;
...
auto s1 = v[0];  // Makes a copy of v[0].
const auto& s2 = v[0];  // s2 is a reference to v[0].

優點:

C++類型名稱有時候很長而且笨重cumbersome, 特別是在模板或名字空間中:

1
sparse_hash_map<string, int>::iterator iter = m.find(val);

返回值很難讀懂, 矇蔽obscure了語句的原本意圖; 改爲:

1
auto iter = m.find(val);

更易讀懂;

沒有auto的話我們有時不得不將一個類型名字在一個表達式內寫兩遍, 對於閱讀者來說沒有意義:

1
diagnostics::ErrorStatus* status = new diagnostics::ErrorStatus("xyz");

使用auto讓中間intermediate變量的使用更合理, 減少了顯式書寫類型的負擔; 

缺點:

有時候變量是manifest的會更清晰, 特別是變量初始化依賴於我們之前聲明的東西; 像這樣的表達式:

1
auto i = x.Lookup(key);

如果x是幾百行之前聲明的, i的類型可能不夠明顯;

程序員不得不去理解auto和const auto&之間的區別, 有時候他們會在沒有意識到的時候拿到一份copy;

auto和C++11 brace-initialization之間的交互interaction可能會令人混淆; 聲明:

1
2
auto x(3);  // Note: parentheses.
auto y{3};  // Note: curly braces.

它們表示不同的東西, x是個int, 但y是一個std::initializer_list<int>; 相同情況會發生在其他普通隱式代理normally-invisible proxy類型上; 

如果一個auto變量被用作接口的一部分, e.g. 在頭文件中作爲一個const, 程序員可能只是爲了改變它的值而改變它的類型, 導致沒有想到的一系列API的徹底radical改變;

結論:

auto只對本地變量開放; 不要在文件範圍file-scopye或名字空間範圍namespace-scope中對變量, 或類成員使用auto; 永遠不要用大括號初始化列表braced initializer list初始化一個auto類型auto-typed變量; 

auto關鍵字也用在C++feature無關的地方: 它作爲一種新的函數聲明的語法的一部分, 尾隨返回值類型 trailing return type; trailing return type只在lambda表達式中被允許使用;


Braced Initializer List

braced initializer lists; [http://en.cppreference.com/w/cpp/language/list_initialization]

在C++03, 聚合類型aggregate type(沒有ctor的數組和結構體)可以用braced initializer list初始化; 

1
2
struct Point { int x; int y; };
Point p = {1, 2};

C++11中, 這個語法被普遍化generalized了, 任何一個對象類型都可以用braced initializer list來初始化, 作爲一個braced-init-list的C++語法grammar; 這裏有幾個例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Vector takes a braced-init-list of elements.
vector<string> v{"foo""bar"};
 
// Basically the same, ignoring some small technicalities.
// You may choose to use either form.
vector<string> v = {"foo""bar"};
 
// Usable with 'new' expressions.
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});

一個用戶定義的類型也可以使用std::initializer_list<T> [http://en.cppreference.com/w/cpp/utility/initializer_list] 定義一個ctor或assignment operator, 會從braced-init-list自動創建;

1
2
3
4
5
6
7
8
9
10
11
12
13
class MyType {
 public:
  // std::initializer_list references the underlying init list.
  // It should be passed by value.
  MyType(std::initializer_list<int> init_list) {
    for (int i : init_list) append(i);
  }
  MyType& operator=(std::initializer_list<int> init_list) {
    clear();
    for (int i : init_list) append(i);
  }
};
MyType m{2, 3, 5, 7};

最後 brace initialization也能調用普通的數據類型的ctor, 即使沒有std::initializer_list<T>構造函數;

1
2
3
4
5
6
7
8
9
10
11
double d{1.23};
// Calls ordinary constructor as long as MyOtherType has no
// std::initializer_list constructor.
class MyOtherType {
 public:
  explicit MyOtherType(string);
  MyOtherType(int, string);
};
MyOtherType m = {1, "b"};
// If the constructor is explicit, you can't use the "= {}" form.
MyOtherType m{"b"};

Note 永遠不要把一個braced-init-list分配給一個auto的本地變量; 在單個元素的case中, 其意義可能會混淆:

Bad:

1
auto d = {1.23};        // d is a std::initializer_list<double>

Good:

1
auto d = double{1.23};  // Good -- d is a double, not a std::initializer_list.

參見 Braced Initializer List Format;

<<<

---TBC---YCR

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章