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 | 保持函数参数数量较少 |
应该做
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 风格可变参数 (F.55)
- 在传递给其他线程的 lambda 中通过引用捕获 (F.53)
- 返回
const T 会抑制移动语义 (F.49)
类与类层次结构 (C.*)
关键规则
| 规则 | 概要 |
|---|
| C.2 | 如果存在不变量,使用 class;如果数据成员独立变化,使用 struct |
| C.9 | 最小化成员的暴露 |
| C.20 | 如果可以不定义默认操作,就不要定义(零原则) |
| C.21 | 如果你定义或 =delete 了任何拷贝/移动/析构函数,则需处理所有(五原则) |
| C.35 | 基类析构函数:公有虚析构或受保护的非虚析构 |
| 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 -- 异常安全风险)
- 使用
shared_ptr 而 unique_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 | 析构函数、释放操作和 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)
- 静默吞掉错误的空 catch 块
- 将异常用于流程控制 (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_guard 和 unique_lock 命名 |
| CP.100 | 除非绝对必要,否则不要使用无锁编程 |
安全加锁
class ThreadSafeQueue {
public:
void push(int value) {
std::scoped_lock lock(mutex_);
data_.push(value);
cond_var_.notify_one();
}
int pop() {
std::unique_lock lock(mutex_);
cond_var_.wait(lock, [this] { return !data_.empty(); });
int value = data_.front();
data_.pop();
return value;
}
private:
std::mutex mutex_;
std::queue<int> data_;
std::condition_variable cond_var_;
};
反模式
- 使用裸的
lock()/unlock() (CP.20 -- 使用 RAII)
- 使用
volatile 进行同步 (CP.8 -- 使用 std::atomic)
- 持有锁时阻塞或调用未知代码 (CP.22 -- 死锁风险)
- 无条件等待(使用带有谓词的 condition_variable)(CP.42)
- 在可能的情况下将任务模型当作线程模型使用 (CP.4)
模板与泛型编程 (T.*)
关键规则
| 规则 | 概要 |
|---|
| T.1 | 使用模板来提升代码抽象 |
| T.10 | 为所有模板参数指定概念 |
| T.20 | 避免无需要求的“概念” |
| T.41 | 在模板中,仅对模板参数进行最少量的假设 |
| T.47 | 避免包含 |
[已截断]