当前位置:   article > 正文

c++核心指南--Philosophy

c++核心指南

本节的规则非常笼统。

理念规则摘要:

理念化规则是笼统的、不能机械的检验的。.然而,反映这些理念主题的单个规则是可以机械的检验的。没有理念基础,更具体/特殊/可检验的规则缺乏合理性。

P.1: 直接用代码表达思想

原因

编译器不读注释(或设计文档),许多程序员也不读注释(或设计文档)。用代码表示的内容已经定义了语义,可以(原则上)由编译器和其他工具检查。

例子
class Date {
public:
    Month month() const;  // do
    int month();          // don't
    // ...
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

month声明的第一个版本明确返回 Month同时也明确表达不会改变 Date 对象的状态。
第二个版本则让读者猜测,也为未捕捉的bug打开了更多的可能性。

坏的例子

里面的for循环是std::find的受限形式

void f(vector<string>& v)
{
    string val;
    cin >> val;
    // ...
    int index = -1;   // 不好,外加应该使用 gsl::index
    for (int i = 0; i < v.size(); ++i) {
        if (v[i] == val) {
            index = i;
            break;
        }
    }
    // ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
好的例子

更加清晰的意图表达应该是:

void f(vector<string>& v)
{
    string val;
    cin >> val;
    // ...
    auto p = find(begin(v), end(v), val);  // 很好
    // ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

设计良好的库比直接使用语言功能更能表达意图 (要做什么,而不仅仅是如何做某事)。一个C++程序员应该知道标准库的基础知识,并适时的使用它。任何程序员都应该知道正在进行的项目所使用的基础库的基础知识,并适当地使用它们。任何使用这些指南的程序员都应该知道指南支持库,并适当地使用它。

示例
change_speed(double s);   // 不好:s 表示什么?
// ...
change_speed(2.3);
  • 1
  • 2
  • 3

更好的方法是明确double的含义(新速度还是旧速度的增量?)和所使用的单位:

change_speed(Speed s);    // 较好:指明了s的含义
// ...
change_speed(2.3);        // 错误: 没有单位
change_speed(23m / 10s);  // 米/秒
  • 1
  • 2
  • 3
  • 4

本来,我们可以接受一个普通的(无单位的)“double”作为增量,但是那样容易出错。如果我们既需要绝对速度有需要增量,我们应该定义一个“增量”类型。

实施(Enforcement)

总的来说很难。.

  • 始终使用const (检查成员函数是否修改其对象;检查函数是否修改通过指针或引用传递的参数)
  • flag uses of casts (类型转换会使类型系统失效)
  • 检测模仿标准库的代码 (难)

P.2: 用ISO标准c++编写代码

原因

这本来就是编写ISO标准c++的一套指导原则.

Note

有些运行环境需要扩展,例如访问系统资源。在这种情况下要在局部使用必要的扩展并使用用非核心编码指南。如果可能,构建封装扩展的接口,以便可以在不支持这些扩展的系统上关闭扩展代码或不编译扩展代码。扩展通常没有严格定义的语义。由于没有严格的标准定义,甚至那些通用的、有多个编译器编译的扩展,可能有细微不同的行为和不同的边界情况行为。大量使用这种扩展将会阻碍可移植性。

Note

使用有效的ISO C++不保证可移植性(更不用说正确性了)。
不要依赖未定义的行为 (e.g., undefined order of evaluation)并且要了解具有明确意义的实现的结构(例如, sizeof(int))。

Note

有些环境需要限制标准C++或库特性的使用,例如,航空控制软件标准要求避免动态分配内存。在这种情况下,要通过那些为特定环境定制的编码标准的扩展来控制核心指南的使用与否。

实施

使用一个最新的,带有一组不接受扩展的选项的C++编译器(目前是C++17、C++14或C++11)。

P.3: 明示意图

原因

不说明某些代码的意图(例如,在名称或注释中),就不可能判断这些代码是否完成了它应该做的事情。

示例
gsl::index i = 0;
while (i < v.size()) {
    // ... do something with v[i] ...
}
  • 1
  • 2
  • 3
  • 4

这段代码并没有表达“仅仅”遍历v 中元素的意图。暴露了索引(index)的实现细节 (这样它可能会被滥用), 并且i 的生存期超过了循环的作用域,这可能是期望的也可能不是期望的。读者无法从上述代码片段获知。

改进:

for (const auto& x : v) { /* do something with the value of x */ }
  • 1

现在,没有明确提到迭代机制,循环对const元素的引用进行操作,因此不会发生意外的修改。如果需要修改,这么写:

for (auto& x : v) { /* modify x */ }
  • 1

for语句的更多细节, 可参考ES.71.
有时更好的做法是,使用一个命名的算法. 这个例子使用了Ranges 技术规范中的 for_each ,他直接表达了意图:

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 */ });
  • 1
  • 2

The last variant makes it clear that we are not interested in the order in which the elements of v are handled.

程序员应该熟悉:

Note

另一种说法:说应该做什么,而不是只说应该怎么做.

Note

有些语言结构比其他语言结构更能表达意图.

示例

如果用两个 int来表示二维点, 比如这么写:

draw_line(int, int, int, int);  // 模糊
draw_line(Point, Point);        // 清晰
  • 1
  • 2
Enforcement

寻找那些有更好替代方案的通用模式。

