cpp-coding-standards

cpp-coding-standards

熱門

基於 C++ Core Guidelines (isocpp.github.io) 的 C++ 編碼標準。在撰寫、審查或重構 C++ 程式碼時使用,以強制執行現代、安全且符合慣例的實作方式。

23萬星標
3.5萬分支
更新於 2026/7/14
SKILL.md
readonlyread-only
name
cpp-coding-standards
description

基於 C++ Core Guidelines (isocpp.github.io) 的 C++ 編碼標準。在撰寫、審查或重構 C++ 程式碼時使用,以強制執行現代、安全且符合慣例的實作方式。

C++ 編碼標準 (C++ Core Guidelines)

源自 C++ Core Guidelines 的現代 C++ (C++17/20/23) 全面編碼標準。強制型別安全、資源安全、不可變性與清晰度。

使用時機

  • 撰寫新的 C++ 程式碼(類別、函式、模板)
  • 審查或重構現有 C++ 程式碼
  • 在 C++ 專案中做出架構決策
  • 在 C++ 程式碼庫中強制一致的風格
  • 在語言特性之間做選擇(例如 enum vs enum class、裸指標 vs 智慧指標)

不應使用時機

  • 非 C++ 專案
  • 無法採用現代 C++ 特性的舊版 C 程式碼庫
  • 特定準則與硬體限制衝突的嵌入式/裸機環境(請選擇性調整)

貫穿原則

這些主題貫穿整個準則,構成基礎:

  1. 處處使用 RAII (P.8, R.1, E.6, CP.20):將資源生命週期綁定到物件生命週期
  2. 預設不可變 (P.10, Con.1-5, ES.25):從 const/constexpr 開始;可變性是例外
  3. 型別安全 (P.4, I.4, ES.46-49, Enum.3):使用型別系統在編譯期防止錯誤
  4. 表達意圖 (P.3, F.1, NL.1-2, T.10):名稱、型別和概念應傳達目的
  5. 最小化複雜度 (F.2-3, ES.5, Per.4-5):簡單的程式碼就是正確的程式碼
  6. 值語義優先於指標語義 (C.10, R.3-5, F.20, CP.31):偏好回傳值與範圍物件

哲學與介面 (P., I.)

關鍵規則

規則 摘要
P.1 直接在程式碼中表達想法
P.3 表達意圖
P.4 理想上,程式應為靜態型別安全
P.5 偏好編譯期檢查勝於執行期檢查
P.8 不要洩漏任何資源
P.10 偏好不可變資料勝於可變資料
I.1 讓介面明確
I.2 避免非 const 的全域變數
I.4 讓介面精確且強型別
I.11 絕不透過裸指標或參考傳遞所有權
I.23 保持函式參數數量低

應做

// P.10 + I.4:不可變、強型別介面
struct Temperature {
    double kelvin;
};

Temperature boil(const Temperature& water);

不應做

// 弱介面:所有權不明確、單位不明確
double boil(double* temp);

// 非 const 全域變數
int g_counter = 0;  // 違反 I.2

函式 (F.*)

關鍵規則

規則 摘要
F.1 將有意義的操作封裝為精心命名的函式
F.2 一個函式應執行單一邏輯操作
F.3 保持函式簡短且簡單
F.4 若函式可能在編譯期求值,宣告為 constexpr
F.6 若函式絕不拋出例外,宣告為 noexcept
F.8 偏好純函式
F.16 對於「輸入」參數,便宜複製的型別傳值,其他傳 const&
F.20 對於「輸出」值,偏好回傳值勝於輸出參數
F.21 若要回傳多個「輸出」值,偏好回傳結構體
F.43 絕不回傳指向區域物件的指標或參考

參數傳遞

// F.16:便宜型別傳值,昂貴型別傳 const&
void print(int x);                           // 便宜:傳值
void analyze(const std::string& data);       // 昂貴:傳 const&
void transform(std::string s);               // 接收:傳值(將移動)

// F.20 + F.21:回傳值,而非輸出參數
struct ParseResult {
    std::string token;
    int position;
};

ParseResult parse(std::string_view input);   // 好:回傳結構體

// 差:輸出參數
void parse(std::string_view input,
           std::string& token, int& pos);    // 避免這樣做

純函式與 constexpr

// F.4 + F.8:盡可能純、constexpr
constexpr int factorial(int n) noexcept {
    return (n <= 1) ? 1 : n * factorial(n - 1);
}

static_assert(factorial(5) == 120);

