C++ 核心指南(C++ Core Guidelines)是由 Bjarne Stroustrup、Herb Sutter 等顶尖 C+ 专家创建的一份 C++ 指南、规则及最佳实践。旨在帮助大家正确、高效地使用“现代 C++”。

这份指南侧重于接口、资源管理、内存管理、并发等 High-level 主题。遵循这些规则可以最大程度地保证静态类型安全,避免资源泄露及常见的错误,使得程序运行得更快、更好。

文中提到的 GSL(Guidelines Support Library) 是 C++ 核心指南支持库 https://github.com/Microsoft/GSL

P:Philosophy 基本理念

本节的规则反映了现代 C++ 的哲学/基本理念,贯穿整个 C++ 核心指南:

规则摘要:

  • P.7:尽早捕获运行时错误
  • P.8:不要泄露任何资源
  • P.9:不要浪费时间或空间
  • P.10:优先使用不可变数据而不是可变数据
  • P.11:封装混乱的结构,而不是让其散布在代码中
  • P.12:根据需要使用支持工具
  • P.13:根据需要使用支持库

这些基本理念是其他章节具体规则的理论基础。

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

以避免(可能无法发现的)错误结果或程序崩溃

例子

//  容易出错
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] = {};
// 可能是手误,或者 m<=n,但万一 m==20 ...
increment1(a, m);
}

use1() 中犯了个小错误,可能导致数据损坏或程序崩溃。形如 foo(pointer, count) 这样形式的接口没有办法彻底避免 “out-of-range” 错误。如果我们检查下标越界,那也要等到访问 p[10] 的时候才能发现。可以改进代码,更早地进行检查:

void increment2(span<int> p)
{
for (int& x : p) ++x;
} void use2(int m)
{
const int n = 10;
int a[n] = {};
// 可能是手误,或者 m<=n
increment2({a, m});
}

现在 m <= n 能在调用 increment2() 时就检查。如果本来就想用 n 作为边界,代码可以进一步简化(同时也消除了错误的可能):

void use3(int m)
{
const int n = 10;
int a[n] = {};
// 不需要重复元素个数
increment2(a);
}

反面例子

不要重复检查同一个值,不要把结构化的数据作为 string 传递:

// 从 istream 中读日期
Date read_date(istream& is); // 从 string 中提取日期
Date extract_date(const string& s); // 操作日期
void user1(const string& date)
{
auto d = extract_date(date);
// ...
} void user2()
{
Date d = read_date(cin);
// ...
user1(d.to_string());
// ...
}

user2() 中同一个日期被 Date 的构造函数校验了 2 次(一次在调用 read_date() 时,一次在调用 user1() --> extract_date() 时),并且原本结构化的 Date d 作为(非结构化的) string 传给了 user1

额外的检查是有开销的。有时过早的检查效率不高,因为可能根本用不到这个值。或者只用到了部分,而只检查用到的这部分比检查整个数据容易得多。类似地,不要增加改变接口复杂度的检查,例如不要在一个平均复杂度 O(1) 的接口中添加一个 O(n) 的检查。

代码检查建议

  • 检查指针和数组:尽早进行范围检查,但不要重复检查
  • 检查转换:标记或消除窄化转换
  • 查找来自输入的、但没有检查的值
  • 查找被转为 string 的结构化数据(invariant 类的对象)
  • ...

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

即便资源占用以极其缓慢的速度增加,最终也会耗尽资源,尤其是对长时间运行的程序来说。

反面例子

void f(char* name)
{
FILE* input = fopen(name, "r");
// 如果 something == true,文件句柄泄漏
if (something) return;
// ...
fclose(input);
}

应该使用 RAII:

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

参见:资源管理 R 篇

泄漏是指没有清理的或者无法再被清理的。例如,在堆上分配一个对象,然后丢失所有指向该对象的指针。

  • 本规则不要求在程序结束时回收 long-lived 对象。例如,依赖系统在程序结束时自动清理打开的文件、分配的内存的保证,可以简化代码。但是依赖隐式清理的抽象更简单,也更安全
  • 生命周期安全 Profile 可以消除泄漏。结合 RAII,可以消除对“垃圾回收”的需求,因为根本不会产生垃圾。再结合 类型和边界 Profiles 可以做到完全的类型&资源安全(由工具保证)

代码检查建议

  • 检查指针:把指针划分为“拥有指针”和“非拥有指针”(参见:资源管理 R 篇)。如果可以,用标准库的资源管理(如上面的例子)。或者把拥有指针用 GSL 库中的 owner 标识
  • 查找裸的 newdelete
  • 查找返回裸指针的资源分配函数(如 fopenmallocstrdup

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

因为这是 C++。但如果花费的时间和空间是为了达成特定目标(例如开发速度,资源安全或简化测试),那么并不算浪费。

“追求效率的另一个好处是,这个过程会迫使你更深入地理解问题。” - 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];
// ... 操作 buffer ...
X x;
x.ch = 'a';
// give x.s space for *p
x.s = string(n);
// copy buf into x.s
for (gsl::index i = 0; i < x.s.size(); ++i)
x.s[i] = buf[i];
delete[] buf;
return x;
} void driver()
{
X x = waste("Typical argument");
// ...
}