  • 简单for 循环vs. 范围-for 循环
  • f(T*, int) 接口 vs. f(span<T>) 接口
  • 太大的循环作用域的循环变量
  • 原始newdelete
  • 带有非常多内置类型参数的函数

智能化和半自动化程序转换有着巨大的发展空间。

P.4: 理想情况下,程序应该是静态类型安全的

原因

理想情况下,程序应该是完全静态(编译时)类型安全的。
不幸的是,这是不可能的。有问题的领域:

  • unions
  • 类型转换
  • array decay
  • range errors
  • 窄化转换
Note

这些领域的问题是严重问题的来源 (例如,崩溃和安全侵犯).
我们尽力提供替代技术。

Enforcement

我们可以根据单个程序的需要和可行性,分别禁止、约束或检测单个问题类别。总是建议另一种选择。例如:

  • unions – 使用variant (in C++17)
  • 类型转换-- 尽量减少它们的使用; 模板
  • array decay – use span (from the GSL)
  • range errors – use span
  • 窄化转换 – minimize their use and use narrow or narrow_cast (from the GSL) where they are necessary

P.5: 优先使用编译时检查而不是运行时检查

原因

代码清晰度和性能。
您不必为编译时捕获的错误编写错误处理程序。

示例
// Int是用来表示整数的别名
int bits = 0;         // 不要:可以避免的代码
for (Int i = 1; i; i <<= 1)
    ++bits;
if (bits < 32)
    cerr << "Int too small\n";
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

此示例未能实现它的目标(因为溢出是未定义的),应将其替换为简单的:static_assert:

// Int is an alias used for integers
static_assert(sizeof(Int) >= 4);    // 好: 编译时检查
  • 1
  • 2

或者更好的方法是使用类型系统并将“Int”替换为“int32”。

示例
void read(int* p, int n);   // 将最大的n个整数读进 *p

int a[100];
read(a, 1000);    // 错误, 数组越界
  • 1
  • 2
  • 3
  • 4

改进

void read(span<int> r); // 读入span<int> r

int a[100];
read(a);        // 比较好:让编译器计算元素数量
  • 1
  • 2
  • 3
  • 4

Alternative formulation: 不要把在编译时能做好的事情推迟到运行时。

Enforcement
  • 寻找指针参数。
  • 寻找对越界的运行时检查。

P.6: 不能在编译时检查的内容应该应该在运行时进行检查

原因

在程序中留下难以检测的错误是在主动要求崩溃和错误的结果

Note

理想情况下,我们在编译时或运行时捕获所有错误(不是程序员逻辑错误)。但实际情况是,在编译时捕获所有错误是不可能的,而且在运行时捕获所有剩余的错误通常也负担不起。但是,我们应该努力编写原则上可以检查的程序,只要有足够的资源 (分析工具,运行时检查,机器资源,时间).

反例
// f是分别编译的,而且有可能是动态加载的。
extern void f(int* p);

void g(int n)
{
    // 错误:元素的数量没有传递给 f()
    f(new int[n]);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

在这里,一个重要的信息(元素的数量)已经被彻底地“模糊”了,以至于静态分析可能变得不可行,当f()是ABI的一部分我们无法“监控”该指针时,动态检查可能非常困难。我们可以在自由存储中嵌入有用的信息,但这需要对系统和编译器进行全局更改. 我们这里的设计使得错误检测非常困难。

反例

当然,我们可以随指针传递元素的数量:

// separately compiled, possibly dynamically loaded
extern void f2(int* p, int n);

void g2(int n)
{
    f2(new int[n], m);  // bad: 有可能将错误的元素数量传递给f()
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

作为参数传递元素的数量比仅仅传递指针和依赖某种(未声明的)约定来知道或发现元素的数量要好(而且更常见)。但是(如上所示),一个简单的打字错误会导致严重的错误。f2()的两个参数之间的联系是常规的,而不是显式的。

而且, 认为 f2()delete 它的参数也不明确 (或者调用者犯了第二个错误?)。

反例

The standard library resource management pointers fail to pass the size when they point to an object:

// f3s是分别编译的,有可能是动态加载的
// NB:假设调用代码是ABI兼容的,即使用兼容的C++编译器和相同的stdlib实现。
extern void f3(unique_ptr<int[]>, int n);

void g3(int n)
{
    f3(make_unique<int[]>(n), m);    // 不好: 分别传递所有权和大小
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
示例

我们需要将指针和元素数量作为一个整体对象进行传递:

extern void f4(vector<int>&);   // separately compiled, possibly dynamically loaded
extern void f4(span<int>);      // separately compiled, possibly dynamically loaded
                                // NB:假设调用代码是ABI兼容的(即使用兼容的C++编译器和相同的stdlib实现)。

void g3(int n)
{
    vector<int> v(n);
    f4(v);                     // 传递参照,保留所有权
    f4(span<int>{v});          // 传递快照,保留所有权
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

这种设计将元素的数量作为对象的一个不可分割的部分进行传递,因此不太可能出现错误,而且动态(运行时)检查始终是可行的,尽管并不总是负担得起。

示例

我们该如何转移所有权以及为了有效使用它所必须的信息呢?

vector<int> f5(int n)    // OK: move
{
    vector<int> v(n);
    // ... initialize v ...
    return v;
}

unique_ptr<int[]> f6(int n)    // 不好:n 丢失
{
    auto p = make_unique<int[]>(n);
    // ... initialize *p ...
    return p;
}

owner<int*> f7(int n)    // 不好:n 丢失,而且有可能会忘记delete
{
    owner<int*> p = new int[n];
    // ... initialize *p ...
    return p;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
例子
  • ???
  • 当他们真的知道他们需要什么的时候,展示如何通过传递多态基类的方式避免可能的检查。
    Or strings as “free-style” options
Enforcement
  • Flag (pointer, count)-style interfaces (this will flag a lot of examples that can’t be fixed for compatibility reasons)
  • ???

P.7: 尽早捕获运行时错误

原因

避免"莫名其妙"的 崩溃。
避免导致 (可能没有被意识到) 不正确结果的错误。

Example
void increment1(int* p, int n)    // 不好: 容易出错
{
    for (int i = 0; i < n; ++i) ++p[i];
}

void use1(int m)
{
    const int n = 10;
    int a[n] = {};
    // ...
    increment1(a, m);   // 也许是打字错误, 也许假设 m <= n
                        // 但是假设 m == 20
    // ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

在这里,我们在 use1中犯了一个小错误,这将导致数据损坏或崩溃。
(pointer, count)-风格的接口 使得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
    // ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

现在, m <= n在调用点被检查(早期) 而不是后面才被检查。
如果只是有个打字错误(我们本来想用n 作为边界),上述代码可以进一步简化(消除了错误的可能性):

void use3(int m)
{
    const int n = 10;
    int a[n] = {};
    // ...
    increment2(a);   // 不需要在此传入a的元素的个数
    // ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

.

反例

不要重复检查同一个值。不要将结构化数据作为字符串传递:

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());
    // ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

date 被验证了两次 (通过 Date 构建函数) 并且通过字符串进行传递 (非结构化数据).

示例

过多的检查成本很高。
有些情况下,早期检查是低效的,因为您可能永远不需要该值,或者可能只需要比整体更容易检查的部分值。 Similarly, don’t add validity checks that change the asymptotic behavior of your interface (e.g., don’t add a O(n) check to an interface with an average complexity of O(1)).

class Jet {    // 物理定律规定: 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)
    {
        // 应该在此处检查值的物理意义吗?
    }

    float m() const
    {
        // 应该在此处处理退化情况吗?
        return sqrt(x * x + y * y + z * z - e * e);
    }

    ???
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

由于存在测量误差,有关 jet (e * e < x * x + y * y + z * z)的物理定律并不是一个不变量。

???

Enforcement
  • 查看指针和数组:尽早进行范围检查,不要重复。
  • 查看转换:消除或标记缩小转换。
  • 查找来自输入的未检查值
  • 寻找被转换成字符串的结构化数据(具有不变量的类的对象)L。
  • ???

P.8: 不要泄露任何资源

原因

即使资源消耗增长缓慢,随着时间的推移,也将耗尽资源。这对于长时间运行的程序来说尤其重要,是负责任的编程行为的重要组成部分。

反例
void f(char* name)
{
    FILE* input = fopen(name, "r");
    // ...
    if (something) return;   // 不好: 如果 something == true,文件句柄input就泄露了。
    // ...
    fclose(input);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

改进RAII:

void f(char* name)
{
    ifstream input {name};
    // ...
    if (something) return;   // OK: 没有泄露
    // ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

See also: The resource management section

Note

通俗地说,泄露是“任何没有清理干净的东西”。
更重要的分类是“任何不能再清理的东西”。例如,在堆上分配一个对象,然后丢失指向该对象的最后一个指针。不应将此规则视为要求在长效对象内的分配必须在程序关闭期间被回收。例如,依赖于系统保证的清理(例如在进程退出时关闭文件和释放内存)可以简化代码。然而,依靠隐式清理是简单的,而且通常更安全。

Note

实施the lifetime safety profile 可以消除泄露。
当同RAII提供的资源安全措施结合时, 它消除了“垃圾回收”的需要(通过不产生垃圾)。将此与类型和边界配置文件的实施相结合,您将获得由工具保证的完整的类型和资源安全性。

Enforcement
  • 检查指针:将它们分为非所有者指针(默认)和所有者指针。
    在可行的情况下,用标准库资源句柄替换所有者指针(如上例所示)。或者,使用the GSL中的owner标记所有者。
  • 寻找原始 newdelete
  • 查找已知的返回原始指针的资源分配函数 (比如 fopen, malloc, 和strdup)

P.9: 不要浪费时间或空间

原因

这可是 C++.

Note

您为实现目标而花费的时间和空间(例如,开发速度、资源安全或测试简化)不算是浪费。
“追求效率的另一个好处是,这个过程迫使你更深入地理解问题。.” - Alex Stepanov

反例
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");
    // ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

上面的写法有点夸张,但其中的每个单一的错误我们都曾经在生产代码中遇到过,而且比上面的更糟糕。注意,X的布局至少浪费了6个字节(很可能更多)。拷贝操作的错误定义是移动语义变得不可能,所以返回操作较慢(注意返回值优化,RVO,不一定会发生)。 bufnewdelete操作是多余的;如果我们确实需要一个局部字符串,我们应该使用一个局部 string。除此之外,还有更多的性能缺陷和无端的复杂性。

反例
void lower(zstring s)
{
    for (int i = 0; i < strlen(s); ++i) s[i] = tolower(s[i]);
}
  • 1
  • 2
  • 3
  • 4

这是一个来自生产代码的真实的例子。可以看到,在条件语句里有个表达式 i < strlen(s)。这个表达式在每次循环中都会被计算,这就意味着每次循环 strlen 都必须遍历整个字符串来发现自己的长度。尽管字符串内容是改变的,但假设 toLower 不会影响到字符串的长度,因此,在循环外计算长度以免去每次循环都计算长度的负担,会更好些。

Note

单点浪费通常没啥影响,如果它影响较大,通常很容易被一个专家修复。但是, 在代码库中大量散布的垃圾很容易造成重大影响,而且专家也不总是像我们那样随时可用就像。那个这个规则的目标(以及支持它的更具体的规则)是在它之前消除与C++的使用有关的大多数浪费。发生了。之后我们可以看看与算法和需求相关的浪费,但这超出了这些准则的范围。
但是,代码库中大量分布的浪费很容易构成显著影响;专家们并非总是像我们希望的那样随时在场。这个规则的目的(以及支持它的更具体的规则)是在发生之前,消除与C++的使用有关的大部分资源浪费。此外,我们可以研究与算法和需求相关的浪费,但这超出了这些指导原则的适用范围。

Enforcement

许多更具体的规则旨在实现简单化和消除无端浪费的总体目标。

  • 标记用户定义的非默认 operator++ or operator--函数中未使用的返回值。而是优先使用前缀形式。 (Note: “User-defined non-defaulted” is intended to reduce noise. Review this enforcement if it’s still too noisy in practice.)

P.10: 优先使用不可变数据而不是可变数据

原因

对常量的推理要比对变量的推理容易。不可变量是不会意外改变的。有时不可变量还可以被更好的优化。你不能在一个常量上进行数据竞争
常量上不存在数据竞争(data race).

Con: Constants and immutability

P.11: :封装那些凌乱的结构,而不是任其在代码中传播

原因

混乱的代码更容易隐藏错误,更难编写。而一个好的界面更容易使用,也更安全。混乱、低级代码又会产生更多混乱、低级的代码。

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;
    // ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

This is low-level, verbose, and error-prone.
For example, we “forgot” to test for memory exhaustion.
Instead, we could use vector:

vector<int> v;
v.reserve(100);
// ...
for (int x; cin >> x; ) {
    // ... check that x is valid ...
    v.push_back(x);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
Note

The standards library and the GSL are examples of this philosophy.
For example, instead of messing with the arrays, unions, cast, tricky lifetime issues, gsl::owner, etc.,
that are needed to implement key abstractions, such as vector, span, lock_guard, and future, we use the libraries
designed and implemented by people with more time and expertise than we usually have.
Similarly, we can and should design and implement more specialized libraries, rather than leaving the users (often ourselves)
with the challenge of repeatedly getting low-level code well.
This is a variant of the subset of superset principle that underlies these guidelines.

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/羊村懒王/article/detail/181182?site
推荐阅读
相关标签
  

闽ICP备14008679号