反模式

  • 從函式回傳 T&& (F.45)
  • 使用 va_arg / C 風格可變參數 (F.55)
  • 在傳遞給其他執行緒的 lambda 中以參考捕獲 (F.53)
  • 回傳 const T 會抑制移動語義 (F.49)

類別與類別階層 (C.*)

關鍵規則

規則 摘要
C.2 若存在不變量則使用 class;若資料成員獨立變化則使用 struct
C.9 最小化成員的暴露
C.20 若可避免定義預設操作,則避免(零法則)
C.21 若定義或 =delete 任何複製/移動/解構子,則處理全部(五法則)
C.35 基底類別解構子:公開 virtual 或受保護 non-virtual
C.41 建構子應建立完全初始化的物件
C.46 單一參數建構子宣告為 explicit
C.67 多型類別應抑制公開複製/移動
C.128 虛擬函式:精確指定 virtualoverridefinal 其中之一

零法則

// C.20:讓編譯器產生特殊成員
struct Employee {
    std::string name;
    std::string department;
    int id;
    // 不需要解構子、複製/移動建構子或賦值運算子
};

五法則

// C.21:若必須管理資源,定義全部五個
class Buffer {
public:
    explicit Buffer(std::size_t size)
        : data_(std::make_unique<char[]>(size)), size_(size) {}

    ~Buffer() = default;

    Buffer(const Buffer& other)
        : data_(std::make_unique<char[]>(other.size_)), size_(other.size_) {
        std::copy_n(other.data_.get(), size_, data_.get());
    }

    Buffer& operator=(const Buffer& other) {
        if (this != &other) {
            auto new_data = std::make_unique<char[]>(other.size_);
            std::copy_n(other.data_.get(), other.size_, new_data.get());
            data_ = std::move(new_data);
            size_ = other.size_;
        }
        return *this;
    }

    Buffer(Buffer&&) noexcept = default;
    Buffer& operator=(Buffer&&) noexcept = default;

private:
    std::unique_ptr<char[]> data_;
    std::size_t size_;
};

類別階層

// C.35 + C.128:虛擬解構子,使用 override
class Shape {
public:
    virtual ~Shape() = default;
    virtual double area() const = 0;  // C.121:純介面
};

class Circle : public Shape {
public:
    explicit Circle(double r) : radius_(r) {}
    double area() const override { return 3.14159 * radius_ * radius_; }

private:
    double radius_;
};

反模式

  • 在建構子/解構子中呼叫虛擬函式 (C.82)
  • 對非平凡型別使用 memset/memcpy (C.90)
  • 為虛擬函式與覆寫者提供不同的預設引數 (C.140)
  • 將資料成員設為 const 或參考,會抑制移動/複製 (C.12)

資源管理 (R.*)

關鍵規則

規則 摘要
R.1 使用 RAII 自動管理資源
R.3 裸指標 (T*) 不擁有所有權
R.5 偏好範圍物件;不要不必要地堆積分配
R.10 避免 malloc()/free()
R.11 避免明確呼叫 newdelete
R.20 使用 unique_ptrshared_ptr 表示所有權
R.21 除非需要共享所有權,否則偏好 unique_ptr 勝於 shared_ptr
R.22 使用 make_shared() 建立 shared_ptr

智慧指標用法

// R.11 + R.20 + R.21:使用智慧指標的 RAII
auto widget = std::make_unique<Widget>("config");  // 唯一所有權
auto cache  = std::make_shared<Cache>(1024);        // 共享所有權

// R.3:裸指標 = 非擁有觀察者
void render(const Widget* w) {  // 不擁有 w
    if (w) w->draw();
}

render(widget.get());

RAII 模式

// R.1:資源取得即初始化
class FileHandle {
public:
    explicit FileHandle(const std::string& path)
        : handle_(std::fopen(path.c_str(), "r")) {
        if (!handle_) throw std::runtime_error("無法開啟: " + path);
    }

    ~FileHandle() {
        if (handle_) std::fclose(handle_);
    }

    FileHandle(const FileHandle&) = delete;
    FileHandle& operator=(const FileHandle&) = delete;
    FileHandle(FileHandle&& other) noexcept
        : handle_(std::exchange(other.handle_, nullptr)) {}
    FileHandle& operator=(FileHandle&& other) noexcept {
        if (this != &other) {
            if (handle_) std::fclose(handle_);
            handle_ = std::exchange(other.handle_, nullptr);
        }
        return *this;
    }

private:
    std::FILE* handle_;
};

