claude-code/claude-zh/skills/cpp-coding-standards/SKILL.md

22 KiB
Raw Permalink Blame History

name description
cpp-coding-standards 基於 C++ Core Guidelines (isocpp.github.io) 的 C++ 編碼標準。在編寫、審查或重構 C++ 程式碼時使用,以強制執行現代、安全且符合慣用法 (Idiomatic) 的實踐。

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

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

何時使用

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

何時「不」使用

  • 非 C++ 專案。
  • 無法採用現代 C++ 特性的遺留 C 程式碼庫。
  • 特定指南與硬體限制衝突的嵌入式/裸機環境 (請選擇性地調整使用)。

橫切原則 (Cross-Cutting Principles)

這些主題貫穿整個指南並構成基礎:

  1. RAII 無處不在 (P.8, R.1, E.6, CP.20):將資源生命週期與物件生命週期綁定。
  2. 預設不可變性 (P.10, Con.1-5, ES.25):從 const/constexpr 開始可變性Mutability是例外。
  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):優先選擇以值回傳與作用域物件 (Scoped objects)。

哲學與介面 (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 保持函式參數數量在低位準

推薦做法 (DO)

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

Temperature boil(const Temperature& water);

應避免的做法 (DON'T)

// 弱介面:所有權不明確、單位不明確
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 優先選用純函式 (Pure functions)
F.16 對於「輸入 (in)」參數,低成本複製之型別以值傳遞,其餘以 const& 傳遞
F.20 對於「輸出 (out)」值,優先選擇回傳值而非輸出參數
F.21 若要回傳多個輸出值,優先回傳一個 struct
F.43 絕不回傳指向區域物件的指標或參照

參數傳遞

// F.16:低成本型別傳值,其餘傳 const&
void print(int x);                           // 低成本:傳值
void analyze(const std::string& data);       // 高成本:傳 const&
void transform(std::string s);               // 接收端:傳值 (將觸發 move)

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

ParseResult parse(std::string_view input);   // 推薦 (GOOD):回傳 struct

// 錯誤範例 (BAD):輸出參數
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);

反模式 (Anti-Patterns)

  • 函式回傳 T&& (F.45)。
  • 使用 va_arg / C 風格變長參數 (F.55)。
  • 在傳遞給其他執行緒的 Lambda 中以傳參照方式擷取 (F.53)。
  • 回傳 const T,會抑制搬移語義 (Move semantics) (F.49)。

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

關鍵規則

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

Rule of Zero (零原則)

// C.20:讓編譯器生成特殊成員
struct Employee {
    std::string name;
    std::string department;
    int id;
    // 無需解構子、copy/move 建構子或賦值運算子
};

Rule of Five (五原則)

// 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_;
};

類別階層 (Class Hierarchy)

// 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)。
  • 在非平凡 (Non-trivial) 型別上使用 memset/memcpy (C.90)。
  • 為虛擬函式與其實現提供不同的預設參數 (C.140)。
  • 將資料成員設為 const 或參照,這會抑制 move/copy (C.12)。

資源管理 (R.*)

關鍵規則

規則 摘要
R.1 使用 RAII 自動管理資源
R.3 原始指標 (T*) 不具備所有權
R.5 優先選用作用域物件;不要進行非必要的堆積配置 (Heap-allocate)
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.21RAII 搭配智慧指標
auto widget = std::make_unique<Widget>("config");  // 唯一所有權
auto cache  = std::make_shared<Cache>(1024);        // 共享所有權

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

render(widget.get());

RAII 模式

// R.1:資源獲取即初始化 (Resource acquisition is initialization)
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_;
};

反模式

  • 赤裸的 (Naked) new/delete (R.11)。
  • 在 C++ 程式碼中呼叫 malloc()/free() (R.10)。
  • 在單一表達式中分配多個資源 (R.13 — 有異常安全風險)。
  • unique_ptr 足以勝任時使用 shared_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 避免強行轉型 (Casts)
