C++ 編碼標準 (C++ Core Guidelines) 是做什麼的?
適用於現代 C++ (C++17/20/23) 的全方位編碼標準,衍生自 C++ Core Guidelines。強制執行型別安全、資源安全、不可變性與清晰度。
使用時機
- 編寫新的 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 | 保持函式參數數量精簡 |
正確作法
struct Temperature {
double kelvin;
};
Temperature boil(const Temperature& water);
錯誤作法
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-style 可變參數 (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 | 多型類別應抑制 public 的複製/移動 |
| 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 | 對於 const 變數的複雜初始化,使用 lambda |
| 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-style 轉型 (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 | 不要試圖在每個函式中捕獲每個例外 |
例外階層
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 | 以任務而非執行緒的角度思考 |
| 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 | 除非絕對必要,否則不要使用無鎖編程 |
安全的鎖定
class ThreadSafeQueue {
public:
void push(int value
…
[為翻譯而截斷]