反模式

  • new/delete (R.11)
  • 在 C++ 程式碼中使用 malloc()/free() (R.10)
  • 在單一表達式中多次資源分配 (R.13 -- 例外安全風險)
  • 使用 shared_ptrunique_ptr 已足夠 (R.21)

表達式與陳述式 (ES.*)

關鍵規則

規則 摘要
ES.5 保持作用域小
ES.20 始終初始化物件
ES.23 偏好 {} 初始化語法
ES.25 除非預期修改,否則宣告物件為 constconstexpr
ES.28 const 變數的複雜初始化使用 lambda
ES.45 避免魔術常數;使用符號常數
ES.46 避免窄化/有損算術轉換
ES.47 使用 nullptr 而非 0NULL
ES.48 避免轉型
ES.50 不要轉型移除 const

初始化

// ES.20 + ES.23 + ES.25:始終初始化,偏好 {},預設為 const
const int max_retries{3};
const std::string name{"widget"};
const std::vector<int> primes{2, 3, 5, 7, 11};

// ES.28:對複雜 const 初始化使用 lambda
const auto config = [&] {
    Config c;
    c.timeout = std::chrono::seconds{30};
    c.retries = max_retries;
    c.verbose = debug_mode;
    return c;
}();

反模式

  • 未初始化的變數 (ES.20)
  • 使用 0NULL 作為指標 (ES.47 -- 使用 nullptr)
  • C 風格轉型 (ES.48 -- 使用 static_castconst_cast 等)
  • 轉型移除 const (ES.50)
  • 沒有命名常數的魔術數字 (ES.45)
  • 混合有號與無號算術 (ES.100)
  • 在巢狀作用域中重複使用名稱 (ES.12)

錯誤處理 (E.*)

關鍵規則

規則 摘要
E.1 在設計初期制定錯誤處理策略
E.2 拋出例外以表示函式無法執行其指定任務
E.6 使用 RAII 防止洩漏
E.12 當不可能或不允許拋出時使用 noexcept
E.14 使用專門設計的使用者定義型別作為例外
E.15 拋出值,捕獲參考
E.16 解構子、解分配和 swap 絕不能失敗
E.17 不要試圖在每個函式中捕獲每個例外

例外階層

// E.14 + E.15:自訂例外型別,拋出值,捕獲參考
class AppError : public std::runtime_error {
public:
    using std::runtime_error::runtime_error;
};

class NetworkError : public AppError {
public:
    NetworkError(const std::string& msg, int code)
        : AppError(msg), status_code(code) {}
    int status_code;
};

void fetch_data(const std::string& url) {
    // E.2:拋出以表示失敗
    throw NetworkError("連線被拒", 503);
}

void run() {
    try {
        fetch_data("https://api.example.com");
    } catch (const NetworkError& e) {
        log_error(e.what(), e.status_code);
    } catch (const AppError& e) {
        log_error(e.what());
    }
    // E.17:不要在這裡捕獲所有例外 -- 讓未預期的錯誤傳播
}

反模式

  • 拋出內建型別如 int 或字串字面值 (E.14)
  • 以值捕獲(切片風險)(E.15)
  • 空的 catch 區塊默默吞掉錯誤
  • 使用例外作為流程控制 (E.3)
  • 基於全域狀態如 errno 的錯誤處理 (E.28)

常數與不可變性 (Con.*)

所有規則

規則 摘要
Con.1 預設使物件不可變
Con.2 預設使成員函式為 const
Con.3 預設傳遞指標和參考為 const
Con.4 對建構後不變的值使用 const
Con.5 對編譯期可計算的值使用 constexpr
// Con.1 到 Con.5:預設不可變
class Sensor {
public:
    explicit Sensor(std::string id) : id_(std::move(id)) {}

    // Con.2:預設 const 成員函式
    const std::string& id() const { return id_; }
    double last_reading() const { return reading_; }

    // 僅在需要修改時才非 const
    void record(double value) { reading_ = value; }

private:
    const std::string id_;  // Con.4:建構後永不改變
    double reading_{0.0};
};

// Con.3:傳遞 const 參考
void display(const Sensor& s) {
    std::cout << s.id() << ": " << s.last_reading() << '\n';
}

// Con.5:編譯期常數
constexpr double PI = 3.14159265358979;
constexpr int MAX_SENSORS = 256;

並行與平行 (CP.*)

關鍵規則

