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^)

機械原始碼問題

原始碼格式

註釋

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

檔案標頭

每個原始程式檔都應該有一個標頭,描述該檔案的基本用途。標準標頭如下所示

//===-- llvm/Instruction.h - Instruction class definition -------*- C++ -*-===//
//
// 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.
///
//===----------------------------------------------------------------------===//

關於這種特定格式需要注意幾點:第一行的「-*- C++ -*-」字串是用來告訴 Emacs 原始程式檔是 C++ 檔案,而不是 C 檔案(Emacs 預設認為 .h 檔案是 C 檔案)。

備註

這個標記在 .cpp 檔案中不是必需的。檔案名稱也在第一行,以及關於檔案用途的簡短描述。

檔案中的下一部分是一個簡潔的註解,定義了釋出檔案所依據的授權條款。這清楚地說明了原始程式碼可以在哪些條款下發布,並且不應以任何方式修改。

主體是一個 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 命令開始新段落。如果參數用作輸出或輸入/輸出參數,請分別使用 \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() { ... }

錯誤和警告訊息

清晰的診斷訊息對於幫助用戶識別和修復輸入中的問題非常重要。使用簡潔但正確的英文散文,為用戶提供理解錯誤原因所需的上下文。此外,為了匹配其他工具通常產生的錯誤訊息樣式,請以小寫字母開頭第一個句子,如果最後一個句子以句點結尾,則不要使用句點。以不同標點符號結尾的句子(例如“您是否忘記了‘;’?”)應該仍然這樣做。

舉例來說,這是一個良好的錯誤訊息

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 guards)之後,應立即列出檔案所需的 最少 #includes 清單。我們希望這些 #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 標頭檔因先前在主原始程式檔或其他較早的標頭檔中包含該標頭檔而意外引入遺漏的 include 的可能性。同樣地,clang 也應該在包含 llvm 標頭檔之前先包含自己的標頭檔。此規則適用於所有 LLVM 子專案。

原始碼寬度

編寫程式碼時,請將其控制在 80 個字元以內。

為了讓開發者在一般尺寸顯示器上能並排顯示多個檔案視窗,程式碼寬度必須有所限制。如果要選擇寬度限制,雖然有點武斷,但最好選擇一個標準。選擇 90 個字元(例如)而不是 80 個字元不會增加任何顯著價值,反而會對列印程式碼造成負面影響。此外,許多其他專案都已標準化為 80 個字元,因此有些人已經為其編輯器設定了相應的設定(而不是其他設定,例如 90 個字元)。

空格

在所有情況下,原始檔案中應優先使用空格而不是定位字元。人們有不同的縮排級別偏好,以及他們喜歡的縮排樣式;這都沒問題。但不同編輯器/檢視器將定位字元展開為不同的定位點,這就會產生問題。這可能會導致您的程式碼看起來完全無法閱讀,而且不值得這樣處理。

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

不要添加尾隨空格。一些常見的編輯器會在儲存檔案時自動刪除尾隨空格,這會導致在差異和提交中出現不相關的更改。

將 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,這些 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 組建,這將在排序之前隨機排列容器。 預設使用 llvm::sort 而不是 std::sort

風格議題

高階議題

自包含標頭檔

標頭檔應為自包含的(自行編譯)並以 .h 結尾。 用於包含的非標頭檔應以 .inc 結尾,並且應謹慎使用。

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

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

通常,標頭檔應該由一個或多個 .cpp 檔案實現。這些 .cpp 檔案中的每一個都應該首先包含定義其介面的標頭檔。這確保了標頭檔的所有依賴關係都已正確添加到標頭檔本身中,而不是隱式的。系統標頭檔應該在翻譯單元的用戶標頭檔之後包含。

程式庫分層

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

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

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

#include 盡可能少用

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

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

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

將「內部」標頭檔設為私有

許多模組具有複雜的實現,導致它們使用多個實現(.cpp)檔。 通常很容易將內部通信介面(輔助類別、額外函式等)放在公共模組標頭檔中。 不要這樣做!

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

備註

可以在公共類別本身中放置額外的實現方法。 只需將它們設為私有(或受保護),一切都沒問題。

使用命名空間限定符來實現先前聲明的函式

在原始程式檔中提供函式的外部實現時,不要在原始程式檔中打開命名空間塊。 相反,請使用命名空間限定符來幫助確保您的定義與現有聲明相符。 這樣做

// 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

這個錯誤要到建置快完成時才會被發現,因為連結器找不到原始函式任何使用位置的定義。如果改用命名空間限定詞定義函式,則在編譯定義時就會立即發現錯誤。

類別方法實作必須已經命名類別,而且無法在外部引入新的多載,因此這項建議不適用於類別方法。

使用提前退出和 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");

這有一些問題,主要問題是某些編譯器可能無法理解斷言,或者在編譯出斷言的建置中警告缺少 return 語句。

今天,我們有更好的方法:llvm_unreachable

llvm_unreachable("Invalid radix for integer literal");

