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 |
对于 “in” 参数,通过值传递便宜类型,其他通过 const& |
| F.20 |
对于 “out” 值,优先返回值而不是输出参数 |
| F.21 |
要返回多个 “out” 值,优先返回结构体 |
| 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 |
基类析构函数:公共虚拟或受保护非虚拟 |
| 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 |
| R.22 |
使用 make_shared() 制作 shared_ptrs |
智能指针使用
// 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("Failed to open: " + 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 |
初始化
// 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: Lambda 用于复杂 const 初始化
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 |
不要在每个函数中尝试捕获每个异常 |
异常层次结构
// 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("connection refused", 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() << '
';
}
// 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 获取多个互斥锁 |
| CP.22 |
永远不要在持有锁时调用未知代码 |
| CP.42 |
不要无条件下等待 |
| CP.44 |
记得给你的 lock_guards 和 unique_locks 命名 |
| 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: 互斥锁及其数据
std::condition_variable cv_;
std::queue<int> queue_;
};
多个互斥锁
// CP.21: std::scoped_lock 用于多个互斥锁(死锁自由)
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(使用 `’ |
'--endl` 强制刷新) |
|
// 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: 使用 '
' 而不是 endl
std::cout << "result: " << value << '
';
枚举(Enum.*)
关键规则
| 规则 |
摘要 |
| Enum.1 |
优先枚举而不是宏 |
| Enum.3 |
优先 enum class 而不是普通 enum |
| Enum.5 |
不要使用 ALL_CAPS 作为枚举器 |
| Enum.6 |
避免未命名枚举 |
// Enum.3 + Enum.5: 作用域枚举,无 ALL_CAPS
enum class Color { red, green, blue };
enum class LogLevel { debug, info, warning, error };
// 坏:普通枚举泄露名称,ALL_CAPS 与宏冲突
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 |
仅对宏使用 ALL_CAPS |
| NL.10 |
优先使用 underscore_style 名称 |
头文件保护
// SF.8: 包含保护(或 #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: 不是 ALL_CAPS(它不是宏)
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)
- 除了宏之外的任何东西使用 ALL_CAPS(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 使用 enum class(Enum.3)
- [ ] 而不是
0/NULL 使用 nullptr(ES.47)
- [ ] 没有收窄转换(ES.46)
- [ ] 没有 C 风格转换(ES.48)
- [ ] 单参数构造函数是
explicit(C.46)
- [ ] 应用零规则或五规则(C.20, C.21)
- [ ] 基类析构函数是公共虚拟或受保护非虚拟(C.35)
- [ ] 模板受到概念的约束(T.10)
- [ ] 头文件中没有
using namespace 在全局范围内(SF.7)
- [ ] 头文件有包含保护并且是自包含的(SF.8, SF.11)
- [ ] 锁使用 RAII(
scoped_lock/lock_guard)(CP.20)
- [ ] 异常是自定义类型,按值抛出,按引用捕获(E.14, E.15)
- [ ] 使用
' ' 而不是 std::endl(SL.io.50)
- [ ] 没有魔术数字(ES.45)