規則 摘要
CP.2 避免資料競爭
CP.3 最小化可寫資料的明確共享
CP.4 以任務而非執行緒的角度思考
CP.8 不要使用 volatile 進行同步
CP.20 使用 RAII,絕不使用裸 lock()/unlock()
CP.21 使用 std::scoped_lock 取得多個 mutex
CP.22 持有鎖時絕不呼叫未知程式碼
CP.42 不要在沒有條件的情況下等待
CP.44 記得命名你的 lock_guardunique_lock
CP.100 除非絕對必要,否則不要使用無鎖程式設計

安全鎖定

// CP.20 + CP.44:RAII 鎖,始終命名
class ThreadSafeQueue {
public:
    void push(int value) {
        std::lock_guard<std::mutex> lock(mutex_);  // CP.44:命名!
        queue_.push(value);
        cv_.notify_one();
    }

    int pop() {
        std::unique_lock<std::mutex> lock(mutex_);
        // CP.42:始終在條件下等待
        cv_.wait(lock, [this] { return !queue_.empty(); });
        const int value = queue_.front();
        queue_.pop();
        return value;
    }

private:
    std::mutex mutex_;             // CP.50:mutex 與其資料一起
    std::condition_variable cv_;
    std::queue<int> queue_;
};

多個 Mutex

// CP.21:std::scoped_lock 用於多個 mutex(無死鎖)
void transfer(Account& from, Account& to, double amount) {
    std::scoped_lock lock(from.mutex_, to.mutex_);
    from.balance_ -= amount;
    to.balance_ += amount;
}

反模式

  • 使用 volatile 進行同步 (CP.8 -- 僅用於硬體 I/O)
  • 分離執行緒 (CP.26 -- 生命週期管理幾乎不可能)
  • 未命名的鎖守衛:std::lock_guard<std::mutex>(m); 立即銷毀 (CP.44)
  • 持有鎖時呼叫回呼 (CP.22 -- 死鎖風險)
  • 沒有深厚專業知識的無鎖程式設計 (CP.100)

模板與泛型程式設計 (T.*)

關鍵規則

規則 摘要
T.1 使用模板提升抽象層級
T.2 使用模板表達適用於多種引數型別的演算法
T.10 為所有模板引數指定概念
T.11 盡可能使用標準概念
T.13 對簡單概念偏好簡寫語法
T.43 偏好 using 勝於 typedef
T.120 僅在真正需要時使用模板元程式設計
T.144 不要特化函式模板(改為重載)

概念 (C++20)

#include <concepts>

// T.10 + T.11:使用標準概念約束模板
template<std::integral T>
T gcd(T a, T b) {
    while (b != 0) {
        a = std::exchange(b, a % b);
    }
    return a;
}

// T.13:簡寫概念語法
void sort(std::ranges::random_access_range auto& range) {
    std::ranges::sort(range);
}

// 領域特定約束的自訂概念
template<typename T>
concept Serializable = requires(const T& t) {
    { t.serialize() } -> std::convertible_to<std::string>;
};

template<Serializable T>
void save(const T& obj, const std::string& path);

反模式

  • 在可見命名空間中的無約束模板 (T.47)
  • 特化函式模板而非重載 (T.144)
  • 使用模板元程式設計而 constexpr 已足夠 (T.120)
  • 使用 typedef 而非 using (T.43)

標準函式庫 (SL.*)

關鍵規則

規則 摘要
SL.1 盡可能使用函式庫
SL.2 偏好標準函式庫勝於其他函式庫
SL.con.1 偏好 std::arraystd::vector 勝於 C 陣列
SL.con.2 預設偏好 std::vector
SL.str.1 使用 std::string 擁有字元序列
SL.str.2 使用 std::string_view 參考字元序列
SL.io.50 避免 endl(使用 '\n' -- endl 會強制 flush)
// SL.con.1 + SL.con.2:偏好 vector/array 勝於 C 陣列
const std::array<int, 4> fixed_data{1, 2, 3, 4};
std::vector<std::string> dynamic_data;

// SL.str.1 + SL.str.2:string 擁有,string_view 觀察
std::string build_greeting(std::string_view name) {
    return "Hello, " + std::string(name) + "!";
}

// SL.io.50:使用 '\n' 而非 endl
std::cout << "result: " << value << '\n';

列舉 (Enum.*)

關鍵規則

規則 摘要
Enum.1 偏好列舉勝於巨集
Enum.3 偏好 enum class 勝於普通 enum
Enum.5 不要對列舉值使用全大寫
Enum.6 避免無名列舉
// Enum.3 + Enum.5:範圍列舉,無全大寫
enum class Color { red, green, blue };
enum class LogLevel { debug, info, warning, error };