这是一个夸张的例子,但每一个错误都在实际生产代码中出现过,甚至更糟。结构体 X 的布局至少浪费了 6 个字节(很可能更多)。显式声明的拷贝构造和拷贝赋值运算符抑制了移动操作,导致返回操作较慢(注意:这里不能保证 RVO 返回值优化)。对 buf 使用 newdelete 是多余的;如果真的需要一个局部字符串,应该使用局部 string。还有几个其他性能错误和不必要的复杂性。

反面例子

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

这是一个真实生产代码的例子。在循环中的条件是 i < strlen(s)。这个表达式在每次循环迭代时都会被计算,这意味着 strlen 必须在每次循环中遍历字符串来计算字符串长度。虽然字符串内容改变了,但 tolower 不会影响字符串的长度,所以最好在循环之外缓存字符串长度,而不是每次循环都重新计算长度。

一个浪费的例子并不会产生显著的影响,如果影响显著,通常可以轻易地发现并消除。但如果代码库中到处都是这样的代码,则很容易产生巨大的影响。这个规则的目的是在问题出现之前,消除 C++ 使用相关的大部分浪费。之后,我们可以考虑与算法和需求相关的浪费,但这超出了 C++ 核心指南的范围。

代码检查建议

有许多更具体的规则,旨在实现“简单,消除不必要浪费”这个整体目标。

  • 标记后置递增/递减运算符 operator++operator-- 函数未使用的返回值,最好使用前置形式。

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

  • 不可变数据不会意外地改变
  • 更容易优化
  • 不会产生数据竞争(data race)

见条款 Con: Constants and immutability

P.11:封装混乱的结构,而不是让其散布在代码中

  • 混乱的代码容易藏着 bug
  • 好的接口更容易使用,也更安全
  • 混乱、低级的代码更容易产生更多混乱的代码(破窗理论)

这里的低级(low-level)指的是代码的抽象层级较低,直接操作指针、malloc、realloc 等这样的低层函数

例子

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

标注库和 GSL 是该思想的体现:vector、span、lock_guard、future 等关键抽象封装了低层的数组、union、类型转换等操作。通常库的设计/实现者远比我们更专业,花的时间也更多。我们应该使用库提供的高级抽象,而不是让低层细节搞乱我们的代码。

代码检查建议

查找混乱的代码,例如

  • 复杂的指针操作
  • casting outside the implementation of abstractions

P.12:根据需要使用支持工具

有的事情让机器来做更合适:计算机不会对重复的工作感到无聊和厌倦,而人可以做一些更有价值的事情

例子

运行静态代码分析工具来确保代码遵循某些规范:如 coverity、clang-tidy、clang-format 等

  • 还有许多其他类型的工具,如代码仓库、构建工具等,这些不在 C++ 核心指南范围内

  • 不要依赖过于复杂、特殊的工具链,这会让可移植代码变得不可移植

P.13:根据需要使用支持库

使用设计/文档/支持良好的库可以节省时间和精力。无论是代码质量还是文档质量,通常要比自己写的好得多。库的开发维护成本是被所有用户平摊的(实际上大多数优秀的库都是可以免费使用的)。如果一个库有很多用户,也更容易保持更新并移植到新系统,并且相关的使用经验和知识也可以在其他项目复用,节约时间和精力。

例子

std::sort(begin(v), end(v), std::greater<>());

除非你是排序算法的专家,并且有大量时间,否则你很难写出比上面那一行更正确、更快的排序代码。

使用标准库(或其他基础库)不需要理由;相反,不用标准库最好有充足的理由。

