C++ Core Guidelines 阅读笔记(1):哲学与接口设计
发布于:
前言
C++ Core Guidelines 是 Bjarne Stroustrup 和 Herb Sutter 等人编写的 C++ 编程最佳实践指南。本文是系列阅读笔记的第一篇,重点讨论指南的哲学原则和接口设计规范。
1. 核心哲学原则
C++ Core Guidelines 建立在几个核心哲学原则之上,这些原则指导着整个指南的设计。
1.1 表达意图
代码应该清楚地表达程序员的意图,让代码自解释:
// 好的:意图明确
void draw_circle(const Circle& c);
void draw_rectangle(const Rectangle& r);
// 不好:意图不明确
void draw(const Shape& s); // 不够具体,不知道画什么
1.2 使用编译期检查
尽可能在编译期捕获错误,而不是等到运行时:
// 好的:编译期检查
template<typename T>
void process(const T& value) {
static_assert(std::is_arithmetic_v<T>, "T must be arithmetic");
// 编译期就能发现类型错误
}
// 不好:运行时检查
void process(const void* value) {
if (!value) return; // 运行时才发现错误
// 需要运行时测试才能发现问题
}
1.3 零开销抽象
C++ 的哲学是”零开销抽象”:使用抽象不应该带来运行时开销。
// 好的:零开销抽象
std::vector<int> vec; // 与手动管理内存性能相同
vec.push_back(42); // 内联,无函数调用开销
// 不好的抽象:带来不必要的开销
class Wrapper {
std::unique_ptr<int> ptr; // 不必要的间接访问
public:
int get() const { return *ptr; } // 额外的间接访问
};
1.4 类型安全
使用类型系统防止错误:
// 好的:类型安全
using UserId = int;
using ProductId = int;
void process_order(UserId user_id, ProductId product_id);
// 类型系统防止传错参数
// 不好:弱类型
void process_order(int user_id, int product_id);
// 可能传错:process_order(product_id, user_id);
1.5 资源安全
使用 RAII 自动管理资源:
// 好的:资源安全
{
std::lock_guard<std::mutex> lock(mtx);
// 自动解锁,即使抛出异常
}
// 不好:手动管理
mtx.lock();
// 如果这里抛出异常,锁不会被释放
mtx.unlock();
2. 接口设计原则
接口是代码的契约,好的接口设计是高质量代码的基础。
2.1 接口应该小而完整
接口应该包含完成其职责所需的所有操作,但不应包含不相关的功能。
// 好的:接口小而完整
class File {
public:
void open(const std::string& path);
void close();
bool is_open() const;
size_t read(void* buffer, size_t size);
size_t write(const void* buffer, size_t size);
// 所有操作都与文件 I/O 相关
};
// 不好:接口太大,包含不相关功能
class File {
public:
void open(const std::string& path);
void close();
void compress(); // 不相关:压缩不是文件 I/O 的核心功能
void encrypt(); // 不相关:加密不是文件 I/O 的核心功能
};
2.2 优先使用抽象接口
抽象接口提供了灵活性和可测试性:
// 好的:抽象接口
class Drawable {
public:
virtual void draw() const = 0;
virtual ~Drawable() = default;
};
class Circle : public Drawable {
public:
void draw() const override {
// 具体实现
}
};
// 不好的:具体实现
class Circle {
public:
void draw() const {
// 具体实现,难以测试和扩展
}
};
2.3 接口应该明确所有权
所有权语义应该通过类型系统明确表达:
// 好的:明确所有权
std::unique_ptr<Resource> create_resource(); // 返回所有权
void process(const Resource& r); // 不拥有,只观察
void take_ownership(std::unique_ptr<Resource> r); // 接受所有权
// 不好:所有权不明确
Resource* create_resource(); // 谁负责删除?
void process(Resource* r); // 是否拥有资源?
2.4 接口应该稳定
接口一旦发布,应该保持稳定,避免频繁变化:
// 好的:稳定接口
class Database {
public:
virtual void connect() = 0;
virtual void disconnect() = 0;
virtual ~Database() = default;
// 接口稳定,可以添加新方法但不破坏现有代码
};
// 不好:频繁变化的接口
class Database {
public:
virtual void connect() = 0;
virtual void connect_v2() = 0; // 版本号在接口中,不稳定
virtual void disconnect() = 0;
};
2.5 接口应该易于使用
接口应该让正确的事情容易做,错误的事情难以做:
// 好的:易于使用
class File {
public:
explicit File(const std::string& path); // 构造时打开
~File(); // 自动关闭
// 使用简单,不容易出错
};
// 不好:容易出错
class File {
public:
File(); // 默认构造
void open(const std::string& path); // 需要手动调用
// 容易忘记调用 open
};
3. 参数传递规则
参数传递方式的选择直接影响代码的性能和可读性。
3.1 值传递 vs 引用传递
参数传递规则取决于对象大小和用途:
// 小对象(< 3 words):值传递
void process(int x, double y); // 小对象,值传递更高效
// 大对象:const 引用传递
void process(const std::vector<int>& vec); // 避免拷贝
// 需要修改:非 const 引用传递
void modify(std::vector<int>& vec); // 明确表示会修改
// 移动语义:右值引用传递
void take_ownership(std::vector<int>&& vec); // 接受所有权
3.2 参数传递的性能影响
// 性能对比示例
void by_value(std::vector<int> vec) {
// 拷贝整个 vector,开销大
}
void by_const_ref(const std::vector<int>& vec) {
// 只传递引用,无拷贝开销
}
void by_rvalue_ref(std::vector<int>&& vec) {
// 移动,开销小
}
// 使用
std::vector<int> data(1000000);
by_value(data); // 拷贝 1000000 个元素
by_const_ref(data); // 无拷贝
by_rvalue_ref(std::move(data)); // 移动,开销小
3.3 输出参数
优先使用返回值而非输出参数:
// 好的:返回值
std::vector<int> compute_result() {
std::vector<int> result;
// 计算
return result; // 移动返回,高效
}
// 不好:输出参数
void compute_result(std::vector<int>& out) {
// 不直观,需要先创建 out
out.clear();
// 计算
}
3.4 完美转发
使用完美转发保持参数的值类别:
// 好的:完美转发
template<typename... Args>
auto make_unique(Args&&... args) {
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}
// 使用
auto ptr1 = make_unique<MyClass>(42); // 值传递
auto ptr2 = make_unique<MyClass>(std::move(x)); // 移动传递
4. 函数设计
4.1 函数应该做一件事
// 好的:单一职责
void save_to_file(const std::string& filename, const Data& data);
// 不好:做多件事
void process_and_save(Data& data); // 处理 + 保存
4.2 函数应该简短
// 好的:简短函数
bool is_valid(const User& user) {
return !user.name.empty() && user.age > 0;
}
// 不好:过长函数
void process_user(User& user) {
// 100+ 行代码
}
5. 命名规范
5.1 使用有意义的名称
// 好的:名称有意义
void calculate_total_price(const std::vector<Item>& items);
// 不好:名称无意义
void calc(const std::vector<Item>& v);
5.2 遵循命名约定
// 类型:PascalCase
class UserAccount {};
// 函数:snake_case 或 camelCase
void get_user_name();
void getUserName();
// 常量:UPPER_CASE
const int MAX_SIZE = 100;
6. 错误处理
6.1 使用异常表示错误
// 好的:使用异常
void open_file(const std::string& path) {
if (!file_exists(path)) {
throw std::runtime_error("File not found");
}
}
// 不好:使用错误码
int open_file(const std::string& path) {
if (!file_exists(path)) {
return -1; // 错误码
}
return 0;
}
6.2 不要忽略错误
// 好的:处理错误
try {
process_data();
} catch (const std::exception& e) {
log_error(e);
handle_error();
}
// 不好:忽略错误
try {
process_data();
} catch (...) {
// 忽略所有错误
}
7. 资源管理
7.1 使用 RAII
// 好的:RAII
{
std::lock_guard<std::mutex> lock(mtx);
// 自动解锁
}
// 不好:手动管理
mtx.lock();
// ... 如果这里抛出异常,锁不会被释放
mtx.unlock();
7.2 使用智能指针
// 好的:智能指针
std::unique_ptr<Resource> resource = create_resource();
// 不好:原始指针
Resource* resource = create_resource();
// 需要手动 delete
8. 类型安全
8.1 避免类型转换
// 好的:类型安全
int value = get_int_value();
// 不好:类型转换
int value = (int)get_double_value(); // 不安全的转换
8.2 使用强类型
// 好的:强类型
using UserId = int;
using ProductId = int;
void process_user(UserId id); // 类型安全
// 不好:弱类型
void process_user(int id); // 可能传错类型
9. 性能考虑
9.1 避免不必要的拷贝
// 好的:移动语义
std::vector<int> create_data() {
std::vector<int> data;
// ...
return data; // 移动,不拷贝
}
// 不好:不必要的拷贝
std::vector<int> data = create_data(); // 可能拷贝
9.2 使用 const 引用
// 好的:const 引用
void process(const std::string& s);
// 不好:值传递(大对象)
void process(std::string s); // 拷贝开销
11. 实际工程案例
11.1 案例:接口设计改进
问题:原始接口设计不合理
// 原始设计:接口混乱
class DataProcessor {
public:
void process(Data* data, int* result, bool* success);
// 所有权不明确,参数太多
};
改进:遵循 Core Guidelines
// 改进设计:清晰的接口
class DataProcessor {
public:
std::optional<ProcessedData> process(const Data& data);
// 返回值明确,参数清晰,所有权明确
};
11.2 案例:参数传递优化
问题:不必要的拷贝导致性能问题
// 原始代码:性能差
void process_data(std::vector<int> data) { // 拷贝
// 处理
}
改进:使用 const 引用
// 改进代码:性能好
void process_data(const std::vector<int>& data) { // 无拷贝
// 处理
}
12. 设计模式与架构原则
12.1 接口隔离原则
12.2 依赖倒置原则
13. 性能分析与优化
13.1 参数传递性能测试
// 性能测试
void benchmark() {
std::vector<int> large_vec(1000000);
// 测试值传递
auto start = std::chrono::high_resolution_clock::now();
process_by_value(large_vec);
auto end = std::chrono::high_resolution_clock::now();
// 值传递:慢(拷贝开销)
// 测试引用传递
start = std::chrono::high_resolution_clock::now();
process_by_ref(large_vec);
end = std::chrono::high_resolution_clock::now();
// 引用传递:快(无拷贝)
}
13.2 接口设计对性能的影响
14. 常见陷阱与最佳实践
14.1 常见陷阱
// 陷阱 1:接口太大
class GodObject {
// 包含所有功能,难以维护
};
// 陷阱 2:所有权不明确
Resource* create(); // 谁负责删除?
// 陷阱 3:参数传递错误
void process(std::vector<int> vec); // 大对象值传递,性能差
14.2 最佳实践检查清单
- 接口小而完整:只包含相关功能
- 明确所有权:使用智能指针表达所有权
- 参数传递正确:小对象值传递,大对象引用传递
- 返回值优先:使用返回值而非输出参数
- 类型安全:使用强类型避免错误
- 异常安全:使用异常表示错误
- 性能考虑:避免不必要的拷贝
15. 小结
C++ Core Guidelines 的哲学强调:表达意图、编译期检查、零开销抽象。
核心要点:
- 哲学原则:表达意图、编译期检查、零开销抽象、类型安全、资源安全
- 接口设计:小而完整、抽象接口、明确所有权、稳定、易于使用
- 参数传递:根据对象大小和用途选择传递方式
- 函数设计:做一件事、简短、参数少
- 命名规范:有意义、遵循约定
- 错误处理:使用异常、不忽略错误
- 资源管理:RAII、智能指针
- 类型安全:避免类型转换、使用强类型
- 性能考虑:避免不必要的拷贝、使用移动语义
遵循这些原则,可以写出更安全、更高效、更易维护的 C++ 代码。