// 差:普通列舉洩漏名稱,全大寫與巨集衝突
enum { RED, GREEN, BLUE };           // 違反 Enum.3 + Enum.5 + Enum.6
#define MAX_SIZE 100                  // 違反 Enum.1 -- 使用 constexpr

原始檔與命名 (SF., NL.)

關鍵規則

規則 摘要
SF.1 程式碼檔使用 .cpp,介面檔使用 .h
SF.7 不要在標頭檔的全域作用域中寫 using namespace
SF.8 所有 .h 檔使用 #include 守衛
SF.11 標頭檔應自給自足
NL.5 避免在名稱中編碼型別資訊(無匈牙利命名法)
NL.8 使用一致的命名風格
NL.9 僅對巨集名稱使用全大寫
NL.10 偏好 underscore_style 名稱

標頭守衛

// SF.8:Include 守衛(或 #pragma once)
#ifndef PROJECT_MODULE_WIDGET_H
#define PROJECT_MODULE_WIDGET_H

// SF.11:自給自足 -- 包含此標頭所需的一切
#include <string>
#include <vector>

namespace project::module {

class Widget {
public:
    explicit Widget(std::string name);
    const std::string& name() const;

private:
    std::string name_;
};

}  // namespace project::module

#endif  // PROJECT_MODULE_WIDGET_H

命名慣例

// NL.8 + NL.10:一致的 underscore_style
namespace my_project {

constexpr int max_buffer_size = 4096;  // NL.9:非全大寫(不是巨集)

class tcp_connection {                 // underscore_style 類別
public:
    void send_message(std::string_view msg);
    bool is_connected() const;

private:
    std::string host_;                 // 成員尾隨底線
    int port_;
};

}  // namespace my_project

反模式

  • 在標頭檔的全域作用域中使用 using namespace std; (SF.7)
  • 依賴包含順序的標頭 (SF.10, SF.11)
  • 匈牙利命名法如 strNameiCount (NL.5)
  • 對非巨集使用全大寫 (NL.9)

效能 (Per.*)

關鍵規則

規則 摘要
Per.1 沒有理由就不要最佳化
Per.2 不要過早最佳化
Per.6 沒有測量就不要對效能做出宣稱
Per.7 設計以允許最佳化
Per.10 依賴靜態型別系統
Per.11 將計算從執行期移至編譯期
Per.19 可預測地存取記憶體

指導方針

// Per.11:盡可能編譯期計算
constexpr auto lookup_table = [] {
    std::array<int, 256> table{};
    for (int i = 0; i < 256; ++i) {
        table[i] = i * i;
    }
    return table;
}();

// Per.19:偏好連續資料以利快取
std::vector<Point> points;           // 好:連續
std::vector<std::unique_ptr<Point>> indirect_points; // 差:指標追逐

反模式

  • 沒有效能分析資料就最佳化 (Per.1, Per.6)
  • 選擇「聰明」的低階程式碼而非清晰的抽象 (Per.4, Per.5)
  • 忽略資料佈局與快取行為 (Per.19)

快速參考檢查清單

在標記 C++ 工作完成前:

  • [ ] 無裸 new/delete -- 使用智慧指標或 RAII (R.11)
  • [ ] 物件在宣告時初始化 (ES.20)
  • [ ] 變數預設為 const/constexpr (Con.1, ES.25)
  • [ ] 成員函式在可能時為 const (Con.2)
  • [ ] 使用 enum class 而非普通 enum (Enum.3)
  • [ ] 使用 nullptr 而非 0/NULL (ES.47)
  • [ ] 無窄化轉換 (ES.46)
  • [ ] 無 C 風格轉型 (ES.48)
  • [ ] 單一參數建構子為 explicit (C.46)
  • [ ] 套用零法則或五法則 (C.20, C.21)
  • [ ] 基底類別解構子為公開 virtual 或受保護 non-virtual (C.35)
  • [ ] 模板以概念約束 (T.10)
  • [ ] 標頭中無全域作用域的 using namespace (SF.7)
  • [ ] 標頭有 include 守衛且自給自足 (SF.8, SF.11)
  • [ ] 鎖使用 RAII (scoped_lock/lock_guard) (CP.20)
  • [ ] 例外為自訂型別,拋出值,捕獲參考 (E.14, E.15)
  • [ ] 使用 '\n' 而非 std::endl (SL.io.50)
  • [ ] 無魔術數字 (ES.45)