LLVM 編碼標準

簡介

本文檔描述了 LLVM 專案中使用的編碼標準。儘管不應將任何編碼標準視為在所有情況下都必須遵守的絕對要求,但對於遵循基於函式庫設計(如 LLVM)的大型程式碼庫而言,編碼標準尤其重要。

雖然本文檔可能為某些機械式格式化問題、空白字元或其他 “微觀細節” 提供指導,但這些並非固定的標準。始終遵循黃金法則

如果您正在擴展、增強或修正已實作的程式碼,請使用已使用的風格,以使原始碼統一且易於追蹤。

請注意,某些程式碼庫(例如 libc++)有特殊原因偏離編碼標準。例如,在 libc++ 的情況下,這是因為命名和其他約定是由 C++ 標準決定的。

程式碼庫中存在一些未統一遵循的約定(例如,命名約定)。這是因為它們相對較新,並且許多程式碼是在它們到位之前編寫的。我們的長期目標是讓整個程式碼庫都遵循約定,但我們明確地希望修補程式對現有程式碼進行大規模的重新格式化。另一方面,如果您要以其他方式更改類別的方法,則重新命名這些方法是合理的。請單獨提交此類變更,以使程式碼審查更容易。

這些指南的最終目標是提高我們通用原始碼庫的可讀性和可維護性。

語言、函式庫與標準

LLVM 和其他使用這些編碼標準的 LLVM 專案中的大多數原始碼都是 C++ 程式碼。在某些地方使用了 C 程式碼,這可能是由於環境限制、歷史限制或匯入到樹狀結構中的第三方原始碼所致。一般而言,我們的偏好是符合標準、現代且可移植的 C++ 程式碼作為首選的實作語言。

對於自動化、建置系統和實用工具腳本,Python 是首選,並且已在 LLVM 儲存庫中廣泛使用。

C++ 標準版本

除非另有文件說明,否則 LLVM 子專案是使用標準 C++17 程式碼編寫的,並避免不必要的供應商特定擴充功能。

儘管如此,我們將自己限制在主要工具鏈中可用的功能,這些工具鏈被支援作為主機編譯器(請參閱LLVM 系統入門頁面中的 軟體 章節)。

每個工具鏈都提供了對其接受內容的良好參考

此外,在 cppreference.com 上有支援的 C++ 功能的編譯器比較表。

C++ 標準函式庫

當 C++ 標準函式庫或 LLVM 支援函式庫可用於特定任務時,我們鼓勵使用它們,而不是實作自訂資料結構。LLVM 和相關專案盡可能地強調和依賴標準函式庫設施和 LLVM 支援函式庫。

LLVM 支援函式庫(例如,ADT)實作了標準函式庫中缺少的專用資料結構或功能。此類函式庫通常在 llvm 命名空間中實作,並遵循預期的標準介面(如果有的話)。

當 C++ 和 LLVM 支援函式庫都提供類似的功能,並且沒有特定理由偏好 C++ 實作時,通常最好使用 LLVM 函式庫。例如,幾乎總是應該使用 llvm::DenseMap 而不是 std::mapstd::unordered_map,並且通常應該使用 llvm::SmallVector 而不是 std::vector

我們明確避免使用某些標準設施,例如 I/O 串流,而是使用 LLVM 的串流函式庫(raw_ostream)。有關這些主題的更多詳細資訊,請參閱 LLVM 程式設計師手冊

有關 LLVM 資料結構及其權衡取捨的更多資訊,請查閱程式設計師手冊的該章節

Python 版本與原始碼格式化

目前所需的最低 Python 版本記錄在 LLVM 系統入門 章節中。LLVM 儲存庫中的 Python 程式碼應僅使用此 Python 版本中可用的語言功能。

LLVM 儲存庫中的 Python 程式碼應遵守 PEP 8 中概述的格式化指南。

為了保持一致性並限制變動,程式碼應使用符合 PEP 8 標準的 black 工具自動格式化。使用其預設規則。例如,即使 --line-length 沒有預設為 80,也請避免指定它。預設規則可能會在 black 的主要版本之間變更。為了避免格式化規則中不必要的變動,我們目前在 LLVM 中使用 black 版本 23.x。

當貢獻與格式化無關的修補程式時,您應僅格式化修補程式修改的 Python 程式碼。為此,請使用 darker 工具,它僅對修改後的 Python 程式碼執行預設的 black 規則。這樣做應確保修補程式將通過 LLVM 預提交 CI 中的 Python 格式檢查,該檢查也使用 darker。當貢獻專門用於重新格式化 Python 檔案的修補程式時,請使用 black,它目前僅支援格式化整個檔案。

以下是一些快速範例,但請參閱 black 和 darker 文件以了解詳細資訊

$ pip install black=='23.*' darker # install black 23.x and darker
$ darker test.py                   # format uncommitted changes
$ darker -r HEAD^ test.py          # also format changes from last commit
$ black test.py                    # format entire file

您可以為 darker 指定目錄而不是個別檔案名稱,它會找到已變更的檔案。但是,如果目錄很大(例如 LLVM 儲存庫的副本),darker 可能會非常慢。在這種情況下,您可能希望使用 git 來列出已變更的檔案。例如

$ darker -r HEAD^ $(git diff --name-only --diff-filter=d HEAD^)

機械式原始碼問題

原始碼格式化

註解

註解對於可讀性和可維護性非常重要。撰寫註解時,請將其寫成英文散文,使用正確的大小寫、標點符號等。目標是描述程式碼試圖做什麼以及為什麼這樣做,而不是微觀層面上的如何做。以下是一些需要記錄的重要事項

檔案標頭

每個原始碼檔案都應在其上包含一個標頭,描述檔案的基本用途。標準標頭如下所示

//===----------------------------------------------------------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.dev.org.tw/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
///
/// \file
/// This file contains the declaration of the Instruction class, which is the
/// base class for all of the VM instructions.
///
//===----------------------------------------------------------------------===//

檔案中的第一個章節是簡潔的註記,定義了檔案發佈時所依據的許可證。這使其非常清楚地表明了原始碼可以在哪些條款下發佈,並且不應以任何方式修改。