C++ 核心指南之 C++ 哲学/基本理念(下)的更多相关文章

  1. 三年磨一剑,robot framework 自动化测试框架核心指南,真正讲透robot framework自动化测试框架(笔者新书上架)。

    序 关于自动化测试的工具和框架其实有很多.自动化测试在测试IT行业中扮演着越来越重要的角色,不管是在传统的IT行业还是高速发展的互联网行业或是如今的大数据和大热的人工智能领域,都离不开测试,也更加离不 ...

  2. Robot Framework自动化测试框架核心指南-如何使用Java编写自定义的RobotFramework Lib

    如何使用Java编写自定义的RobotFramework Lib 本文包括2个章节 1. Robot Frdamwork中如何调用java Lib库 2.使用 java编写自定义的Lib 本文作者为: ...

  3. Robot Framework自动化测试框架核心指南-如何做好自动化测试平台框架的设计

    自动化测试如果需要能高效快速的支撑软件项目的测试,项目的快速迭代以及上线,除了以上我们介绍的需要许多的Lib来支持以及需要高效的去编写自动化测试案例外,还需要一个好的自动化测试框架平台来支撑我们的自动 ...

  4. [译] JavaScript核心指南(JavaScript Core) 【转】

    本文转自:http://remember2015.info/blog/?p=141#scope-chain 零.索引 对象(An Object) 原型链(A Prototype Chain) 构造函数 ...

  5. 【windows核心编程】 第八章 用户模式下的线程同步

    Windows核心编程 第八章 用户模式下的线程同步 1. 线程之间通信发生在以下两种情况: ①    需要让多个线程同时访问一个共享资源,同时不能破坏资源的完整性 ②    一个线程需要通知其他线程 ...

  6. Java多线程编程模式实战指南:Active Object模式(下)

    Active Object模式的评价与实现考量 Active Object模式通过将方法的调用与执行分离,实现了异步编程.有利于提高并发性,从而提高系统的吞吐率. Active Object模式还有个 ...

  7. [Windows核心编程]32bit程序在64bit操作系统下处理重定向细节[1]

    这段时间,都在做Ring3层的普通32bit程序兼容64bit操作系统的代码修改,在此记录修改和学习心德.编程领域太广, 任何人经历有限,本人不是专家,所以我一贯原则是: 用到的时候,才去研究,在去记 ...

  8. 《JavaScript权威指南》学习笔记 第二天 下好一盘大棋

    前段学习js的时候总是零零散散的,以至于很多东西都模棱两可.时间稍微一久,就容易忘记.最主要的原因是这些东西,原来学的时候就不是太懂,以至于和其他知识无法形成记忆链,所以孤零零的知识特别容易忘记.重温 ...

  9. windows核心编程---第七章 用户模式下的线程同步

    用户模式下的线程同步 系统中的线程必须访问系统资源,如堆.串口.文件.窗口以及其他资源.如果一个线程独占了对某个资源的访问,其他线程就无法完成工作.我们也必须限制线程在任何时刻都能访问任何资源.比如在 ...

  10. Python核心编程--学习笔记--6--序列(下)列表、元组

    11 列表 类似于C语言的数组,但是列表可以包含不同类型的任意对象.列表是可变类型. 创建列表——手动赋值.工厂函数: >>> aList = [12, 'abc'] >> ...

随机推荐

  1. SpringBoot导出Word文档的三种方式

    SpringBoot导出Word文档的三种方式 一.导出方案 1.直接在Java代码里创建Word文档,设置格式样式等,然后导出.(略) 需要的见:https://blog.csdn.net/qq_4 ...

  2. 数据库定时备份winserver2012篇

    目录 1 序言 2 任务计划相关知识点介绍 2.1 任务计划 是什么? 2.2 批处理文件 2.2.1 批处理文件简介 2.2.2 批处理常用命令介绍 3 各个数据库备份脚本 3.1 Oracle数据 ...

  3. 2021-03-21:给定一棵二叉树的头节点head,求以head为头的树中,最小深度是多少?

    2021-03-21:给定一棵二叉树的头节点head,求以head为头的树中,最小深度是多少? 福大大 答案2021-03-21: 1.递归. 2.莫里斯遍历. 代码用golang编写,代码如下: p ...

  4. WPF入门教程系列二十四——DataGrid使用示例(1)

    WPF入门教程系列二--Application介绍 WPF入门教程系列三--Application介绍(续) WPF入门教程系列四--Dispatcher介绍 WPF入门教程系列五--Window 介 ...

  5. 在windows下安装elk

    一.下载elasticsearch-5.1.1 cd D:\bigdata\elasticsearch-5.1.1\bin elasticsearch-service.bat cmd 运行 servi ...

  6. 【GiraKoo】Github无法打开,导致无法下载Git安装包

    环境 Windows 11 原因 Git应用的安装程序在Github上,由于Github访问不稳定,导致无法下载. 对策 打开迅雷.将下载链接拷贝进去,利用迅雷的P2P技术,从其他网友处进行下载. 打 ...

  7. Python竖版大屏 | 用pyecharts开发可视化的奇妙探索!

    你好!我是@马哥python说,一枚10年程序猿‍,正在试错用pyecharts开发可视化大屏的非常规排版. 以下,我用8种ThemeType展示的同一个可视化数据大屏. 1.SHINE主题 2.LI ...

  8. 记录部署Datax、Datax-web 过程碰到的问题

    我的第一篇博客 datax在网络上部署的文档有很多,这里不重复阐述,只描述过程中碰到的些许问题,记录下来. 1. 1 ERROR RetryUtil - Exception when calling ...

  9. Codeforces Round #879 (Div. 2) A-E

    比赛链接 A 代码 #include <bits/stdc++.h> using namespace std; using ll = long long; bool solve() { i ...

  10. React ISR 如何实现 - 最后的 Demo

    之前写了两个 demo 讲解了如何实现 SSR 和 SSG,今天再写个 demo 说在 ISR 如何实现. 什么是 ISR ISR 即 Incremental Static Regeneration ...