| 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)
這些主題貫穿整個指南並構成基礎:
- RAII 無處不在 (P.8, R.1, E.6, CP.20):將資源生命週期與物件生命週期綁定。
- 預設不可變性 (P.10, Con.1-5, ES.25):從
const/constexpr 開始;可變性(Mutability)是例外。
- 型別安全 (P.4, I.4, ES.46-49, Enum.3):使用型別系統在編譯時期防止錯誤。
- 表達意圖 (P.3, F.1, NL.1-2, T.10):名稱、型別與概念應傳達其用途。
- 最小化複雜度 (F.2-3, ES.5, Per.4-5):簡單的程式碼才是正確的程式碼。
- 數值語義優於指標語義 (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 |
虛擬函式:精確指定 virtual、override 或 final 其中之一 |
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 |
避免明確呼叫 new 與 delete |
| R.20 |
使用 unique_ptr 或 shared_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:原始指標 = 非擁有式觀察者 (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 |
除非打算修改,否則宣告物件為 const 或 constexpr |
| ES.28 |
對於 const 變數的複雜初始化使用 Lambda |
| ES.45 |
避免魔術常數;使用符號常數 |
| ES.46 |
避免窄化/有損的算術轉型 |
| ES.47 |
使用 nullptr 而非 0 或 NULL |
| 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)。
- 使用
0 或 NULL 作為指標 (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_guard 與 unique_lock 命名 |
| CP.100 |
除非絕對必要,否則不要使用無鎖 (Lock-free) 程式開發 |
安全加鎖
// 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:互斥鎖應與其保護的資料放在一起
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 guards:
std::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::array 或 std::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 << "結果: " << 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 (底線風格) 名稱 |
// 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 { // 底線風格類別名
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)。
- 匈牙利命名法,如
strName、iCount (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++ 工作標記為完成之前: