本节中的规则非常笼统。
哲学规则总结:
- P.1:直接在代码中表达想法
- P.2:用ISO标准C ++编写代码
- P.3:表达意图
- P.4:理想情况下,程序应该是静态类型安全的
- P.5:与运行时检查相比,更喜欢编译时检查
- P.6:在编译时无法检查的内容应该在运行时检查
- P.7:尽早发现运行时错误
- P.8:不要泄漏任何资源
- P.9:不要浪费时间或空间
- P.10: 更喜欢不可变数据而不是可变数据
- P.11:封装凌乱的结构,而不是遍布代码
- P.12:适当使用支持工具
- P.13:根据需要使用支持库
哲学规则通常不是机械式的检查, 然而,个人规则反映了这些哲学的主题。 没有哲学基础,更具体/明确/可检查的规则就缺乏基本原理。
P.1:直接在代码中表达想法
Reason
编译器不读取注释(或设计文档),也不会读取许多程序员。 代码中表达的内容定义了语义,并且(原则上)可以由编译器和其他工具检查。
Example
class Date {
// ...
public:
Month month() const; // do
int month(); // don't
// ...
};
month
的第一个声明明确是关于返回a month
而不是修改Date对象的状态。 第二个版本让读者猜测并为未捕获的错误打开更多可能性。
Example; bad
这个循环是std :: find
的限制形式:
void f(vector<string>& v)
{
string val;
cin >> val;
// ...
int index = -1; // bad, plus should use gsl::index
for (int i = 0; i < v.size(); ++i) {
if (v[i] == val) {
index = i;
break;
}
}
// ...
}
Example; good
意图更清晰的表达应该是:
void f(vector<string>& v)
{
string val;
cin >> val;
// ...
auto p = find(begin(v), end(v), val); // better
// ...
}
一个设计良好的库表达意图(what is to be done, rather than just how something is being done)远比直接使用语言功能更好。
C ++程序员应该了解标准库的基础知识,并在适当的地方使用它。 任何程序员都应该了解正在做的项目的基础库的知识,并在适当地方使用它们。 任何了解过这些指南的程序员都应该知道指南支持库,适时使用它在自己工作中或者学习当中。
Example
change_speed(double s); // bad: what does s signify?
// ...
change_speed(2.3);
一个好方法是明确双重的意义(new speed or delta on old speed?) 和使用的单位:
change_speed(Speed s); // better: the meaning of s is specified
// ...
change_speed(2.3); // error: no unit
change_speed(23m / 10s); // meters per second
我们本可以接受一个普通 (unit-less) double
as a delta,但这很容易出错。 如果我们想要绝对速度和增量,我们就会定义一个Delta
类型。
Enforcement
一般来说很难。
- 习惯性使用const(检查成员函数是否修改其对象;检查函数是否修改指针或引用传递的参数)
- cast是个标记((强制类型转换中和类型系统)
- 检测模仿标准库的代码(hard)
P.2:用ISO标准C ++编写代码
Reason
这是一套编写ISO标准C ++的指南。
Note
存在需要扩展的环境,例如,访问系统资源。 在这种情况下,本地化使用必要的扩展并使用非核心编码指南控制它们的使用。 如果可能,构建封装扩展的接口,以便可以在不支持这些扩展的系统上关闭或编译它们。
扩展通常没有严格定义的语义。 即使是多个编译器常见且由多个编译器实现的扩展,也可能具有略微不同的行为和边缘情况行为,这是由于没有严格的标准定义。 充分利用任何此类扩展后,预计可移植性将受到影响。
Note
使用有效的ISO C ++并不能保证可移植性(更不用说正确性)了。 避免依赖于未定义的行为(e.g., undefined order of evaluation)并且注意具有实现定义含义的构造(e.g., sizeof(int)
)。
Note
存在必须限制使用标准C ++语言或库特征的环境,例如,以避免飞行器控制软件标准所要求的动态存储器分配。 在这种情况下,通过针对特定环境定制的这些编码指南的扩展来控制它们的(dis)使用。
Enforcement
使用最新的C ++编译器(当前为C ++ 17,C ++ 14或C ++ 11),其中包含一组不接受扩展的选项。
P.3:表达意图
Reason
除非声明某些代码的意图(例如,在名称或注释中),否则无法判断代码是否完成了应该执行的操作。
Example
gsl::index i = 0;
while (i < v.size()) {
// ... do something with v[i] ...
}
这里没有表达“仅仅”循环v
元素的意图。 索引的实现细节被公开(因此可能被滥用),并且i
比循环的范围更长,这可能是也可能不是。 读者无法从这部分代码中了解到。
Better:
for (const auto& x : v) { /* do something with the value of x */ }
现在,没有明确提到迭代机制,并且循环操作对const元素的引用,以便不会发生意外修改。 如果需要修改,请说:
for (auto& x : v) { /* modify x */ }
有关for语句的更多详细信息,请参阅ES.71。 有时更好,使用命名算法:
for_each(v, [](int x) { /* do something with the value of x */ });
for_each(par, v, [](int x) { /* do something with the value of x */ });
最后一个变量清楚地表明我们对v
的元素的处理顺序不感兴趣。
程序员应该熟悉:
- The guidelines support library
- The ISO C++ Standard Library
- 无论什么基础库都可用于当前项目
Note
替代表述:说出应该做什么,而不仅仅是应该如何做。
Note
一些语言结构比其他语言结构表达意图更好
Example
如果两个int
s意味着是2D点的坐标,那么说:
draw_line(int, int, int, int); // obscure
draw_line(Point, Point); // clearer
Enforcement
寻找有更好选择的常见模式:
- simple
for
loops vs.range-for
loops
+f(T*, int)
interfaces vs.f(span<T>)
interfaces - 循环变量范围太大
- 赤裸裸的new和delete
- 具有许多内置类型参数的函数
这里是聪明和高级程序转换的巨大区别所在。
P.4:理想情况下,程序应该是静态类型安全的
Reason
理想情况下,程序应该是完全静态(编译时)类型安全的。不幸的是,这是不可能的。问题:
- unions
- casts(数据类型转换)
- array decay(阵列衰变)
- range errors
- narrowing conversions(缩小转换率)
Note
这些区域是严重问题的根源(例如,崩溃和安全违规)。 我们尝试提供替代技术。
Enforcement
我们可以根据需要单独禁止,限制或检测各个问题类别,并为各个程序提供可行性。 总是提出另一种选择。 例如:
- unions -- use
variant
(in C++17) - casts -- 尽量减少使用; 模板可以帮助
- array decay -- use
span
(from the GSL) - range errors -- use
span
+narrowing conversions -- 尽量减少使用,并在必要时使用narrow
或narrow_cast
(来自GSL)
P.5:与运行时检查相比,更喜欢编译时检查
Reason
代码清晰度和高效。 您不需要为编译时捕获的错误编写错误处理程序。
Example
// Int is an alias used for integers
int bits = 0; // don't: avoidable code
for (Int i = 1; i; i <<= 1)
++bits;
if (bits < 32)
cerr << "Int too small\n";
这个例子无法实现它想要实现的目标(因为溢出是未定义的),应该用简单的替换static_assert
:
// Int is an alias used for integers
static_assert(sizeof(Int) >= 4); // do: compile-time check
或者更好的方法是使用类型系统并用Int32_t
替换Int
。
Example
void read(int* p, int n); // read max n integers into *p
int a[100];
read(a, 1000); // bad, off the end
better
void read(span<int> r); // read into the range of integers r
int a[100];
read(a); // better: let the compiler figure out the number of elements
替代配方:尽量把类型安全判断放在编译时候去做
Enforcement
- 查找指针参数。
- 查找运行时参数可能违规范围。
P.6:在编译时无法检查的内容应该在运行时检查
Reason
在程序中留下难以检测的错误会导致崩溃和糟糕的结果。
Note
理想情况下,我们在编译时或运行时捕获所有错误(不是程序员逻辑中的错误)。 在编译时捕获所有错误是不可能的,并且通常无法在运行时捕获所有剩余错误。 但是,如果有足够的资源(分析程序,运行时检查,机器资源,时间),我们应该努力编写原则上可以检查的程序。
Example, bad
// separately compiled, possibly dynamically loaded
extern void f(int* p);
void g(int n)
{
// bad: the number of elements is not passed to f()
f(new int[n]);
}
在这里,一些关键的信息(元素的数量)被完全"obscured"了,以至于静态分析可能变得不可行,当f()
是ABI的一部分时,动态检查可能非常困难,因此我们无法"instrument" 指针。我们可以将有用的信息嵌入到免费存储中,但这需要对系统和编译器进行全局更改。这里的设计使得错误检测非常困难。
Example, bad
当然,我们可以通过指针传递元素的数量:
// separately compiled, possibly dynamically loaded
extern void f2(int* p, int n);
void g2(int n)
{
f2(new int[n], m); // bad: a wrong number of elements can be passed to f()
}
将元素的数量作为参数传递比仅传递指针并依赖于某些(未说明的)约定来更新(并且更常见),以便知道或发现元素的数量。 但是(如图所示),简单的拼写错误可能会引入严重的错误。 f2()
的两个参数之间的连接是常规的,而不是显式的。
另外,f2()
应该delete
它的参数(或者调用者犯了第二个错误?)
Example, bad
标准库资源管理指针在指向对象时无法传递大小:
// separately compiled, possibly dynamically loaded
// NB: this assumes the calling code is ABI-compatible, using a
// compatible C++ compiler and the same stdlib implementation
extern void f3(unique_ptr<int[]>, int n);
void g3(int n)
{
f3(make_unique<int[]>(n), m); // bad: pass ownership and size separately
}
Example
我们需要将指针和元素数作为一个整体对象传递:
extern void f4(vector<int>&); // separately compiled, possibly dynamically loaded
extern void f4(span<int>); // separately compiled, possibly dynamically loaded
// NB: this assumes the calling code is ABI-compatible, using a
// compatible C++ compiler and the same stdlib implementation
void g3(int n)
{
vector<int> v(n);
f4(v); // pass a reference, retain ownership
f4(span<int>{v}); // pass a view, retain ownership
}
这种设计将元素的数量作为对象的组成部分,假如错误是不可能的,在外部因素可以承担情况下,动态(运行时)总是进行动态检查。
Example
我们如何转移所有权和验证使用所需的所有信息?
vector<int> f5(int n) // OK: move
{
vector<int> v(n);
// ... initialize v ...
return v;
}
unique_ptr<int[]> f6(int n) // bad: loses n
{
auto p = make_unique<int[]>(n);
// ... initialize *p ...
return p;
}
owner<int*> f7(int n) // bad: loses n and we might forget to delete
{
owner<int*> p = new int[n];
// ... initialize *p ...
return p;
}
Example
- ???
- 展示如何通过传递多态基类的接口来避免可能的检查,当他们真正知道他们需要什么时? 或字符串作为 "free-style"选项
Enforcement
- 标志(指针,计数)式接口(这将标记许多由于兼容性原因无法修复的示例)
- ???
P.7: 尽早发现运行时错误
Reason
避免"mysterious"的崩溃。 避免导致(可能是无法识别的)错误结果。
Example
void increment1(int* p, int n) // bad: error-prone
{
for (int i = 0; i < n; ++i) ++p[i];
}
void use1(int m)
{
const int n = 10;
int a[n] = {};
// ...
increment1(a, m); // maybe typo, maybe m <= n is supposed
// but assume that m == 20
// ...
}
这里我们在use1
中犯了一个小错误,导致数据损坏或崩溃。 (指针,计数)式接口调用increment1()
,没有现实的方法来防御超出范围的错误。 如果我们可以检查超出范围访问的下标,那么在访问p [10]
之前不会发现错误。 我们可以提前检查并改进代码:
void increment2(span<int> p)
{
for (int& x : p) ++x;
}
void use2(int m)
{
const int n = 10;
int a[n] = {};
// ...
increment2({a, m}); // maybe typo, maybe m <= n is supposed
// ...
}
现在,可以在定义的时候(早期)而不是稍后检查m <= n
。 如果我们只有一个拼写错误,以便我们打算使用n作为界限,那么代码可以进一步简化(消除错误的可能性):
void use3(int m)
{
const int n = 10;
int a[n] = {};
// ...
increment2(a); // the number of elements of a need not be repeated
// ...
}
Example, bad
不要反复检查相同的值。 不要将结构化数据作为字符串传递:
Date read_date(istream& is); // read date from istream
Date extract_date(const string& s); // extract date from string
void user1(const string& date) // manipulate date
{
auto d = extract_date(date);
// ...
}
void user2()
{
Date d = read_date(cin);
// ...
user1(d.to_string());
// ...
}
日期验证两次(由Date
构造函数)并作为字符串(非结构化数据)传递。
Example
过度检查可能代价高昂。 有些情况下,早期检查是愚蠢的,因为您可能不需要该值,或者可能只需要比整体更容易检查的部分值。 同样,不要添加更改接口渐近性的有效性检查(例如,不要向平均复杂度为O(1)
的接口添加O(n)
检查。
class Jet { // Physics says: e * e < x * x + y * y + z * z
float x;
float y;
float z;
float e;
public:
Jet(float x, float y, float z, float e)
:x(x), y(y), z(z), e(e)
{
// Should I check here that the values are physically meaningful?
}
float m() const
{
// Should I handle the degenerate case here?
return sqrt(x * x + y * y + z * z - e * e);
}
???
};
射频的物理定律(e * e <x * x + y * y + z * z)不是不变量,因为可能存在测量误差。
Enforcement
- 查看指针和数组:尽早进行范围检查,而不是重复检查
- 查看转化次数:消除或标记缩小的转化次数
- 查找来自输入的未经检查的值
- 查找结构化数据(具有不变量的类的对象)转换为字符串
- ???
P.8:不要泄漏任何资源
Reason
随着时间的推移,即使资源的缓慢增长也会耗尽这些资源的可用性。 这对于长期运行的程序尤为重要,但却是负责任的编程行为的重要组成部分。
Example, bad
void f(char* name)
{
FILE* input = fopen(name, "r");
// ...
if (something) return; // bad: if something == true, a file handle is leaked
// ...
fclose(input);
}
Prefer RAII:
void f(char* name)
{
ifstream input {name};
// ...
if (something) return; // OK: no leak
// ...
}
See also: The resource management section
Note
泄漏通俗地称为“任何未清理的东西”。 更重要的分类是“任何无法再清理的东西”。 例如,在堆上分配一个对象,然后失去指向该分配的最后一个指针。 此规则不应该被理解为要求长期对象中分配必须计划停机期间将返回。 例如,依赖系统保证的清理(例如文件关闭和进程关闭时的内存释放)可以简化代码。 但是,依赖隐式清理的抽象可以简单,通常更安全。
Note
实施终身安全配置可消除泄漏。 当与RAII提供的资源安全相结合时,它消除了“垃圾收集”的需要(通过不产生垃圾)。 将此与类型和边界配置文件的强制执行相结合,您可以获得完整的类型和资源安全性,并由工具保证。
Enforcement
- 查看指针:将它们分类为非所有者(默认)和所有者。 在可行的情况下,使用标准库资源句柄替换所有者(如上例所示)。 或者,使用GSL的
owner
标记所有者。 - 寻找裸
new
和delete
- 查找返回原始指针的已知资源分配函数(例如
fopen
,malloc
和strdup
)
P.9:不要浪费时间或空间
Reason
This is C++.
Note
您没有浪费时间和空间来实现目标(例如,开发速度,资源安全性或测试的简化)。 “追求效率的另一个好处是,这个过程迫使你更深入地理解这个问题。” -- Alex Stepanov
Example, bad
struct X {
char ch;
int i;
string s;
char ch2;
X& operator=(const X& a);
X(const X&);
};
X waste(const char* p)
{
if (!p) throw Nullptr_error{};
int n = strlen(p);
auto buf = new char[n];
if (!buf) throw Allocation_error{};
for (int i = 0; i < n; ++i) buf[i] = p[i];
// ... manipulate buffer ...
X x;
x.ch = 'a';
x.s = string(n); // give x.s space for *p
for (gsl::index i = 0; i < x.s.size(); ++i) x.s[i] = buf[i]; // copy buf into x.s
delete[] buf;
return x;
}
void driver()
{
X x = waste("Typical argument");
// ...
}
是的,这是一种讽刺,但我们已经看到在生产代码,更糟的是每一个人的错误。 请注意,X的布局保证浪费至少6个字节(并且很可能更多)。 复制操作的虚假定义会禁用移动语义,因此返回操作很慢(请注意,此处不保证返回值优化,RVO)。 对buf
使用new
和delete
是多余的; 如果我们真的需要一个本地字符串,我们应该使用本地字符串。 其他还有一些性能缺陷和无偿的并发症。
Example, bad
void lower(zstring s)
{
for (int i = 0; i < strlen(s); ++i) s[i] = tolower(s[i]);
}
是的,这是从实际项目中代码的例子。 我们留给读者来弄清楚浪费了什么。
Note
浪费(时间或者空间)的个别例子很少有重要意义,如果是重要的,通常很容易被专家消除。 然而,在代码库中广泛传播的浪费很容易变得非常重要,专家并不总是像我们希望的那样可用。 此规则的目标(以及支持它的更具体的规则)是在C ++发生之前消除与使用C ++相关的大多数浪费。 之后,我们可以查看与算法和要求相关的浪费,但这超出了这些指南的范围。
Enforcement
许多更具体的规则旨在实现简化和消除无偿浪费的总体目标。
P.10: 更喜欢不可变数据而不是可变数据
Reason
关于常数而不是关于变量的推理更容易。 不可变的东西不能意外地改变。 有时,不变性可以实现更好的优化。 您不能在常量上进行数据竞争。
See Con: Constants and immutability
P.11:封装凌乱的结构,而不是传播代码
Reason
凌乱的代码更容易隐藏错误,更难写。 良好的界面使用起来更简单,更安全。 凌乱的低级代码会产生更多此类代码。
Example
int sz = 100;
int* p = (int*) malloc(sizeof(int) * sz);
int count = 0;
// ...
for (;;) {
// ... read an int into x, exit loop if end of file is reached ...
// ... check that x is valid ...
if (count == sz)
p = (int*) realloc(p, sizeof(int) * sz * 2);
p[count++] = x;
// ...
}
这是低级的,冗长的,容易出错的。 例如,我们“忘记”测试内存耗尽。 相反,我们可以使用vector:
vector<int> v;
v.reserve(100);
// ...
for (int x; cin >> x; ) {
// ... check that x is valid ...
v.push_back(x);
}
Note
标准库和GSL就是这种理念的例子。 例如,我们使用所设计的库,而不是搞乱实现关键抽象(例如vector,span,lock_guard和future)所需的数组,联合,强制转换,棘手的生命周期问题,gsl :: owner等。 由比我们通常拥有的更多时间和专业知识的人实施。 同样,我们可以而且应该设计和实现更专业的库,而不是让用户(通常是我们自己)面临重复获得低级代码的挑战。 这是超集原则子集的变体,是这些准则的基础。
Enforcement
- 寻找“杂乱的代码”,例如复杂的指针操作和在抽象实现之外的转换。
P.12:适当使用支持工具
Reason
“通过机器”可以做得更好。 计算机不会偷懒或厌倦重复性任务。 我们通常有比做反复做常规任务更好的事情。
Example
运行静态分析器以验证您的代码是否遵循您希望它遵循的准则。
Note Seen
- Static analysis tools(静态分析)
- Concurrency tools(并行分析)
-
Testing tools(测试分析)
还有许多其他类型的工具,例如源代码存储库,构建工具等,但这些工具超出了这些指南的范围。
Note
注意不要依赖于过于复杂或过度专业化的工具链。 这些可以使您的便携式代码不可移植。
P.13:根据需要使用支持库
Reason
使用设计良好,文档齐全且支持良好的库可节省时间和精力; 如果您的大部分时间都花在实施上,那么它的质量和文档可能会比您可以做的更大。 图书馆的成本(时间,精力,金钱等)可以在许多用户之间共享。 与单个应用程序相比,广泛使用的库更有可能保持最新并移植到新系统。 了解广泛使用的库可以节省其他/未来项目的时间。 因此,如果您的应用程序域存在合适的库,请使用它。
Example
std::sort(begin(v), end(v), std::greater<>());
除非您是排序算法的专家并且有足够的时间,否则这更可能是正确的,并且比您为特定应用程序编写的任何内容运行得更快。 您需要一个不使用标准库(或您的应用程序使用的任何基础库)而不是使用它的理由的理由。
Note 默认使用:
Note
如果没有为重要域存在设计良好,文档齐全且支持良好的库,那么您可能应该设计并实现它,然后使用它。
网友评论