C++ 編碼標準 (C++ Core Guidelines)
源自 C++ Core Guidelines 的現代 C++ (C++17/20/23) 全面編碼標準。強制型別安全、資源安全、不可變性與清晰度。
使用時機
- 撰寫新的 C++ 程式碼(類別、函式、模板)
- 審查或重構現有 C++ 程式碼
- 在 C++ 專案中做出架構決策
- 在 C++ 程式碼庫中強制一致的風格
- 在語言特性之間做選擇(例如
enum vs enum class、裸指標 vs 智慧指標)
不應使用時機
- 非 C++ 專案
- 無法採用現代 C++ 特性的舊版 C 程式碼庫
- 特定準則與硬體限制衝突的嵌入式/裸機環境(請選擇性調整)
貫穿原則
這些主題貫穿整個準則,構成基礎:
- 處處使用 RAII (P.8, R.1, E.6, CP.20):將資源生命週期綁定到物件生命週期
- 預設不可變 (P.10, Con.1-5, ES.25):從
const/constexpr 開始;可變性是例外
- 型別安全 (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):偏好回傳值與範圍物件
哲學與介面 (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 |
虛擬函式:精確指定 virtual、override 或 final 其中之一 |
零法則
// 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 |
避免明確呼叫 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:裸指標 = 非擁有觀察者
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_ptr 而 unique_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 |
避免轉型 |
| 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)
- 使用
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 |
拋出值,捕獲參考 |
| 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_guard 和 unique_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::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 << "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)
- 匈牙利命名法如
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; // 好:連續
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)