ES.50 不要轉型掉 const (Cast away 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_cast, const_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 傳值抛出,傳引用 (by reference) 捕捉
E.16 解構子、釋放操作與 swap 絕不可失敗
E.17 不要試圖在每個函式中捕捉所有異常

異常階層 (Exception Hierarchy)

// 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)。
  • 以傳值方式捕捉 (有切割 Slicing 風險) (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 避免資料競爭 (Data races)
CP.3 最小化可寫入資料的明確共享
CP.4 以「任務」而非「執行緒」的角度思考
CP.8 不要使用 volatile 進行同步
CP.20 使用 RAII絕不單獨使用 lock()/unlock()
CP.21 使用 std::scoped_lock 獲取多個互斥鎖 (Mutexes)
CP.22 持有鎖時絕不呼叫未知程式碼
CP.42 不要無條件等待
CP.44 務必為您的 lock_guardunique_lock 命名
CP.100 除非絕對必要,否則不要使用無鎖 (Lock-free) 程式開發

安全加鎖

// CP.20 + CP.44RAII 鎖,務必命名
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:互斥鎖應與其保護的資料放在一起
    std::condition_variable cv_;
    std::queue<int> queue_;
};

多重互斥鎖

// CP.21:使用 std::scoped_lock 處理多個互斥鎖 (無死結風險)
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)。
  • 分離 (Detach) 執行緒 (CP.26 — 生命週期管理會變得幾乎不可能)。
  • 未命名的 Lock guardsstd::lock_guard<std::mutex>(m); 會立即銷毀 (CP.44)。
  • 在呼叫回呼函式 (Callbacks) 時持有鎖 (CP.22 — 有死結風險)。
  • 在不具備深厚專業知識的情況下進行無鎖程式開發 (CP.100)。

範本與泛型程式開發 (T.*)

關鍵規則

規則 摘要
T.1 使用範本提升抽象層級
T.2 使用範本針對多種參數型別表達演算法
T.10 為所有範本參數指定概念 (Concepts)
T.11 盡可能使用標準概念
T.13 對於簡單概念優先選用簡寫記法
T.43 優先選用 using 而非 typedef
T.120 僅在絕對必要時使用範本元編程 (Template metaprogramming)
T.144 不要特化 (Specialize) 函式範本 (應改用多載 Overload)

概念 (Concepts - 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.2string 擁有資料string_view 觀察資料
std::string build_greeting(std::string_view name) {
    return "Hello, " + std::string(name) + "!";
}

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

列舉 (Enum.*)

關鍵規則

規則 摘要
Enum.1 優先選用列舉而非巨集 (Macros)
Enum.3 優先選用 enum class 而非一般 enum
Enum.5 不要對列舉值使用 全大寫 (ALL_CAPS)
Enum.6 避免匿名列舉
// Enum.3 + Enum.5:強型別列舉,不使用全大寫
enum class Color { red, green, blue };
enum class LogLevel { debug, info, warning, error };

// 錯誤範例 (BAD):一般列舉會洩漏名稱,全大寫會與巨集衝突
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 標頭檔應具備自給自足 (Self-contained) 特性
NL.5 避免在名稱中編碼型別資訊 (不使用匈牙利命名法)
NL.8 使用一致的命名風格
NL.9 僅對巨集名稱使用 全大寫 (ALL_CAPS)
NL.10 優先選用 underscore_style (底線風格) 名稱

標頭檔防護 (Header Guard)

// SF.8Include 防護 (或使用 #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 {                 // 底線風格類別名
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;           // 推薦 (GOOD):連續存取
std::vector<std::unique_ptr<Point>> indirect_points; // 錯誤 (BAD):指標追蹤

反模式

  • 在沒有分析 (Profiling) 數據的情況下進行優化 (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)
  • 套用零原則 (Rule of Zero) 或五原則 (Rule of Five) (C.20, C.21)
  • 基底類別解構子為 public virtual 或 protected non-virtual (C.35)
  • 範本使用概念 (Concepts) 約束 (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)