CppCoreGuidelines 二周目笔记 01 (完结)

CppCoreGuidelines 二周目笔记 01 (完结)

RayAlto OP

原话: One way of thinking about these guidelines is as a specification for tools that happens to be readable by humans.

我的观点是不要死记硬背,最简单的方法是把 clang-tidy 之类的工具集成到你的开发环境里

重读一遍 Cpp Core Guidelines ,记一些我喜欢的条目,不保证我的理解百分百正确。

00. 优先用标准库的工具(ES.1)

家人们谁懂啊, Standard Library 的工具真的好用到跺 jiojio ,咱就是说怎么还有人写这种代码:

1
2
3
4
double sum = 0.0;
for (double d : v) {
sum += d;
}

真虾头,前几天去星巴克 gap hours 的时候看到一个集美这样写:

1
auto sum = std::accumulate(std::begin(v), std::end(v), 0.0);

啊啊啊啊啊啊啊啊一整个爱住,那么多 std:: 真的绝绝子,还有那个 auto 一下子击中了咱的心巴,家人们狠狠地爱上了,咱不允许世界上还有人不知道这个方法。

01. 更先进的 if 语句(ES.6)

C++17 之后 if 里面也可以有初始化语句,比如这样的逻辑:

1
2
3
4
Json::Value some_config = config["some"];
if (some_config.isUInt()) {
c.foo = some_config.asUInt();
}

some_config 已经完成了它的使命,但它还没有离开它的作用域,如果后面还要解析更多配置的话就会有很多 Json::Value 漏在外面很难看,如果有 C++17 可以改成:

1
2
3
if (Json::Value some_config = config["some"]; some_config.isUInt()) {
c.foo = some_config.asUInt();
}

非常干净, some_config 只在 if 里存活。

02. 不要把变量的声明和使用隔开很远(ES.22, NR.1)

很多 C 库函数都是开头一大堆变量(各种 struct 指针),然后隔了 114514 行才用到,这个时候没有 LSP 根本知不道这个变量到底是什么类型的。不是否认这些 C 库的优秀,只是我和 BS 的观点一致

不要这样:

1
2
3
4
5
6
7
void foo() {
Data x{ ... };

// 省略 114514 行代码没有用到 x

x.herpderp(); // 突然开始用了
}

03. 一个变量只用来完成一个逻辑(ES.26)

比如这种代码存在可读性问题:

1
2
3
4
5
6
7
8
9
void foo() {
int i = 0;
for (i = 0; i < 114514; i++) {
// ...
}
Json::Value v = config["integer"]
i = v.asInt(); // 重复使用 i ,可读性差
bar(i);
}

04. 为复杂的初始化(尤其是 const 变量)使用 lambda(ES.28)

其实我经常用 lambda 来初始化静态常量,只是以前不知道这么做有没有问题,现在有 BS 认可辣

比如这种:

1
2
3
4
5
Data d; // 先默认初始化
for (int i = 0; i < 114514; i++) {
d += foo.bar(i); // d 需要这些步骤才能完成初始化
}
// 然后 d 就不会再被更改了

可以改成:

1
2
3
4
5
6
7
const Data d{[&] {
Data d;
for (int i = 0; i < 114514; i++) {
d += foo.bar(i);
}
return d;
}()};

05. 真的需要在 switch 中直落到下一个 case 的情况下使用 [[fallthrough]] 标注(ES.78)

1
2
3
4
5
6
7
8
switch (reason) {
case CONNECTION_ESTABLISHED:
process_connection();
[[fallthrough]];
case CLIENT_READY:
process_client();
break;
}

06. 不要在原生的 for 循环里修改循环控制变量

这种写法非常容易出错(我也经历过很多次因为这么写出现错误的情况):

1
2
3
4
5
6
7
for (int i = 0; i < 114514; i++) {
herpderp();
if (foobar()) {
i++; // 想跳过一轮循环,但这样很容易导致错误
}
ieatpasta();
}

可以改成:

1
2
3
4
5
6
7
8
9
10
11
for (bool skip = false; int i = 0; i < 114514; i++) {
if (skip) {
skip = false;
continue;
}
herpderp();
if (foobar()) {
skip = true;
}
ieatpasta();
}

07. 「疑问」使用 gsl::indexsize_t 混用是正确的

众所周知 C++ 的标准库的容器使用无符号作下标,一般 size() 返回值类型 size_type 就是 size_ttypedef (实际上在 X86-64 上就是 uint64_t ), CppCoreGuidelines 原文很多地方都出现了类似这种写法:

1
2
3
gsl::index i = 0;
i = v.size(); // ??
i < v.size(); // ??

我以为 gsl::index 能整出什么花活能避免 size_t 转换成有符号类型时的溢出( int128_t ?我不好说),结果 gsl::index 就是 ptrdiff_t (实际上在 X86-64 上就是 int64_t ),问题根本没有得到解决,然后 CppCoreGuidelines 写了这样一条建议:

(To avoid noise) Do not flag on a mixed signed/unsigned comparison where one of the arguments is sizeof or a call to container .size() and the other is ptrdiff_t.

大致意思就是 gsl::indexuint64_t 混用不要报错,而其他类型(比如 int64_t )与 uint64_t 混用却建议报错。意义不明,可能 ptrdiff_t 有什么我不知道的黑魔法?希望知道真相的大佬指点一下。

08. 进行可预测的内存访问(Per.19)

优先访问相邻数据(线性的)是更利于缓存算法的,比如这种写法:

1
2
3
4
5
6
7
8
constexpr const int ROWS = 114514;
constexpr const int COLUMNS = 114514;
std::array<std::array<int, COLUMNS>, ROWS> matrix{};
for (int column = 0; column < COLUMNS; column++) {
for (int row = 0; row < ROWS; row++) {
process(matrix[row][column]); // 一列一列遍历
}
}

最好换成这样:

1
2
3
4
5
6
7
8
constexpr const int ROWS = 114514;
constexpr const int COLUMNS = 114514;
std::array<std::array<int, COLUMNS>, ROWS> matrix{};
for (int row = 0; row < ROWS; row++) {
for (int column = 0; column < COLUMNS; column++) {
process(matrix[row][column]); // 一行一行遍历
}
}

效率一般更高。

09. 「嘲讽」建议开发时假设自己的代码会用在多线程里

你说的对,请问标准库什么时候支持多线程?

10. #pragma once 并不是标准(SF.9)

虽然我一直都在用 Define Guard (老壁灯)而不是 #pragma once ,但没想到 BS 说这种写法是厂商扩展:

1
#pragma once

建议用传统的 Define Guard :

1
2
3
4
5
6
#ifndef PROJECT_SOME_DATA_H_
#define PROJECT_SOME_DATA_H_

// ...

#endif // PROJECT_SOME_DATA_H_

11. 不要没活硬整乱放 const (NL.26)

1
2
3
4
const int i = 0; // 挺好的
int const i = 0; // 你在发什么神经?
const int* const pi = nullptr; // 嗯嗯
int const *const pi = nullptr; //李在赣什么?

完结

感觉整体读下来帮助不是很大,因为几年前从 M$ 的 VS 逃到 Neovim + LSP 之后我就一直在用 clang-tidy ,很多习惯都是正确的。当然还有很多错误处理、并发并行之类的内容,我只是粗略看了一遍,因为 CppCoreGuidelines 写的也很粗略所以没有做记录。