主體是一個 Doxygen 註解(由 /// 註解標記而不是常用的 // 標識),描述了檔案的用途。第一句話(或以 \brief 開頭的段落)用作摘要。任何其他資訊都應以空行分隔。如果演算法基於論文或在其他來源中描述,請提供參考。

標頭保護

標頭檔的保護應為使用者將 #include 的全大寫路徑,使用 ‘_’ 而不是路徑分隔符和副檔名標記。例如,標頭檔 llvm/include/llvm/Analysis/Utils/Local.h 將以 #include 的方式包含 #include "llvm/Analysis/Utils/Local.h",因此其保護是 LLVM_ANALYSIS_UTILS_LOCAL_H

類別概述

類別是物件導向設計的基本組成部分。因此,類別定義應具有註解區塊,說明類別的用途和工作方式。每個非平凡的類別都應具有 doxygen 註解區塊。

方法資訊

方法和全域函式也應記錄在案。這裡只需要簡要說明其作用以及邊緣情況的描述。讀者應該能夠在不閱讀程式碼本身的情況下了解如何使用介面。

這裡要討論的好處是當發生意外情況時會發生什麼,例如,方法是否傳回 null?

註解格式化

一般而言,偏好 C++ 風格的註解(// 用於一般註解,/// 用於 doxygen 文件註解)。但在以下幾種情況下,使用 C 風格 (/* */) 註解很有用

  1. 撰寫與 C89 相容的 C 程式碼時。

  2. 撰寫可能被 C 原始碼檔案 #include 的標頭檔時。

  3. 撰寫由僅接受 C 風格註解的工具使用的原始碼檔案時。

  4. 記錄用作呼叫中實際參數的常數的意義時。這對於 bool 參數或傳遞 0nullptr 最有幫助。註解應包含參數名稱,該名稱應具有意義。例如,在以下呼叫中,參數的含義不明確

    Object.emitName(nullptr);
    

    行內 C 風格註解使意圖顯而易見

    Object.emitName(/*Prefix=*/nullptr);
    

不鼓勵註解掉大型程式碼區塊,但如果您真的必須這樣做(為了文件目的或作為偵錯列印的建議),請使用 #if 0#endif。這些可以正確巢狀化,並且通常比 C 風格註解表現更好。

Doxygen 在文件註解中的使用

使用 \file 命令將標準檔案標頭轉換為檔案層級註解。

為所有公開介面(公開類別、成員和非成員函式)包含描述性段落。避免重複可以使用 API 名稱推斷的資訊。第一句話(或以 \brief 開頭的段落)用作摘要。嘗試使用單一句子作為 \brief,因為它會增加視覺混亂感。將詳細討論放在單獨的段落中。

若要在段落內引用參數名稱,請使用 \p name 命令。不要使用 \arg name 命令,因為它會啟動一個新段落,其中包含參數的文件。

將非行內程式碼範例包裝在 \code ... \endcode 中。

若要記錄函式參數,請使用 \param name 命令啟動新段落。如果參數用作 out 或 in/out 參數,請分別使用 \param [out] name\param [in,out] name 命令。

若要描述函式傳回值,請使用 \returns 命令啟動新段落。

最簡化的文件註解

/// Sets the xyzzy property to \p Baz.
void setXyzzy(bool Baz);

以偏好的方式使用所有 Doxygen 功能的文件註解

/// Does foo and bar.
///
/// Does not do foo the usual way if \p Baz is true.
///
/// Typical usage:
/// \code
///   fooBar(false, "quux", Res);
/// \endcode
///
/// \param Quux kind of foo to do.
/// \param [out] Result filled with bar sequence on foo success.
///
/// \returns true on success.
bool fooBar(bool Baz, StringRef Quux, std::vector<int> &Result);

不要在標頭檔和實作檔中重複文件註解。將公開 API 的文件註解放在標頭檔中。私有 API 的文件註解可以放在實作檔中。在任何情況下,實作檔都可以包含其他註解(不一定採用 Doxygen 標記)以根據需要解釋實作細節。

不要在註解開頭重複函式或類別名稱。對於人類來說,很明顯正在記錄哪個函式或類別;自動文件處理工具足夠聰明,可以將註解綁定到正確的宣告。

避免

// Example.h:

// example - Does something important.
void example();

// Example.cpp:

// example - Does something important.
void example() { ... }

偏好

// Example.h:

/// Does something important.
void example();

// Example.cpp:

/// Builds a B-tree in order to do foo.  See paper by...
void example() { ... }

錯誤與警告訊息

清晰的診斷訊息對於幫助使用者識別和修正其輸入中的問題非常重要。使用簡潔但正確的英文散文,為使用者提供了解錯誤原因所需的上下文。此外,為了與其他工具常用的錯誤訊息風格相符,請以小寫字母開始第一句話,並在最後一句話結束時不要加句點(如果它本來會以句點結尾的話)。以不同標點符號結尾的句子,例如 “did you forget ‘;’?”,仍然應該這樣做。

例如,這是一個好的錯誤訊息

error: file.o: section header 3 is corrupt. Size is 10 when it should be 20

這是一個糟糕的訊息,因為它沒有提供有用的資訊,並且使用了錯誤的風格

error: file.o: Corrupt section header.

與其他編碼標準一樣,個別專案(例如 Clang 靜態分析器)可能具有不符合此規範的現有風格。如果整個專案都一致地使用不同的格式化方案,請改用該風格。否則,此標準適用於所有 LLVM 工具,包括 clang、clang-tidy 等等。

如果工具或專案沒有現有的函式來發出警告或錯誤,請使用 Support/WithColor.h 中提供的錯誤和警告處理常式,以確保它們以適當的風格列印,而不是直接列印到 stderr。

使用 report_fatal_error 時,請遵循與一般錯誤訊息相同的訊息標準。斷言訊息和 llvm_unreachable 呼叫不一定需要遵循相同的風格,因為它們會自動格式化,因此這些指南可能不適用。

#include 風格

緊接在標頭檔註解之後(以及如果在標頭檔上工作,則包含保護),應列出檔案所需的最小 #include 列表。我們偏好按以下順序排列這些 #include

  1. 主要模組標頭檔

  2. 本機/私有標頭檔

  3. LLVM 專案/子專案標頭檔 (clang/..., lldb/..., llvm/..., 等等)

  4. 系統 #include

並且每個類別都應按完整路徑依字母順序排序。

主要模組標頭檔適用於實作由 .h 檔案定義的介面的 .cpp 檔案。無論此 #include 位於檔案系統上的哪個位置,都應始終將其首先包含在內。通過在實作介面的 .cpp 檔案中首先包含標頭檔,我們確保標頭沒有任何隱藏的依賴項,這些依賴項未在標頭中明確 #include,但應該包含。它也是 .cpp 檔案中的一種文件形式,用於指示其實作的介面在哪裡定義。

LLVM 專案和子專案標頭檔應從最特定到最不特定分組,原因與上述相同。例如,LLDB 依賴於 clang 和 LLVM,而 clang 依賴於 LLVM。因此,LLDB 原始碼檔案應首先包含 lldb 標頭檔,然後是 clang 標頭檔,然後是 llvm 標頭檔,以減少(例如)LLDB 標頭檔因先前在主要原始碼檔案或某些較早的標頭檔中包含該標頭檔而意外拾取遺失的包含的可能性。clang 應類似地在其自己的標頭檔之前包含 llvm 標頭檔。此規則適用於所有 LLVM 子專案。

原始碼寬度

將您的程式碼寫在 80 個字元寬度內。

程式碼的寬度必須有一些限制,以便開發人員可以在適度的顯示器上的視窗中並排顯示多個檔案。如果您要選擇寬度限制,這在某種程度上是任意的,但您不妨選擇一些標準的東西。使用 90 個字元寬度(例如)而不是 80 個字元寬度不會增加任何顯著價值,並且不利於列印程式碼。此外,許多其他專案已將 80 個字元寬度標準化,因此有些人已經為其配置了編輯器(與其他寬度(例如 90 個字元寬度)相比)。

空白字元

在所有情況下,原始碼檔案中都偏好使用空格而不是 Tab 字元。人們有不同的偏好縮排層級,以及他們喜歡的不同縮排風格;這沒關係。不好的是,不同的編輯器/檢視器將 Tab 字元擴展到不同的 Tab 停靠位。這可能會導致您的程式碼看起來完全無法閱讀,並且不值得處理。

與往常一樣,請遵循上面的黃金法則:如果您正在修改和擴展現有程式碼,請遵循現有程式碼的風格。

不要新增尾隨空白字元。某些常見的編輯器會在儲存檔案時自動移除尾隨空白字元,這會導致不相關的變更出現在差異和提交中。

將 Lambda 函式如同程式碼區塊般格式化

格式化多行 Lambda 函式時,請將其格式化為程式碼區塊。如果語句中只有一個多行 Lambda 函式,並且在語句中沒有詞彙上位於其後的表達式,則將縮排減少到程式碼區塊的標準兩個空格縮排,就好像它是語句的前一部分開啟的 if 區塊一樣

std::sort(foo.begin(), foo.end(), [&](Foo a, Foo b) -> bool {
  if (a.blah < b.blah)
    return true;
  if (a.baz < b.baz)
    return true;
  return a.bam < b.bam;
});

為了充分利用此格式化,如果您正在設計一個接受續傳或單一可呼叫引數(無論是函式物件還是 std::function)的 API,則應盡可能將其作為最後一個引數。

如果語句中有多個多行 Lambda 函式,或者 Lambda 函式之後還有其他參數,請從 [] 的縮排處將區塊縮排兩個空格

dyn_switch(V->stripPointerCasts(),
           [] (PHINode *PN) {
             // process phis...
           },
           [] (SelectInst *SI) {
             // process selects...
           },
           [] (LoadInst *LI) {
             // process loads...
           },
           [] (AllocaInst *AI) {
             // process allocas...
           });
大括號初始化列表

從 C++11 開始,大括號列表在執行初始化方面有更多用途。例如,它們可用於在表達式中建構聚合暫時物件。它們現在有一種自然的方式來最終嵌套在彼此內部以及函式呼叫內部,以便從區域變數建構聚合(例如選項結構)。

歷史上常見的聚合變數大括號初始化格式化與深度嵌套、一般表達式上下文、函式引數和 Lambda 函式不能乾淨地混合使用。我們建議新程式碼使用一個簡單的規則來格式化大括號初始化列表:就像大括號是函式呼叫中的括號一樣。格式化規則與已經很好理解的嵌套函式呼叫格式化規則完全匹配。範例

foo({a, b, c}, {1, 2, 3});

llvm::Constant *Mask[] = {
    llvm::ConstantInt::get(llvm::Type::getInt32Ty(getLLVMContext()), 0),
    llvm::ConstantInt::get(llvm::Type::getInt32Ty(getLLVMContext()), 1),
    llvm::ConstantInt::get(llvm::Type::getInt32Ty(getLLVMContext()), 2)};

這種格式化方案也使得使用 Clang Format 等工具獲得可預測、一致且自動的格式化變得特別容易。

語言與編譯器問題

將編譯器警告視為錯誤

編譯器警告通常很有用,並且有助於改進程式碼。那些沒有用的警告通常可以通過少量程式碼變更來抑制。例如,if 條件中的賦值通常是錯字

if (V = getValue()) {
  ...
}

多個編譯器將為上面的程式碼列印警告。可以通過新增括號來抑制它

if ((V = getValue())) {
  ...
}

撰寫可移植程式碼

在幾乎所有情況下,都可以編寫完全可移植的程式碼。當您需要依賴不可移植的程式碼時,請將其放在明確定義且有良好文件的介面之後。

不要使用 RTTI 或例外

為了努力減少程式碼和可執行檔的大小,LLVM 不使用例外處理或 RTTI(執行時期型別資訊,例如,dynamic_cast<>)。

話雖如此,LLVM 確實廣泛使用手動實作的 RTTI 形式,其使用像是 isa<>、cast<> 和 dyn_cast<> 等樣板。這種形式的 RTTI 是選擇性加入的,並且可以加入到任何類別

偏好 C++ 風格的轉型

當進行轉型時,請使用 static_castreinterpret_castconst_cast,而不是 C 風格的轉型。但有兩個例外情況:

  • 當轉型為 void 以抑制關於未使用變數的警告時(作為 [[maybe_unused]] 的替代方案)。在這種情況下,偏好使用 C 風格的轉型。

  • 當在整數型別(包括非強型別的列舉)之間進行轉型時,允許使用函式風格的轉型作為 static_cast 的替代方案。

不要使用靜態建構子

不應將靜態建構子和解構子(例如,型別具有建構子或解構子的全域變數)添加到程式碼庫中,並且應盡可能移除。

不同原始碼檔案中的全域變數以任意順序初始化,這使得程式碼更難以理解。

靜態建構子對使用 LLVM 作為函式庫的程式的啟動時間有負面影響。我們非常希望將額外的 LLVM 目標或其他函式庫連結到應用程式中時,不會有任何成本,但靜態建構子破壞了這個目標。

使用 classstruct 關鍵字

在 C++ 中,classstruct 關鍵字幾乎可以互換使用。唯一的區別是當它們用於宣告類別時:class 預設將所有成員設為私有,而 struct 預設將所有成員設為公有。

  • 給定 classstruct 的所有宣告和定義都必須使用相同的關鍵字。例如:

// Avoid if `Example` is defined as a struct.
class Example;

// OK.
struct Example;

struct Example { ... };
  • 所有成員都被宣告為公有時,應該使用 struct

// Avoid using `struct` here, use `class` instead.
struct Foo {
private:
  int Data;
public:
  Foo() : Data(0) { }
  int getData() const { return Data; }
  void setData(int D) { Data = D; }
};

// OK to use `struct`: all members are public.
struct Bar {
  int Data;
  Bar() : Data(0) { }
};

不要使用大括號初始化列表來呼叫建構子

從 C++11 開始,有一種「通用初始化語法」,允許使用大括號初始化列表來呼叫建構子。不要使用這些來呼叫具有非簡單邏輯的建構子,或者如果您在意您正在呼叫特定的建構子。那些應該看起來像使用括號的函式呼叫,而不是像聚合初始化。同樣地,如果您需要明確命名型別並呼叫其建構子來建立暫時物件,請不要使用大括號初始化列表。相反地,當進行聚合初始化或概念上等效的操作時,請使用大括號初始化列表(對於暫時物件不使用任何型別)。範例:

class Foo {
public:
  // Construct a Foo by reading data from the disk in the whizbang format, ...
  Foo(std::string filename);

  // Construct a Foo by looking up the Nth element of some global data ...
  Foo(int N);

  // ...
};

// The Foo constructor call is reading a file, don't use braces to call it.
std::fill(foo.begin(), foo.end(), Foo("name"));

// The pair is being constructed like an aggregate, use braces.
bar_map.insert({my_key, my_value});

如果您在使用大括號初始化列表初始化變數時,請在左大括號之前使用等號:

int data[] = {0, 1, 2, 3};

使用 auto 型別推導來使程式碼更具可讀性

有些人提倡在 C++11 中採用「幾乎總是 auto」的策略,然而 LLVM 採用更溫和的立場。僅當 auto 使程式碼更具可讀性或更易於維護時才使用它。不要「幾乎總是」使用 auto,但在使用像 cast<Foo>(...) 這樣的初始化器或其他型別從上下文中已經很明顯的地方,請使用 auto。另一個 auto 在這些目的上運作良好的時機是,當型別無論如何都會被抽象化時,通常是在容器的 typedef 後面,例如 std::vector<T>::iterator

同樣地,C++14 添加了泛型 lambda 表達式,其中參數型別可以是 auto。在您會使用樣板的地方使用這些。

注意使用 auto 時不必要的複製

auto 的便利性很容易讓人忘記它的預設行為是複製。特別是在基於範圍的 for 迴圈中,不小心進行複製會非常耗費效能。

除非您需要進行複製,否則對於數值請使用 auto &,對於指標請使用 auto *

// Typically there's no reason to copy.
for (const auto &Val : Container) observe(Val);
for (auto &Val : Container) Val.change();

// Remove the reference if you really want a new copy.
for (auto Val : Container) { Val.change(); saveSomewhere(Val); }

// Copy pointers, but make it clear that they're pointers.
for (const auto *Ptr : Container) observe(*Ptr);
for (auto *Ptr : Container) Ptr->change();

注意由於指標排序造成的不確定性

一般來說,指標之間沒有相對順序。因此,當無序容器(如集合和映射)與指標鍵一起使用時,迭代順序是未定義的。因此,迭代此類容器可能會導致不確定的程式碼產生。雖然產生的程式碼可能可以正確運作,但不確定性可能會使錯誤重現和編譯器除錯變得更加困難。

如果預期有排序結果,請記住在迭代之前對無序容器進行排序。或者,如果您想要迭代指標鍵,請使用有序容器,如 vector/MapVector/SetVector

注意相等元素的不確定排序順序

std::sort 使用不穩定的排序演算法,其中不保證保留相等元素的順序。因此,對具有相等元素的容器使用 std::sort 可能會導致不確定的行為。為了揭露此類不確定性的實例,LLVM 引入了一個新的 llvm::sort 包裝函式。對於 EXPENSIVE_CHECKS 建置,這將在排序之前隨機shuffle容器。預設情況下,請使用 llvm::sort 而不是 std::sort

風格問題

高階問題

自包含標頭檔

標頭檔應該是自包含的(可以單獨編譯)並且以 .h 結尾。旨在包含的非標頭檔應該以 .inc 結尾,並且應謹慎使用。

所有標頭檔都應該是自包含的。使用者和重構工具不應該為了包含標頭檔而必須遵守特殊條件。具體來說,標頭檔應該具有標頭保護並且包含它需要的所有其他標頭檔。

在極少數情況下,設計為包含的檔案不是自包含的。這些檔案通常旨在包含在不尋常的位置,例如另一個檔案的中間。它們可能不使用標頭保護,並且可能不包含其先決條件。以 .inc 副檔名命名此類檔案。謹慎使用,並在可能的情況下偏好自包含標頭檔。

一般來說,標頭檔應該由一個或多個 .cpp 檔案實作。每個 .cpp 檔案都應該首先包含定義其介面的標頭檔。這確保了標頭檔的所有依賴項都已正確添加到標頭檔本身中,而不是隱含的。系統標頭檔應該在翻譯單元的使用者標頭檔之後包含。

函式庫分層

標頭檔目錄(例如 include/llvm/Foo)定義了一個函式庫(Foo)。一個函式庫(包括其標頭檔和實作)應該只使用其依賴項中列出的函式庫中的內容。

經典的 Unix 連結器可以強制執行部分此約束(Mac 和 Windows 連結器以及 lld 不會強制執行此約束)。Unix 連結器從左到右搜尋在其命令列上指定的函式庫,並且永遠不會重新訪問函式庫。這樣,函式庫之間就不可能存在循環依賴關係。

這並不能完全強制執行所有函式庫間的依賴關係,重要的是,它不能強制執行由 inline 函式建立的標頭檔循環依賴關係。「這是否正確分層」的一個好方法是考慮,如果所有 inline 函式都是 out-of-line 定義的,Unix 連結器是否可以成功連結程式。(& 對於依賴項的所有有效排序 - 由於連結解析是線性的,因此一些隱含的依賴關係可能會偷偷溜走:A 依賴於 B 和 C,因此有效的排序是「C B A」或「B C A」,在這兩種情況下,顯式依賴關係都出現在它們的使用之前。但在第一種情況下,如果 B 隱含地依賴於 C,則 B 仍然可以成功連結,或者在第二種情況下則相反)

#include 盡可能少用

#include 會損害編譯時間效能。除非必要,否則不要這樣做,尤其是在標頭檔中。

但是等等!有時您需要擁有類別的定義才能使用它,或從它繼承。在這些情況下,請繼續 #include 該標頭檔。但是請注意,在許多情況下,您不需要擁有類別的完整定義。如果您正在使用指向類別的指標或參考,則不需要標頭檔。如果您只是從原型函式或方法傳回類別實例,則不需要它。事實上,在大多數情況下,您根本不需要類別的定義。而且不 #include 會加快編譯速度。

然而,很容易在這項建議上過度努力。您必須包含您正在使用的所有標頭檔 — 您可以直接或間接地通過另一個標頭檔包含它們。為了確保您不會意外忘記在模組標頭檔中包含標頭檔,請確保在實作檔案中首先包含您的模組標頭檔(如上所述)。這樣就不會有任何隱藏的依賴關係,您稍後才會發現。

保持「內部」標頭檔私有

許多模組都有複雜的實作,使其需要使用多個實作 (.cpp) 檔案。通常很想將內部通訊介面(輔助類別、額外函式等)放在公用模組標頭檔中。不要這樣做!

如果您真的需要執行類似的操作,請在與原始碼檔案相同的目錄中放置一個私有標頭檔,並在本地包含它。這確保了您的私有介面保持私有,並且不受外部人員的干擾。

注意

在公用類別本身中放置額外的實作方法是可以的。只需將它們設為私有(或受保護),一切都會很好。

使用命名空間限定詞來實作先前宣告的函式

當在原始碼檔案中提供函式的 out-of-line 實作時,不要在原始碼檔案中打開命名空間區塊。相反地,使用命名空間限定詞來幫助確保您的定義與現有的宣告相符。這樣做:

// Foo.h
namespace llvm {
int foo(const char *s);
}

// Foo.cpp
#include "Foo.h"
using namespace llvm;
int llvm::foo(const char *s) {
  // ...
}

這樣做有助於避免定義與標頭檔中的宣告不符的錯誤。例如,以下 C++ 程式碼定義了 llvm::foo 的新過載,而不是為標頭檔中宣告的現有函式提供定義:

// Foo.cpp
#include "Foo.h"
namespace llvm {
int foo(char *s) { // Mismatch between "const char *" and "char *"
}
} // namespace llvm

直到建置即將完成時,當連結器找不到原始函式的任何用法的定義時,才會捕獲到此錯誤。如果函式改為使用命名空間限定詞定義,則在編譯定義時會立即捕獲到該錯誤。

類別方法實作必須已經命名類別,並且無法 out-of-line 引入新的過載,因此此建議不適用於它們。

使用提早退出和 continue 來簡化程式碼

在閱讀程式碼時,請記住讀者需要記住多少狀態和多少先前的決策才能理解程式碼區塊。在不使程式碼更難以理解的情況下,目標是盡可能減少縮排。做到這一點的一個好方法是在長迴圈中利用提早退出和 continue 關鍵字。考慮以下不使用提早退出的程式碼:

Value *doSomething(Instruction *I) {
  if (!I->isTerminator() &&
      I->hasOneUse() && doOtherThing(I)) {
    ... some long code ....
  }

  return 0;
}

如果 'if' 的主體很大,則此程式碼存在幾個問題。當您查看函式頂部時,並不立即清楚這對非終止符指令執行有趣的操作,並且僅適用於具有其他謂詞的內容。其次,相對難以(在註解中)描述為什麼這些謂詞很重要,因為 if 語句使得難以佈局註解。第三,當您深入程式碼主體時,它會額外縮排一個層級。最後,當閱讀函式頂部時,不清楚如果謂詞為假時結果是什麼;您必須閱讀到函式末尾才能知道它傳回 null。

最好將程式碼格式化為如下所示:

Value *doSomething(Instruction *I) {
  // Terminators never need 'something' done to them because ...
  if (I->isTerminator())
    return 0;

  // We conservatively avoid transforming instructions with multiple uses
  // because goats like cheese.
  if (!I->hasOneUse())
    return 0;

  // This is really just here for example.
  if (!doOtherThing(I))
    return 0;

  ... some long code ....
}

這解決了這些問題。在 for 迴圈中也經常發生類似的問題。一個愚蠢的例子是這樣的:

for (Instruction &I : BB) {
  if (auto *BO = dyn_cast<BinaryOperator>(&I)) {
    Value *LHS = BO->getOperand(0);
    Value *RHS = BO->getOperand(1);
    if (LHS != RHS) {
      ...
    }
  }
}

當您的迴圈非常非常小時,這種結構很好。但如果它超過 10-15 行,人們就很難一目了然地閱讀和理解。這種程式碼的問題在於它會很快變得非常巢狀。這意味著程式碼的讀者必須在其大腦中保留大量上下文,才能記住迴圈中正在發生的事情,因為他們不知道 if 條件是否會有 else 等。強烈建議將迴圈結構化為如下所示:

for (Instruction &I : BB) {
  auto *BO = dyn_cast<BinaryOperator>(&I);
  if (!BO) continue;

  Value *LHS = BO->getOperand(0);
  Value *RHS = BO->getOperand(1);
  if (LHS == RHS) continue;

  ...
}

這具有對函式使用提早退出的所有好處:它減少了迴圈的巢狀結構,使描述條件為真的原因更容易,並且使讀者清楚地知道沒有 else 即將出現,他們不必將上下文推入他們的大腦中。如果迴圈很大,這可能會大大提高可理解性。

return 後不要使用 else

出於與上述類似的原因(減少縮排和更容易閱讀),請不要在使用中斷控制流程的東西(如 returnbreakcontinuegoto 等)之後使用 'else''else if'。例如:

case 'J': {
  if (Signed) {
    Type = Context.getsigjmp_bufType();
    if (Type.isNull()) {
      Error = ASTContext::GE_Missing_sigjmp_buf;
      return QualType();
    } else {
      break; // Unnecessary.
    }
  } else {
    Type = Context.getjmp_bufType();
    if (Type.isNull()) {
      Error = ASTContext::GE_Missing_jmp_buf;
      return QualType();
    } else {
      break; // Unnecessary.
    }
  }
}

最好這樣寫:

case 'J':
  if (Signed) {
    Type = Context.getsigjmp_bufType();
    if (Type.isNull()) {
      Error = ASTContext::GE_Missing_sigjmp_buf;
      return QualType();
    }
  } else {
    Type = Context.getjmp_bufType();
    if (Type.isNull()) {
      Error = ASTContext::GE_Missing_jmp_buf;
      return QualType();
    }
  }
  break;

或者,在這個例子中,最好寫成這樣:

case 'J':
  if (Signed)
    Type = Context.getsigjmp_bufType();
  else
    Type = Context.getjmp_bufType();

  if (Type.isNull()) {
    Error = Signed ? ASTContext::GE_Missing_sigjmp_buf :
                     ASTContext::GE_Missing_jmp_buf;
    return QualType();
  }
  break;

這個想法是減少縮排和在閱讀程式碼時必須追蹤的程式碼量。

注意:此建議不適用於 constexpr if 語句。else 子句的子語句可能是被丟棄的語句,因此移除 else 可能會導致意外的樣板實例化。因此,以下範例是正確的:

template<typename T>
static constexpr bool VarTempl = true;

template<typename T>
int func() {
  if constexpr (VarTempl<T>)
    return 1;
  else
    static_assert(!VarTempl<T>);
}

將謂詞迴圈轉換為謂詞函式

編寫僅計算布林值的小迴圈非常常見。人們通常以多種方式編寫這些迴圈,但這種迴圈的一個例子是:

bool FoundFoo = false;
for (unsigned I = 0, E = BarList.size(); I != E; ++I)
  if (BarList[I]->isFoo()) {
    FoundFoo = true;
    break;
  }

if (FoundFoo) {
  ...
}

與這種迴圈不同,我們更喜歡使用謂詞函式(可能是靜態的),它使用提早退出

/// \returns true if the specified list has an element that is a foo.
static bool containsFoo(const std::vector<Bar*> &List) {
  for (unsigned I = 0, E = List.size(); I != E; ++I)
    if (List[I]->isFoo())
      return true;
  return false;
}
...

if (containsFoo(BarList)) {
  ...
}

這樣做有很多原因:它減少了縮排並分解了程式碼,這些程式碼通常可以由檢查相同謂詞的其他程式碼共用。更重要的是,它強迫您為函式選擇名稱,並強迫您為其編寫註解。在這個愚蠢的例子中,這並沒有增加太多價值。但是,如果條件很複雜,這可以讓讀者更容易理解查詢此謂詞的程式碼。我們可以信任函式名稱,並繼續以更好的局部性閱讀,而不是面對我們如何檢查 BarList 是否包含 foo 的內聯細節。

底層問題

正確命名型別、函式、變數和列舉器

選擇不當的名稱可能會誤導讀者並導致錯誤。我們怎麼強調使用描述性名稱的重要性都不為過。在合理的範圍內,選擇與底層實體的語義和角色相符的名稱。避免縮寫,除非它們是眾所周知的。在選擇一個好名稱後,請確保名稱使用一致的大小寫,因為不一致性要求客戶端要么記住 API,要么查找它以找到確切的拼寫。

一般來說,名稱應該使用駝峰式命名法(例如,TextFileReaderisLValue())。不同種類的宣告有不同的規則:

  • 型別名稱(包括類別、結構、列舉、typedef 等)應該是名詞,並以大寫字母開頭(例如,TextFileReader)。

  • 變數名稱應該是名詞(因為它們代表狀態)。名稱應該是駝峰式命名法,並以大寫字母開頭(例如,LeaderBoats)。

  • 函式名稱應該是動詞短語(因為它們代表動作),並且命令式函式應該是祈使句。名稱應該是駝峰式命名法,並以小寫字母開頭(例如,openFile()isFoo())。

  • 列舉宣告(例如,enum Foo {...})是型別,因此它們應該遵循型別的命名慣例。列舉的一個常見用途是作為聯合的辨別器,或子類的指示器。當列舉用於類似目的時,它應該具有 Kind 後綴(例如,ValueKind)。

  • 列舉器(例如,enum { Foo, Bar })和公有成員變數應該以大寫字母開頭,就像型別一樣。除非列舉器是在它們自己的小命名空間中或在類別內部定義的,否則列舉器應該具有與列舉宣告名稱相對應的前綴。例如,enum ValueKind { ... }; 可能包含像 VK_ArgumentVK_BasicBlock 等列舉器。僅僅是方便常數的列舉器免除前綴的要求。例如:

    enum {
      MaxSize = 42,
      Density = 12
    };
    

作為例外,模仿 STL 類別的類別可以具有 STL 風格的成員名稱,即小寫單字以底線分隔(例如,begin()push_back()empty())。提供多個迭代器的類別應該為 begin()end() 添加單數前綴(例如,global_begin()use_begin())。

以下是一些範例:

class VehicleMaker {
  ...
  Factory<Tire> F;            // Avoid: a non-descriptive abbreviation.
  Factory<Tire> Factory;      // Better: more descriptive.
  Factory<Tire> TireFactory;  // Even better: if VehicleMaker has more than one
                              // kind of factories.
};

Vehicle makeVehicle(VehicleType Type) {
  VehicleMaker M;                         // Might be OK if scope is small.
  Tire Tmp1 = M.makeTire();               // Avoid: 'Tmp1' provides no information.
  Light Headlight = M.makeLight("head");  // Good: descriptive.
  ...
}

自由地使用斷言

充分利用「assert」巨集。檢查您的所有先決條件和假設,您永遠不知道何時可能會通過斷言及早捕獲錯誤(不一定是您的錯誤),這會大大減少除錯時間。「<cassert>」標頭檔可能已經被您正在使用的標頭檔包含,因此使用它不會有任何成本。

為了進一步協助除錯,請確保在斷言語句中放入某種錯誤訊息,如果觸發斷言,則會列印該訊息。這有助於可憐的除錯器理解為什麼要進行和強制執行斷言,並希望知道該怎麼辦。這是一個完整的範例:

inline Value *getOperand(unsigned I) {
  assert(I < Operands.size() && "getOperand() out of range!");
  return Operands[I];
}

以下是更多範例:

assert(Ty->isPointerType() && "Can't allocate a non-pointer type!");

assert((Opcode == Shl || Opcode == Shr) && "ShiftInst Opcode invalid!");

assert(idx < getNumSuccessors() && "Successor # out of range!");

assert(V1.getType() == V2.getType() && "Constant types must be identical!");

assert(isa<PHINode>(Succ->front()) && "Only works on PHId BBs!");

您明白了。

過去,斷言用於指示不應到達的程式碼片段。這些通常採用以下形式:

assert(0 && "Invalid radix for integer literal");

這有一些問題,主要問題是某些編譯器可能不理解斷言,或者在編譯掉斷言的建置中警告缺少傳回。

今天,我們有了更好的東西:llvm_unreachable

llvm_unreachable("Invalid radix for integer literal");

啟用斷言後,如果到達這裡,它將列印訊息,然後退出程式。當禁用斷言時(即在發佈版本中),llvm_unreachable 成為編譯器跳過為此分支產生程式碼的提示。如果編譯器不支援此功能,它將回退到「abort」實作。

使用 llvm_unreachable 來標記程式碼中永遠不應到達的特定點。這對於解決關於不可到達分支等的警告尤其可取,但只要到達特定程式碼路徑無條件地是一種錯誤(不是源自使用者輸入;請參見下文)即可使用。 assert 的使用應始終包含可測試的謂詞(而不是 assert(false))。

如果錯誤條件可能由使用者輸入觸發,則應使用LLVM 程式設計師手冊中描述的可恢復錯誤機制。在不切實際的情況下,可以使用 report_fatal_error

另一個問題是,僅由斷言使用的值將在禁用斷言時產生「未使用值」警告。例如,以下程式碼會警告:

unsigned Size = V.size();
assert(Size > 42 && "Vector smaller than it should be");

bool NewToSet = Myset.insert(Value);
assert(NewToSet && "The value shouldn't be in the set yet");

這是兩個有趣的不同的案例。在第一種情況下,對 V.size() 的呼叫僅對斷言有用,我們不希望在禁用斷言時執行它。像這樣的程式碼應該將呼叫移動到斷言本身中。在第二種情況下,無論是否啟用斷言,都必須發生呼叫的副作用。在這種情況下,該值應強制轉換為 void 以禁用警告。具體來說,最好將程式碼寫成這樣:

assert(V.size() > 42 && "Vector smaller than it should be");

bool NewToSet = Myset.insert(Value); (void)NewToSet;
assert(NewToSet && "The value shouldn't be in the set yet");

不要使用 using namespace std

在 LLVM 中,我們偏好使用「std::」前綴顯式前綴標準命名空間中的所有識別符號,而不是依賴「using namespace std;」。

在標頭檔中,添加 'using namespace XXX' 指令會污染任何 #include 標頭檔的原始碼檔案的命名空間,從而產生維護問題。

在實作檔案(例如 .cpp 檔案)中,規則更像是一種風格規則,但仍然很重要。基本上,使用顯式命名空間前綴使程式碼更清晰,因為可以立即清楚地了解正在使用哪些設施以及它們來自何處。並且更具可移植性,因為 LLVM 程式碼和其他命名空間之間不會發生命名空間衝突。可移植性規則很重要,因為不同的標準函式庫實作公開了不同的符號(可能是不應該公開的符號),並且 C++ 標準的未來修訂版將向 std 命名空間添加更多符號。因此,我們永遠不會在 LLVM 中使用 'using namespace std;'

一般規則的例外情況(即,它不是 std 命名空間的例外情況)是用於實作檔案。例如,LLVM 專案中的所有程式碼都實作了位於 'llvm' 命名空間中的程式碼。因此,.cpp 檔案在 #include 之後的頂部具有 'using namespace llvm;' 指令是可以接受的,並且實際上更清晰。這減少了原始碼編輯器中基於大括號縮排的檔案主體的縮排,並保持了概念上下文的更清晰。此規則的一般形式是,任何在任何命名空間中實作程式碼的 .cpp 檔案都可以使用該命名空間(及其父命名空間),但不應使用任何其他命名空間。

為標頭檔中的類別提供虛擬方法錨點

如果一個類別在標頭檔中定義,並且具有 vtable(它具有虛擬方法或從具有虛擬方法的類別繼承),則它必須始終在類別中至少有一個 out-of-line 虛擬方法。如果沒有這個,編譯器會將 vtable 和 RTTI 複製到每個 #include 標頭檔的 .o 檔案中,從而膨脹 .o 檔案大小並增加連結時間。

不要在完全涵蓋列舉的 switch 語句中使用 default 標籤

-Wswitch 會在 switch 語句缺少 default 標籤,且未涵蓋列舉的每個列舉值時發出警告。如果您在完全涵蓋列舉的 switch 語句上編寫 default 標籤,那麼當向該列舉添加新元素時,-Wswitch 警告將不會觸發。為了幫助避免添加這些種類的 default,Clang 具有警告 -Wcovered-switch-default,預設情況下此警告處於關閉狀態,但在使用支援警告的 Clang 版本建置 LLVM 時會開啟。

這種風格要求的連帶效應是,當使用 GCC 建置 LLVM 時,如果您從涵蓋列舉的 switch 語句的每個 case 中傳回,您可能會收到與「控制可能會到達非 void 函式的末尾」相關的警告,因為 GCC 假設列舉表達式可能採用任何可表示的值,而不僅僅是個別列舉器的值。為了抑制此警告,請在 switch 語句後使用 llvm_unreachable

盡可能使用基於範圍的 for 迴圈

C++11 中基於範圍的 for 迴圈的引入意味著幾乎不需要顯式操作迭代器。對於所有新添加的程式碼,我們盡可能使用基於範圍的 for 迴圈。例如:

BasicBlock *BB = ...
for (Instruction &I : *BB)
  ... use I ...

除非 callable object 已經存在,否則不鼓勵使用 std::for_each()/llvm::for_each() 函數。

不要在每次迴圈迭代時都評估 end()

在無法使用基於範圍的 for 迴圈,且必須編寫顯式的基於迭代器的迴圈的情況下,請密切注意是否在每次迴圈迭代時重新評估 end()。一種常見的錯誤是以這種風格編寫迴圈

BasicBlock *BB = ...
for (auto I = BB->begin(); I != BB->end(); ++I)
  ... use I ...

這種結構的問題在於它在每次迴圈迭代時都會評估 “BB->end()”。 我們強烈建議迴圈應編寫成在迴圈開始之前評估一次。 一種方便的方法是這樣

BasicBlock *BB = ...
for (auto I = BB->begin(), E = BB->end(); I != E; ++I)
  ... use I ...

眼尖的人可能會很快指出,這兩個迴圈可能具有不同的語義:如果容器(在本例中為基本區塊)正在被修改,那麼 “BB->end()” 可能會在每次迴圈迭代時更改其值,而第二個迴圈實際上可能不正確。 如果您實際上依賴此行為,請以第一種形式編寫迴圈,並添加註釋表明您是有意這樣做的。

為什麼我們更喜歡第二種形式(在正確的情況下)? 以第一種形式編寫迴圈有兩個問題。 首先,它可能比在迴圈開始時評估效率更低。 在這種情況下,成本可能很小——每次迴圈迭代時增加一些額外的載入。 但是,如果基本表達式更複雜,則成本可能會迅速上升。 我見過迴圈的結束表達式實際上類似於:“SomeMap[X]->end()”,而 map 查找真的不便宜。 通過始終如一地以第二種形式編寫它,您可以完全消除這個問題,甚至不必考慮它。

第二個(甚至更大的)問題是以第一種形式編寫迴圈會向讀者暗示迴圈正在修改容器(註釋可以方便地確認這一點!)。 如果您以第二種形式編寫迴圈,則即使不查看迴圈主體,也很明顯容器沒有被修改,這使得更容易閱讀程式碼並理解它的作用。

雖然第二種形式的迴圈需要多按幾下鍵盤,但我們確實強烈推薦它。

#include <iostream> 為禁用項

在此禁止在程式庫檔案中使用 #include <iostream>,因為許多常見的實作會透明地將 靜態建構子 注入到每個包含它的翻譯單元中。

請注意,在這方面,使用其他串流標頭(例如 <sstream>)沒有問題——只有 <iostream> 有問題。 然而,raw_ostream 提供了各種 API,在幾乎所有用途中都比 std::ostream 樣式的 API 效能更好。

注意

新程式碼應始終使用 raw_ostream 進行寫入,或使用 llvm::MemoryBuffer API 進行檔案讀取。

使用 raw_ostream

LLVM 在 llvm/Support/raw_ostream.h 中包含一個輕量級、簡單且高效的串流實作,它提供了 std::ostream 的所有常見功能。 所有新程式碼都應使用 raw_ostream 而不是 ostream

std::ostream 不同,raw_ostream 不是模板,可以前向宣告為 class raw_ostream。 公共標頭通常不應包含 raw_ostream 標頭,而是使用前向宣告和對 raw_ostream 實例的常數引用。

避免使用 std::endl

std::endl 修飾符與 iostreams 一起使用時,會將換行符輸出到指定的輸出串流。 然而,除了執行此操作外,它還會刷新輸出串流。 換句話說,它們是等效的

std::cout << std::endl;
std::cout << '\n' << std::flush;

在大多數情況下,您可能沒有理由刷新輸出串流,因此最好使用字面量 '\n'

在類別定義中定義函數時,請勿使用 inline

在類別定義中定義的成員函數隱含地是 inline 的,因此在這種情況下不要放置 inline 關鍵字。

不要

class Foo {
public:
  inline void bar() {
    // ...
  }
};

應該

class Foo {
public:
  void bar() {
    // ...
  }
};

微觀細節

本節介紹了首選的低階格式化指南,以及我們為什麼偏好它們的原因。

括號前的空格

僅在控制流程語句中,在左括號前放置一個空格,但在常規函數呼叫表達式和類似函數的巨集中則不放置。 例如

if (X) ...
for (I = 0; I != 100; ++I) ...
while (LLVMRocks) ...

somefunc(42);
assert(3 != 4 && "laws of math are failing me");

A = foo(42, 92) + bar(X);

這樣做的原因並非完全隨意。 這種風格使控制流程運算符更加突出,並使表達式流動更順暢。

偏好前置遞增

硬性規定:前置遞增(++X)可能不比後置遞增(X++)慢,而且很可能比它快得多。 盡可能使用前置遞增。

後置遞增的語義包括建立要遞增的值的副本,返回它,然後前置遞增「工作值」。 對於原始類型,這不是什麼大問題。 但對於迭代器來說,這可能是一個巨大的問題(例如,某些迭代器包含堆疊和集合物件……複製迭代器也可能調用這些物件的複製建構子)。 總之,養成始終使用前置遞增的習慣,您就不會有問題。

命名空間縮排

一般來說,我們力求盡可能減少縮排。 這很有用,因為我們希望程式碼能夠符合 80 個欄位寬度,而不會過度換行,而且因為它使程式碼更容易理解。 為了方便這一點並避免有時出現極其深的巢狀結構,請勿縮排命名空間。 如果它有助於可讀性,請隨時添加註釋,指示哪個命名空間正被 } 關閉。 例如

namespace llvm {
namespace knowledge {

/// This class represents things that Smith can have an intimate
/// understanding of and contains the data associated with it.
class Grokable {
...
public:
  explicit Grokable() { ... }
  virtual ~Grokable() = 0;

  ...

};

} // namespace knowledge
} // namespace llvm

當要關閉的命名空間因任何原因而顯而易見時,可以隨時跳過關閉註釋。 例如,標頭檔案中最外層的命名空間很少會引起混淆。 但是,原始碼檔案中途關閉的匿名和命名空間可能需要澄清。

限制可見性

函數和變數應具有盡可能最受限制的可見性。 對於類別成員,這意味著使用適當的 privateprotectedpublic 關鍵字來限制其存取。 對於非成員函數、變數和類別,這意味著如果未在該檔案外部引用,則將可見性限制為單個 .cpp 檔案。

檔案範圍的非成員變數和函數的可見性可以使用 static 關鍵字或匿名命名空間限制為當前翻譯單元。 匿名命名空間是一個很棒的語言特性,它告訴 C++ 編譯器命名空間的內容僅在當前翻譯單元中可見,從而允許更積極的優化並消除符號名稱衝突的可能性。 匿名命名空間對於 C++ 就像 static 對於 C 函數和全域變數一樣。 雖然 static 在 C++ 中可用,但匿名命名空間更通用:它們可以使整個類別成為檔案私有的。

匿名命名空間的問題在於它們自然而然地傾向於鼓勵縮排其主體,並且它們降低了參考的局部性:如果您在 C++ 檔案中看到隨機函數定義,則很容易看出它是否標記為 static,但是查看它是否在匿名命名空間中需要掃描檔案的很大一部分。

因此,我們有一個簡單的指南:使匿名命名空間盡可能小,並且僅將它們用於類別宣告。 例如

namespace {
class StringSort {
...
public:
  StringSort(...)
  bool operator<(const char *RHS) const;
};
} // namespace

static void runHelper() {
  ...
}

bool StringSort::operator<(const char *RHS) const {
  ...
}

避免將類別以外的宣告放入匿名命名空間

namespace {

// ... many declarations ...

void runHelper() {
  ...
}

// ... many declarations ...

} // namespace

當您在大型 C++ 檔案的中間查看 “runHelper” 時,您無法立即判斷此函數是否是檔案本地的。 相反,當函數標記為 static 時,您不需要交叉引用檔案中遙遠的位置來判斷該函數是本地的。

不要在 if/else/loop 語句的簡單單語句主體上使用大括號

在編寫 ifelse 或 for/while 迴圈語句的主體時,我們更喜歡省略大括號以避免不必要的行雜訊。 但是,在省略大括號會損害程式碼的可讀性和可維護性的情況下,應使用大括號。

我們認為,當存在帶有註釋的單個語句時(假設註釋不能提升到 if 或迴圈語句之上,請參見下文),省略大括號會損害可讀性。

同樣,當單語句主體足夠複雜以至於難以看出包含以下語句的區塊從哪裡開始時,應使用大括號。 if/else 鏈或迴圈在本規則中被視為單個語句,並且此規則遞迴適用。

此列表並非詳盡無遺。 例如,如果 if/else 鏈沒有為其所有或任何成員使用帶大括號的主體,或者具有複雜的條件、深度巢狀結構等,也會損害可讀性。 以下範例旨在提供一些指導方針。

如果 if 的主體以(直接或間接)巢狀 if 語句結尾且沒有 else,則可維護性會受到損害。 外部 if 上的大括號將有助於避免遇到「懸空的 else」情況。

// Omit the braces since the body is simple and clearly associated with the
// `if`.
if (isa<FunctionDecl>(D))
  handleFunctionDecl(D);
else if (isa<VarDecl>(D))
  handleVarDecl(D);

// Here we document the condition itself and not the body.
if (isa<VarDecl>(D)) {
  // It is necessary that we explain the situation with this surprisingly long
  // comment, so it would be unclear without the braces whether the following
  // statement is in the scope of the `if`.
  // Because the condition is documented, we can't really hoist this
  // comment that applies to the body above the `if`.
  handleOtherDecl(D);
}

// Use braces on the outer `if` to avoid a potential dangling `else`
// situation.
if (isa<VarDecl>(D)) {
  if (shouldProcessAttr(A))
    handleAttr(A);
}

// Use braces for the `if` block to keep it uniform with the `else` block.
if (isa<FunctionDecl>(D)) {
  handleFunctionDecl(D);
} else {
  // In this `else` case, it is necessary that we explain the situation with
  // this surprisingly long comment, so it would be unclear without the braces
  // whether the following statement is in the scope of the `if`.
  handleOtherDecl(D);
}

// Use braces for the `else if` and `else` block to keep it uniform with the
// `if` block.
if (isa<FunctionDecl>(D)) {
  verifyFunctionDecl(D);
  handleFunctionDecl(D);
} else if (isa<GlobalVarDecl>(D)) {
  handleGlobalVarDecl(D);
} else {
  handleOtherDecl(D);
}

// This should also omit braces.  The `for` loop contains only a single
// statement, so it shouldn't have braces.  The `if` also only contains a
// single simple statement (the `for` loop), so it also should omit braces.
if (isa<FunctionDecl>(D))
  for (auto *A : D.attrs())
    handleAttr(A);

// Use braces for a `do-while` loop and its enclosing statement.
if (Tok->is(tok::l_brace)) {
  do {
    Tok = Tok->Next;
  } while (Tok);
}

// Use braces for the outer `if` since the nested `for` is braced.
if (isa<FunctionDecl>(D)) {
  for (auto *A : D.attrs()) {
    // In this `for` loop body, it is necessary that we explain the situation
    // with this surprisingly long comment, forcing braces on the `for` block.
    handleAttr(A);
  }
}

// Use braces on the outer block because there are more than two levels of
// nesting.
if (isa<FunctionDecl>(D)) {
  for (auto *A : D.attrs())
    for (ssize_t i : llvm::seq<ssize_t>(count))
      handleAttrOnDecl(D, A, i);
}

// Use braces on the outer block because of a nested `if`; otherwise the
// compiler would warn: `add explicit braces to avoid dangling else`
if (auto *D = dyn_cast<FunctionDecl>(D)) {
  if (shouldProcess(D))
    handleVarDecl(D);
  else
    markAsIgnored(D);
}

參見

許多這些評論和建議是從其他來源收集而來的。 對於我們的工作,兩本特別重要的書是

  1. Effective C++,作者 Scott Meyers。 同樣有趣和有用的是同一作者的 “More Effective C++” 和 “Effective STL”。

  2. Large-Scale C++ Software Design,作者 John Lakos

如果您有空閒時間,並且還沒有讀過它們:請務必閱讀,您可能會學到一些東西。