C++ 编码规范 (C++ Core Guidelines) 是做什么的?
基于 C++ Core Guidelines 的现代 C++ (C++17/20/23) 综合编码规范。强制实施类型安全、资源安全、不变性和清晰度。
何时使用
- 编写新的 C++ 代码(类、函数、模板)
- 审查或重构现有的 C++ 代码
- 在 C++ 项目中做出架构决策
- 在 C++ 代码库中强制执行一致的风格
- 在语言特性之间做出选择(例如,
enum 与 enum class,原始指针与智能指针)
何时不应使用
- 非 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 | 基类析构函数:应为公开虚函数或受保护的非虚函数 |
| 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 | 对 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 风格强制转换 (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_);
queue_.push(value);
}
std::optional<int> pop() {
std::scoped_lock lock(mutex_);
if (queue_.empty()) return std::nullopt;
auto val = queue_.front();
queue_.pop();
return val;
}
private:
std::queue<int> queue_;
mutable std::mutex mutex_;
};
反模式
- 持有锁时调用未知代码 (CP.22)
- 无条件等待 (
std::condition_variable 无谓词) (CP.42)
- 将
volatile 用于线程同步 (CP.8)
- 裸
lock()/unlock() 而不是 RAII (CP.20)
- 对简单任务使用共享数据而不是消息传递
模板与泛型编程 (T.*)
关键规则
| 规则 | 摘要 |
|---|
| T.1 | 使用模板来提升代码的抽象层次 |
| T.2 | 在泛型代码中使用概念来指定要求 |
| T.10 | 为所有模板参数指定概念 |
| T.20 | 避免使用“概念”而不使用 requires 来模拟概念 |
| T.41 | 仅在模板中需要时,才在类模板中定义好友函数 |
| T.47 | 避免带有未命名、未 lambda 化的参数的模板 |
| T.65 | 当转发参数是完美转发时,使用 auto&& |
概念
template<typename T>
concept Hashable = requires(T a) {
{ std::hash<T>{}(a) } -> std::convertible_to<std::size_t>;
};
template<Hashable T>
void process(const T& value) {
auto hash = std::hash<T>{}(value);
}
完美转发
template<typename... Args>
auto make_widget(Args&&... args) {
return std::make_unique<Widget>(std::forward<Args>(args)...);
}
反模式
- 对简单代码使用模板 (T.5)
- 模板使用的类型上非必要的限制
- 不指定概念(在 C++20 中)(T.10)
性能 (Per.*)
关键规则
| 规则 | 摘要 |
|---|
| Per.1 | 不要无理由地优化 |
| Per.2 | 不要过早优化 |
| Per.3 | 不要优化尚未配置的内容 |
| Per.4 | 不要假设复杂代码必然比简单代码快 |
| Per.5 | 先编写清晰的代码,然后再优化 |
| Per.7 | 设计以支持优化 |
| Per.10 | 依赖静态类型系统 |
要点
- 首先关注清晰的架构和正确的设计
- 使用 RAII 避免动态内存分配
- 当需要避免堆分配时,优先选择
std::array 而非 std::vector
- 将成员按照对齐要求从最大到最小排序,以最小化填充
- 通过
const/constexpr 为优化器提供尽可能多的信息
反模式
- 假设没有配置的代码中存在的瓶颈 (Per.3)
- 使用低效算法而不是更好的数据结构 (Per.13 -- 使用
O(n log n) 排序,而非 O(n^2) 冒泡排序)
- 在
std::array 足够时使用 std::vector 处理小尺寸
源文件与命名空间 (SF., SL.)
关键规则
| 规则 | 摘要 |
|---|
| SF.1 | 使用 .cpp 后缀表示实现文件,.h 表示接口文件 |
| SF.2 | 头文件不得包含对象定义或非内联函数定义 |
| SF.3 | 在头文件中使用 namespace 来限定全局作用域 |
| SF.4 | 头文件必须是自包含的 |
| SF.5 | .cpp 文件必须包含定义其接口的 .h 文件 |
| SF.6 | 使用 using namespace 指令进行转换,但对于库代码,在 .cpp 文件中使用它们 |
| SF.7 | 不要在头文件作用域中使用 using namespace |
| SF.8 | 为所有代码使用 #include 防护 |
| SF.21 | 不要使用未命名的命名空间在头文件中拥有内部链接 |
包含顺序
#include "widget.h"
#include <vector>
#include <boost/format.hpp>
#include "utils.h"
反模式
- 在头文件中使用
using namespace std; (SF.7)
- 包含顺序违反本地优先规则
- 缺少
#pragma once 或包含防护 (SF.8)
- 在头文件中定义非内联、非模板的函数 (SF.2)
补充资源