I.1 Make interface explicit
不好的示例->
int round(double d)
{
return (round_up) ? ceil(d) : d; // don't: "invisible" dependency
}
这个例子我主要理解是这样的,函数名是round
但是控制逻辑中有在调用时非常可能被忽略的全局变量round_up
。我理解round_up
应该出现在参数列表里。但是这种设计非常有可能出现在类的成员函数里,设想如下代码片段(非原文)
class A{
int round(double d) {
return (round_up) ? ceil(d) : d;
}
round_up = true;
};
在round()
的调用点,我们还是要检查成员变量round_up
的值才能期待round()
给出正确的行为。原文最后有一个看似这种的guideline,就是如果函数比较简单,那么它不应该执行基于全局变量(namespace级别的变量)的逻辑。
I.4 Make interfaces precisely and strongly typed
作者强调了函数参数最好要typed,
void draw_rectangle(Point top_left, Point bottom_right);
void draw_rectangle(Point top_left, Size height_width);
draw_rectangle(p, Point{10, 20}); // two corners
draw_rectangle(p, Size{10, 20}); // one corner and a (height, width) pair
所有上述例子中,当参数直接使用数字时,很难推断出参数的含义。作者推荐使用enum作为boolean flag使用。
enable_lamp_options(lamp_option::on | lamp_option::animate_state_transitions);
所以,当一个函数接受超过两个bool变量作为参数时,这个函数的interface可能需要重新设计。作者再次强调参数的数值单位的重要性。在CppCon2017 Bjarne Stroustrup上做的报告有特殊讲到数值单位引起的bug导致一个NASA的火星卫星没有进入预定轨道,科研人员15年的研究付之东流。
I.5 State preconditions
推荐了GSL
的Expects()
函数。
I.6 Prefer Expects()
for expressing preconditions
再次推荐使用Expects()
,但是C++20估计都没有标准化。
I.7 State postconditions
下面代码为了确保res
作为整形不会溢出。这种情况在进行整数运算时可能会出现,例如对RGB图像进行处理时,大部分时间数据对象可能是uint8_t类型,数值在0-255之间。
int area(int height, int width)
{
auto res = height * width;
Ensures(res > 0);
return res;
}
另一个例子
void f() // better
{
char buffer[MAX];
// ...
memset(buffer, 0, sizeof(buffer));
Ensures(buffer[0] == 0);
}
此实例中,不加入Ensures()
时,编译器优化阶段可能会remove掉memset()
。
此外原文列出了一个多线程mutex锁的示例
void manipulate(Record& r) // best
{
lock_guard<mutex> _ {m};
// ...
}
改示例确保m将在函数结束时释放,无论是否发生Exception。
I.9 If an interfaceis a template, document its parameters using concepts.
使用concept
在编译期间对模板参数进行检查。特指出GCC6.1之后都对concept
进行了支持。
I.10 Use exceptions to signal a failure to perfomr a required task
作者认为对于定义好的error,应该抛出异常而不是利用返回值表示状态。特别指出performance 并不是拒绝使用exception的好理由。若可以今早检查异常,可以提升critical code的performance。
I.11 Never transfer ownership by a raw pointer or reference
这里强调了rvalue 和move semantics。
I.12 Declare a pointer that must not be null as not_null
Wow, that’s nice.
int length(const char* p); // it is not clear whether length(nullptr) is valid
length(nullptr); // OK?
int length(not_null<const char*> p); // better: we can assume that p cannot be nullptr
int length(const char* p); // we must assume that p can be nullptr
not_null
定义在GSL
中。
I.13 Do not pass an array as a single pointer
如下的代码可能产生多种错误
void copy_n(const T* p, T* q, int n); // copy from [p:p+n) to [q:q+n)
包括q
的大小不足n,p
的内容少于n。使用下述代码会更好
void copy(span<const T> r, span<T> r2); // copy r to r2
I.22 Avoid complex initialization of global objects
原文这里编号一下跳到了I.22。原文指出全局变量的初始化次序是不确定的,所以尽量不要使用。全局变量初始化依赖函数应该是constexpr
。
I.23 Keep the number of function arguments low
当函数的参数过多时,非常肯恩意味着“one function, one responsibility”的规则被打破。同时,过多的参数说明这些参数应当被抽象成type。
作者认为,4个参数以上的,都算是参数数量太多。
I.24 Avoid adjacent unrelated parameters of the same type
意思很简单,参数表里,如果两个参数的类型完全一致,但是参数次序不能调换,那么这种参数表容易被用错。
考虑将参数表定义成一个struct
,这样在调用函数时需要用变量名指定函数的每一个参数,降低了发生错误的风险。但是这样做代码边长了。有点像python的keyword argument。
I.25 Prefer abstract classes as interfaces to class hierarchies
意思也比较简单,不定义base class,而只用abstract class。在abstract class中,没有成员变量,所有借口都是=0的虚函数。