當 assertion 啟用時,如果執行到此處將會印出訊息並結束程式。當 assertion 被停用時(例如在發佈版本中),llvm_unreachable 會變成一個提示,讓編譯器跳過此分支的程式碼生成。如果編譯器不支援此功能,它將會回到「中止」的實作方式。

使用 llvm_unreachable 來標記程式碼中永遠不應該到達的特定點。這對於處理關於不可到達分支等的警告特別有用,但只要到達特定程式碼路徑無條件地是某種錯誤(並非源自使用者輸入;請見下文),就可以使用它。使用 assert 應該始終包含一個可測試的謂詞(而不是 assert(false))。

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

另一個問題是,當 assertion 被停用時,僅由 assertion 使用的值將會產生「未使用值」的警告。例如,以下程式碼將會發出警告

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() 僅對 assertion 有用,我們不希望在 assertion 被停用時執行它。像這樣的程式碼應該將呼叫移到 assertion 本身中。在第二種情況中,無論 assertion 是否啟用,呼叫的副作用都必須發生。在這種情況下,應將值轉換為 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'」指令會污染任何包含該標頭檔的原始檔的命名空間,從而產生維護問題。

在實作檔(例如 .cpp 檔)中,該規則更多地是一種風格規則,但仍然很重要。基本上,使用明確的命名空間前綴會使程式碼**更清晰**,因為它可以立即清楚地知道正在使用哪些工具以及它們來自哪裡。並且**更具可攜性**,因為 LLVM 程式碼與其他命名空間之間不會發生命名空間衝突。可攜性規則很重要,因為不同的標準函式庫實作會公開不同的符號(可能是它們不應該公開的符號),而且 C++ 標準的未來版本將會向 std 命名空間添加更多符號。因此,我們從不在 LLVM 中使用「using namespace std;'」。

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

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

如果一個類別在標頭檔中定義,並且具有虛擬函式表(它具有虛擬方法,或者它繼承自具有虛擬方法的類別),則它必須始終在類別中至少具有一個非內嵌的虛擬方法。否則,編譯器會將虛擬函式表和 RTTI 複製到每個 #include 標頭檔的 .o 檔案中,從而導致 .o 檔案大小過大和連結時間增加。

不要在涵蓋所有列舉值的 switch 陳述式中使用預設標籤

如果一個沒有預設標籤的 switch 陳述式沒有涵蓋列舉類型的所有列舉值,-Wswitch 會發出警告。如果您在涵蓋所有列舉值的 switch 陳述式中寫入預設標籤,則當向該列舉類型添加新元素時,-Wswitch 警告將不會觸發。為了避免添加這種類型的預設值,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 ...

不建議使用 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()」,而地圖查找確實不便宜。通過始終以第二種形式編寫,您可以完全消除這個問題,甚至不必考慮它。

第二個(更大的)問題是,以第一種形式編寫迴圈會向讀者暗示迴圈正在修改容器(註釋可以很容易地確認這一點!)。如果您以第二種形式編寫迴圈,則無需查看迴圈體就能立即清楚地知道容器沒有被修改,這使得閱讀和理解代碼變得更容易。

雖然第二種形式的迴圈需要多按幾個鍵,但我們強烈建議使用它。

#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::ostreamraw_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 關鍵字。

不要這樣做

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

如果因為任何原因被關閉的命名空間很明顯,可以跳過結尾註解。例如,標頭檔中最外層的命名空間很少會造成混淆。但是,原始程式檔中在檔案中間關閉的匿名和命名命名空間可能需要說明。

匿名命名空間

在討論了一般命名空間之後,您可能會想了解匿名命名空間。匿名命名空間是一種很棒的語言功能,它告訴 C++ 編譯器命名空間的內容僅在當前的翻譯單元中可見,從而允許更積極的優化並消除符號名稱衝突的可能性。匿名命名空間之於 C++ 就像“static”之於 C 函數和全域變數。雖然 C++ 中可以使用“static”,但匿名命名空間更通用:它們可以使整個類別對檔案私有。

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

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

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”時,您無法立即判斷此函數是否對檔案是局部的。相反,當函數被標記為靜態時,您不需要交叉引用檔案中的遙遠位置即可知道該函數是局部的。

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

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

我們認為,如果在省略大括號的情況下存在單個語句且該語句帶有註釋(假設該註釋不能提升到 if 或迴圈語句上方,請參見下文),則可讀性會受到損害。

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

此列表並非詳盡無遺。例如,如果 if/else 鏈的成員沒有全部或都沒有使用大括號主體,或者具有複雜的條件、深度嵌套等,則可讀性也會受到損害。以下示例旨在提供一些準則。

如果一個 if 語句主體以一個(直接或間接)巢狀的沒有 elseif 語句結束,則會損害可維護性。在外層 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);
}

// 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. Scott Meyers 的Effective C++。同樣有趣且實用的還有同一位作者的「More Effective C++」和「Effective STL」。

  2. John Lakos 的Large-Scale C++ Software Design

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