P: 理念
P.1: 在代码中直接表达你的想法
P.2: 用 ISO 标准 C++ 来编码
P.3: 表达你的设计意图
程序员应当熟悉:
P.4: 理想情况下,程序应当是静态类型安全的
但有些场景无法在编译器确定静态安全类型:
- union - 使用 variant(C++17 提供)
- 强制转换 - 尽可能减少其使用;使用模板有助于这点
- 数组退化 - 使用 span(来自 GSL)
- 范围错误 - 使用 span
- 窄化转换 - 尽可能减少其使用,必须使用时则使用 narrow 或者 narrow_cast(来自 GSL)
P.5: 编译期检查优先于运行时检查
P.6: 应当使无法在编译期进行的检查能够在运行时实施
示例中,尽量保障交互是变量生命周期的有效性及安全。
extern void f4(vector<int>&); // 分离编译,可能会被动态加载
extern void f4(span<int>); // 分离编译,可能会被动态加载
// NB: 这里假定调用代码是 ABI 兼容的,使用的是
// 兼容的 C++ 编译器和同一个 stdlib 实现
void g3(int n)
{
vector<int> v(n);
f4(v); // 传递引用,保留所有权
f4(span<int>{v}); // 传递视图,保留所有权
}
P.8: 不要泄漏任何资源
基于RAII
的方式而不用基于fopen
的方式避免内存泄漏。具体可参考资源管理。
void f(char* name)
{
ifstream input {name};
// ...
if (something) return; // OK: 没有泄漏
// ...
}
// BAD
void f(char* name)
{
FILE* input = fopen(name, "r");
// ...
if (something) return; // 不好的:如果 something == true 的话,将会泄漏一个文件句柄
// ...
fclose(input);
}
P.9: 不要浪费时间或空间
// BAD:每次都计算strlen
void lower(zstring s)
{
for (int i = 0; i < strlen(s); ++i) s[i] = tolower(s[i]);
}
P.10: 不可变数据优先于可变数据
具体参考:Con: 常量和不可变性
P.11: 把杂乱的构造封装起来,而别让其散布到代码中
P.12: 适当采用支持工具
- 并发工具
- 静态分析工具
- 测试工具
P.13: 适当采用支持程序库
I: 接口
I.1: 使接口明确
I.2: 避免非 const 全局变量
I.3: 避免使用单例
I.4: 使接口严格和强类型化
I.5: 说明前条件(如果有)
I.6: 优先使用 Expects() 来表达前条件
I.7: 说明后条件
I.8: 优先使用 Ensures() 来表达后条件
I.9: 当接口是模板时,用概念来文档化其参数
template<typename Iter, typename Val>
requires input_iterator<Iter> && equality_comparable_with<iter_value_t<Iter>, Val>
Iter find(Iter first, Iter last, Val v)
{
// ...
}
参见: 泛型编程和概念。
I.10: 使用异常来表明无法实施所要求的任务
I.11: 决不以原始指针(T*)或引用(T&)来传递所有权
I.12: 把不能为空的指针声明为 not_null
I.13: 不要只用一个指针来传递数组
void copy_n(const T* p, T* q, int n); // 从 [p:p+n) 复制到 [q:q+n)
->
void copy(span<const T> r, span<T> r2); // 将 r 复制给 r2
I.22: 避免全局对象之间进行复杂的初始化
复杂的初始化可能导致未定义的执行顺序。
I.23: 保持较少的函数参数数量
template<class InputIterator1, class InputIterator2, class OutputIterator, class Compare>
OutputIterator merge(InputIterator1 first1, InputIterator1 last1,
InputIterator2 first2, InputIterator2 last2,
OutputIterator result, Compare comp);
->
template<class InputRange1, class InputRange2, class OutputIterator>
OutputIterator merge(InputRange1 r1, InputRange2 r2, OutputIterator result);
I.24: 避免可以由同一组实参以不同顺序调用造成不同含义的相邻形参
I.25: 优先以空抽象类作为类层次的接口
空的(没有非静态成员数据)抽象类要比带有状态的基类更倾向于保持稳定。
class Shape { // 不好: 接口类中加载了数据
public:
Point center() const { return c; }
virtual void draw() const;
virtual void rotate(int);
// ...
private:
Point c;
vector<Point> outline;
Color col;
};
class Shape { // 有改进: Shape 是一个纯接口
public:
virtual Point center() const = 0; // 纯虚函数
virtual void draw() const = 0;
virtual void rotate(int) = 0;
// ...
// ... 没有数据成员 ...
// ...
virtual ~Shape() = default;
};
I.26: 当想要跨编译器的 ABI 时,使用一个 C 风格的语言子集
不同的编译器会实现不同的类的二进制布局,异常处理,函数名字,以及其他的实现细节。
I.27: 对于稳定的程序库 ABI,考虑使用 Pimpl 手法
由于私有数据成员参与类的内存布局,而私有成员函数参与重载决议, 对这些实现细节的改动都要求使用了这类的所有用户全部重新编译。而持有指向实现的指针(Pimpl)的 非多态的接口类,则可以将类的用户从其实现的改变隔离开来,其代价是一层间接。
I.30: 将有违规则的部分封装
F: 函数
F.1: 把有意义的操作“打包”成为精心命名的函数
F.2: 一个函数应当实施单一一项逻辑操作
F.3: 保持函数短小简洁
F.4: 如果函数可能必须在编译期进行求值,就将其声明为 constexpr
需要用 constexpr 来告诉编译器允许对其进行编译期求值。
constexpr int min(int x, int y) { return x < y ? x : y; }
void test(int v)
{
int m1 = min(-1, 2); // 可能进行编译期求值
constexpr int m2 = min(-1, 2); // 编译期求值
int m3 = min(-1, v); // 运行期求值
constexpr int m4 = min(-1, v); // 错误: 无法在编译期求值
}
F.5: 如果函数非常小,并且是时间敏感的,就将其声明为 inline
F.6: 如果函数必然不会抛出异常,就将其声明为 noexcept
如果不打算抛出异常的话,程序就会认为无法处理这种错误,并且应当尽早终止。把函数声明为 noexcept 对优化器有好处,因为其减少了可能的执行路径的数量。它也能使发生故障之后的退出动作有所加速。
F.7: 对于常规用法,应当接受 T* 或 T& 参数而不是智能指针
F.8: 优先采用纯函数
TOC