c++核心指南--Philosophy

2023-12-19 03:08
文章标签 c++ 指南 核心 philosophy

本文主要是介绍c++核心指南--Philosophy,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

本节的规则非常笼统。

理念规则摘要:

  • 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: 直接用代码表达思想

原因

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

例子
class Date {
public:Month month() const;  // doint month();          // don't// ...
};

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

坏的例子

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

void f(vector<string>& v)
{string val;cin >> val;// ...int index = -1;   // 不好,外加应该使用 gsl::indexfor (int i = 0; i < v.size(); ++i) {if (v[i] == val) {index = i;break;}}// ...
}
好的例子

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

void f(vector<string>& v)
{string val;cin >> val;// ...auto p = find(begin(v), end(v), val);  // 很好// ...
}

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

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

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

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

本来,我们可以接受一个普通的(无单位的)“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] ...
}

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

改进:

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

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

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

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

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

程序员应该熟悉:

  • The guidelines support library
  • The ISO C++ Standard Library
  • 当前项目所使用的任何基础库。
Note

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

Note

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

示例

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

draw_line(int, int, int, int);  // 模糊
draw_line(Point, Point);        // 清晰
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";

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

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

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

示例
void read(int* p, int n);   // 将最大的n个整数读进 *pint a[100];
read(a, 1000);    // 错误, 数组越界

改进

void read(span<int> r); // 读入span<int> rint a[100];
read(a);        // 比较好:让编译器计算元素数量

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

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

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

原因

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

Note

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

反例
// f是分别编译的,而且有可能是动态加载的。
extern void f(int* p);void g(int n)
{// 错误:元素的数量没有传递给 f()f(new int[n]);
}

在这里,一个重要的信息(元素的数量)已经被彻底地“模糊”了,以至于静态分析可能变得不可行,当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()
}

作为参数传递元素的数量比仅仅传递指针和依赖某种(未声明的)约定来知道或发现元素的数量要好(而且更常见)。但是(如上所示),一个简单的打字错误会导致严重的错误。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);    // 不好: 分别传递所有权和大小
}
示例

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

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});          // 传递快照,保留所有权
}

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

示例

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

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;
}
例子
  • ???
  • 当他们真的知道他们需要什么的时候,展示如何通过传递多态基类的方式避免可能的检查。
    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// ...
}

在这里,我们在 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// ...
}

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

void use3(int m)
{const int n = 10;int a[n] = {};// ...increment2(a);   // 不需要在此传入a的元素的个数// ...
}

.

反例

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

Date read_date(istream& is);    // read date from istreamDate extract_date(const string& s);    // extract date from stringvoid user1(const string& date)    // manipulate date
{auto d = extract_date(date);// ...
}void user2()
{Date d = read_date(cin);// ...user1(d.to_string());// ...
}

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 * zfloat 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);}???
};

由于存在测量误差,有关 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);
}

改进RAII:

void f(char* name)
{ifstream input {name};// ...if (something) return;   // OK: 没有泄露// ...
}

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 *pfor (gsl::index i = 0; i < x.s.size(); ++i) x.s[i] = buf[i];  // copy buf into x.sdelete[] buf;return x;
}void driver()
{X x = waste("Typical argument");// ...
}

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

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

这是一个来自生产代码的真实的例子。可以看到,在条件语句里有个表达式 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;// ...
}

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);
}
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.

这篇关于c++核心指南--Philosophy的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/510702

相关文章

SpringBoot请求参数接收控制指南分享

《SpringBoot请求参数接收控制指南分享》:本文主要介绍SpringBoot请求参数接收控制指南,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录Spring Boot 请求参数接收控制指南1. 概述2. 有注解时参数接收方式对比3. 无注解时接收参数默认位置

C++如何通过Qt反射机制实现数据类序列化

《C++如何通过Qt反射机制实现数据类序列化》在C++工程中经常需要使用数据类,并对数据类进行存储、打印、调试等操作,所以本文就来聊聊C++如何通过Qt反射机制实现数据类序列化吧... 目录设计预期设计思路代码实现使用方法在 C++ 工程中经常需要使用数据类,并对数据类进行存储、打印、调试等操作。由于数据类

CentOS7更改默认SSH端口与配置指南

《CentOS7更改默认SSH端口与配置指南》SSH是Linux服务器远程管理的核心工具,其默认监听端口为22,由于端口22众所周知,这也使得服务器容易受到自动化扫描和暴力破解攻击,本文将系统性地介绍... 目录引言为什么要更改 SSH 默认端口?步骤详解:如何更改 Centos 7 的 SSH 默认端口1

SpringBoot多数据源配置完整指南

《SpringBoot多数据源配置完整指南》在复杂的企业应用中,经常需要连接多个数据库,SpringBoot提供了灵活的多数据源配置方式,以下是详细的实现方案,需要的朋友可以参考下... 目录一、基础多数据源配置1. 添加依赖2. 配置多个数据源3. 配置数据源Bean二、JPA多数据源配置1. 配置主数据

Linux下如何使用C++获取硬件信息

《Linux下如何使用C++获取硬件信息》这篇文章主要为大家详细介绍了如何使用C++实现获取CPU,主板,磁盘,BIOS信息等硬件信息,文中的示例代码讲解详细,感兴趣的小伙伴可以了解下... 目录方法获取CPU信息:读取"/proc/cpuinfo"文件获取磁盘信息:读取"/proc/diskstats"文

python中各种常见文件的读写操作与类型转换详细指南

《python中各种常见文件的读写操作与类型转换详细指南》这篇文章主要为大家详细介绍了python中各种常见文件(txt,xls,csv,sql,二进制文件)的读写操作与类型转换,感兴趣的小伙伴可以跟... 目录1.文件txt读写标准用法1.1写入文件1.2读取文件2. 二进制文件读取3. 大文件读取3.1

SpringBoot中配置Redis连接池的完整指南

《SpringBoot中配置Redis连接池的完整指南》这篇文章主要为大家详细介绍了SpringBoot中配置Redis连接池的完整指南,文中的示例代码讲解详细,具有一定的借鉴价值,感兴趣的小伙伴可以... 目录一、添加依赖二、配置 Redis 连接池三、测试 Redis 操作四、完整示例代码(一)pom.

Linux内核参数配置与验证详细指南

《Linux内核参数配置与验证详细指南》在Linux系统运维和性能优化中,内核参数(sysctl)的配置至关重要,本文主要来聊聊如何配置与验证这些Linux内核参数,希望对大家有一定的帮助... 目录1. 引言2. 内核参数的作用3. 如何设置内核参数3.1 临时设置(重启失效)3.2 永久设置(重启仍生效

C++使用printf语句实现进制转换的示例代码

《C++使用printf语句实现进制转换的示例代码》在C语言中,printf函数可以直接实现部分进制转换功能,通过格式说明符(formatspecifier)快速输出不同进制的数值,下面给大家分享C+... 目录一、printf 原生支持的进制转换1. 十进制、八进制、十六进制转换2. 显示进制前缀3. 指

Python列表去重的4种核心方法与实战指南详解

《Python列表去重的4种核心方法与实战指南详解》在Python开发中,处理列表数据时经常需要去除重复元素,本文将详细介绍4种最实用的列表去重方法,有需要的小伙伴可以根据自己的需要进行选择... 目录方法1:集合(set)去重法(最快速)方法2:顺序遍历法(保持顺序)方法3:副本删除法(原地修改)方法4: