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 | 保持函式參數數量較少 |
DO
struct Temperature {
double kelvin;
};
Temperature boil(const Temperature& water);
DON'T
double boil(double* temp);
int g_counter = 0;
函式 (F.*)
關鍵規則
| 規則 | 摘要 |
|---|
| F.1 | 將有意義的操作打包為精心命名的函式 |
| F.2 | 一個函式應執行單一邏輯操作 |
| F.3 | 保持函式簡短且簡單 |
| F.4 | 如果函式可能在編譯時被求值,將其宣告為 constexpr |
| F.6 | 如果函式保證不拋出異常,將其宣告為 noexcept |
| F.8 | 偏好純函式 |
| F.16 | 對於「輸入」參數,傳遞廉價複製型別時按值傳遞,其它情況按 const& 傳遞 |
| F.20 | 對於「輸出」值,偏好回傳值而非輸出參數 |
| F.21 | 要回傳多個「輸出」值,偏好回傳一個結構體 |
| F.43 | 永遠不要回傳指向區域物件的指標或參考 |
參數傳遞
void print(int x);
void analyze(const std::string& data);
void transform(std::string s);
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
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 | 基底類別解構函式:應為 public virtual 或 protected non-virtual |
| C.41 | 建構函式應建立一個完全初始化的物件 |
| C.46 | 宣告單一參數的建構函式為 explicit |
| C.67 | 多型類別應抑制公開的複製/移動 |
| C.128 | 虛擬函式:只須指定 virtual、override 或 final 中的一個 |
零法則
struct Employee {
std::string name;
std::string department;
int id;
};
五大法則
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 Shape {
public:
virtual ~Shape() = default;
virtual double area() const = 0;
};
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 |
智慧指標用法
auto widget = std::make_unique<Widget>("config");
auto cache = std::make_shared<Cache>(1024);
void render(const Widget* w) {
if (w) w->draw();
}
render(widget.get());
RAII 模式
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 -- 異常安全隱患)
- 在
unique_ptr 就足夠的情況下使用 shared_ptr (R.21)
運算式與陳述式 (ES.*)
關鍵規則
| 規則 | 摘要 |
|---|
| ES.5 | 保持作用域小 |
| ES.20 | 始終初始化一個物件 |
| ES.23 | 偏好 {} 初始化語法 |
| ES.25 | 除非需要修改,否則將物件宣告為 const 或 constexpr |
| ES.28 | 使用 lambda 進行複雜的 const 變數初始化 |
| ES.45 | 避免魔術常量;使用符號常量 |
| ES.46 | 避免窄化/有損耗的算術轉換 |
| ES.47 | 使用 nullptr 而非 0 或 NULL |
| ES.48 | 避免轉型 |
| ES.50 | 不要轉換掉 const |
初始化
const int max_retries{3};
const std::string name{"widget"};
const std::vector<int> primes{2, 3, 5, 7, 11};
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 | 解構函式、釋放和交換永遠不能失敗 |
| E.17 | 不要試圖在每個函式中捕獲每個異常 |
異常階層
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) {
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());
}
}
反模式
- 拋出像
int 或字串字面量這樣的內建型別 (E.14)
- 按值捕獲(有切片風險)(E.15)
- 默默吞掉錯誤的空捕獲區塊
- 使用異常進行流程控制 (E.3)
- 基於像
errno 這樣的全域狀態進行錯誤處理 (E.28)
常數與不可變性 (Con.*)
所有規則
| 規則 | 摘要 |
|---|
| Con.1 | 預設情況下,使物件不可變 |
| Con.2 | 預設情況下,使成員函式為 const |
| Con.3 | 預設情況下,傳遞指標和參考時指向 const |
| Con.4 | 對在建構後不會變更的值使用 const |
| Con.5 | 對可在編譯時計算的值使用 constexpr |
class Sensor {
public:
explicit Sensor(std::string id) : id_(std::move(id)) {}
const std::string& id() const { return id_; }
double last_reading() const { return reading_; }
void record(double value) { reading_ = value; }
private:
const std::string id_;
double reading_{0.0};
};
void display(const Sensor& s) {
std::cout << s.id() << ": " << s.last_reading() << '\n';
}
constexpr double PI = 3.14159265358979;
constexpr int MAX_SENSORS = 256;
並行與平行處理 (CP.*)
關鍵規則
| 規則 | 摘要 |
|---|
| CP.2 | 避免資料競爭 |
| CP.3 | 最小化可寫入資料的顯式共享 |
| CP.4 | 以任務(task)而非執行緒(thread)的方式思考 |
| CP.8 | 不要使用 volatile 進行同步 |
| CP.20 | 使用 RAII,永遠不要用裸 lock()/unlock() |
| CP.21 | 使用 std::scoped_lock 獲取多個互斥鎖 |
| CP.22 | 在持有鎖時,永遠不要呼叫未知的程式碼 |
| CP.42 | 不要無條件等待 |
| CP.44 | 記得為你的 lock_guards 和 unique_locks 命名 |
| CP.100 | 除非萬不得已,否則不要使用無鎖編程 |
安全鎖定
#include <queue>
#include <mutex>
#include <condition_variable>
template<typename T>
class ThreadSafeQueue {
public:
void push(T value) {
std::scoped_lock lock(mutex_);
queue_.push(std::move(value));
cond_.notify_one();
}
T pop() {
std::unique_lock lock(mutex_);
cond_.wait(lock, [this] { return !queue_.empty(); });
T value = std::move(queue_.front());
queue_.pop();
return value;
}
private:
std::queue<T> queue_;
std::mutex mutex_;
std::condition_variable cond_;
};
反模式
- 使用
volatile 進行執行緒間同步(使用 std::atomic)(CP.8)
- 裸
lock()/unlock() 模式(忘記解鎖)(CP.20)
- 在持有鎖時阻塞或呼叫使用者程式碼 (CP.22)
- 使用
shared_ptr 線上程之間共享可變資料(改為考慮拷貝或 unique_ptr)(CP.31)
模板與泛型編程 (T.*)
關鍵規則
| 規則 | 摘要 |
|---|
| T.10 | 為所有模板引數指定概念 |
| T.20 | 避免「無概念」的模板 |
| T.41 | 僅在模板中需要時才使用前置宣告 |
| T.47 | 避免高度可見的未受檢模板 |
| T.120 | 僅對真正的對映使用模板元編程 |
DO
#include <concepts>
template<std::integral T>
T square(T value) {
return value * value;
}
DON'T
template<typename T>
T max(T a, T b) {
return a > b ? a : b;
}
標準函式庫 (SL.*)
關鍵規則
| 規則 | 摘要 |
|---|
| SL.1 | 使用標準函式庫;不要重新發明輪子 |
| SL.2 | 偏好標準函式庫而非其他函式庫 |
| SL.3 | 避免非標準擴展 |
| SL.4 | 以便利且安全的方式使用標準函式庫 |
| SL.10 | 偏好 std::string 而非 char[] |
| SL.20 | 使用 std::span 或 std::string_view 表示一系列元素或字元 |
| SL.30 | 使用 std::optional 表示可能未初始化的值 |
| SL.40 | 使用 std::variant 表示型別安全的聯合體 |
| SL.50 | 使用 std::expected 表示帶有預期結果的函式(C++23) |
| SL.60 | 偏好 std::array 或 std::vector 而非 C 風格的陣列 |
DO
std::optional<int> find_value(const std::vector<int>& data, int target);
using ErrorCode = int;
std::variant<int, ErrorCode> compute_risk(const std::string& profile);
void process_text(std::string_view input);
std::array<double, 4> coefficients{1.0, 2.0, 3.0, 4.0};
DON'T
char buffer[256];
int data[100];
實作提示
編譯器標誌(GCC/Clang)
-Wall -Wextra -Wpedantic -Werror
-Wnon-virtual-dtor -Wold-style-cast -Wcast-align
-Woverloaded-virtual -Wconversion -Wsign-conversion
-Wmisleading-indentation -Wduplicated-cond -Wnull-dereference
工具
- clang-tidy:自動化程式碼檢查(包含
cppcoreguidelines-* 檢查組)
- clang-format:自動格式化(基於
.clang-format)
- cppcheck:靜態分析
- AddressSanitizer / UndefinedBehaviorSanitizer:執行時期檢查
clang-tidy 配置範例
Checks: '
cppcoreguidelines-*,
modernize-*,
performance-*,
readability-*,
bugprone-*,
-cppcoreguidelines-avoid-magic-numbers,
-readability-magic-numbers'
結論
遵循這些標準可以建立更安全、更易於維護、更具表現力的 C++ 程式碼。關鍵要點:
- 自動管理資源(RAII)
- 預設使用
const 和值語義
- 使用型別系統在編譯時防止錯誤
- 編寫表達意圖的簡單程式碼
- 偏好標準函式庫而非自製解決方案
- 注意異常安全和執行緒安全
當在有約束的環境(嵌入式、即時系統)中偏離這些規則時,始終記錄理由。