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