findingsea's Studio.

C++ Tips

Word count: 1,566 / Reading time: 6 min
2019/03/07 Share

读了《C++ 的门门道道 | 技术头条》这篇文章之后有很多共鸣,可以说是近期看过的最好的技术 tips 文章了。按照这篇文章里面讲到的几点,结合工作上实际遇到的问题,我也来说一下我的感受。

成员变量初始化

成员变量忘了初始化是一个相当经典的错误,甚至《Effective C++》中还专门列了一条来讲这个事情。在工作中,我就看到过这种错误,同事对一个新增的功能加上了开关控制的逻辑,但是忘了对这个开关的标识进行初始化,导致了一条分支逻辑失效。而且因为 C++ 没有默认初始值,那它的初始值是随机的,所以导致线上的表现是概率性复现,增加了 debug 的难度。当然增加 Coverity 扫描提早发现这个问题,当时项目上线比较急就直接跳过了这一步。

从 C++11 开始支持在声明成员变量的时候直接初始化,有了这个特性之后,我已经养成了所有成员变量都直接在声明的时候初始化。

1
2
3
4
class Ad {
private:
unsinged int lifetime = 10000;
};

sort() 里的坑

这个坑即便是有点经验的程序员也会踩到,有一次线上事故就是一个稳定跑了很久的逻辑,突然出现了 core,而且是持续地 core 在 sort 上。花了很长时间排查,最后才意识到实现新增的 sort 比较函数没有保证严格弱序(strict weak order),比较两个对象的属性时用了 <=。这里就涉及到 C++ 中 sort 的实现。细节之后会写一篇文章来讲,简单说来就是 STL sort 核心排序算法是快排,在依据 pivot 调整元素位置时采用的实现方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13

while (true)
{
while (__comp(*__first, __pivot))
++__first;
--__last;
while (__comp(__pivot, *__last))
--__last;
if (!(__first < __last))
return __first;
std::iter_swap(__first, __last);
++__first;
}

重点就在于 while (__comp(*__first, __pivot)) ++__first;,当整个容器里的元素都相等时,就会导致 __first 这个迭代器越界,程序就 core 了。

操作符短路

原文关于如何避免操作符短路讲得很好了,其实我们还可以利用操作符短路来简化代码。对我更常用的场景:if (!stack.empty() && stack.top() == 0),这恰恰是利用短路来合并判断。

别让循环停不下来

这个有个经典场景,在 vector 里面,我要找到首个递增序列的最后一个元素,很容易写成这样的代码:

1
while (i < ve.size() - 1 && ve[i] <= ve[i + 1]) i++;

这里如果传入的 ve 是个空 vector,那么就会成为超大循环,因为 vector::size() 返回的是 unsigned int,根据数值类型传递,ve.size() - 1 的类型也是 unsigned int,那么就会返回一个很大的数,导致 while 陷入超大循环。

理解 vector 的实现

vector 可以说是在日常开发中使用频率最高的容器了,支持下标访问,动态扩容,二分查找的效率,C++11 之后支持移动构造,这些优点都让它非常好用。vector 的坑都集中在它的动态扩容上,理解它动态扩容的机制可以在开发中避开这些坑。

vector 动态扩容的两个特点:

  1. vector 扩容是按照 2 的指数倍往上翻的,也就是 2, 4, 8, 64, 128, ……。

  2. 动态扩容时是会全量复制一遍现有的所有元素到新分配的内存中。

根据这两个特点,结合 vector 的其他特性得到的:

  1. 尽量预先分配好 vector 的空间,使用 reserve() 预分配空间,避免多次扩容。

  2. 不要在 vector 里存储大对象,扩容的时候会全量复制,额外的性能开销很大。

  3. 不要保存指向 vector 内部对象的指针,扩容时对象地址会发生变化。

  4. reserve() 是提前分配空间,此时不能直接用下标索引访问(如果用基本类型倒是能访问,但是这种行为仍然是未定义的)。

有时候真的不必用 std::unorder_map

组里有一个项目升级到 C++11 之后,一窝蜂地使用 unordered_map,但是其实对于小数据量,比如本次请求命中的一些配置,其实数据量基本都在 10 项以内,那其实用 map 就完全够用了,unorder_map 查找的效率当然是高的,但是也要认识到它维护一个哈希表额外付出的性能代价。

慎用short,char

因为一开始设计的数值类型过于严格而导致的重写,我遇到过不止一次了。

有些人写代码的时候有一种倾向,就是能省则省,能用 int 的绝不用 long,能用 short 的绝不用 int。但是其实有些情况下 short 并不能节省空间(字节对齐),还导致过度「优化」,导致逻辑变化之后要重写,或者实际的取值不符合设计导致溢出。

避免箭头型代码

什么是「箭头型代码」?见下图:

箭头型代码,图片来源:CoolShell

这种代码其实在业务复杂的场景下并不少见,酷壳上有一篇文件专门讲过如何重构这种代码:《如何重构“箭头型”代码》

我在实际项目中应用比较多是利用 while (0) 来规避这种代码。

在项目经常遇到的场景是对一连串条件进行判断,不符合条件的分支需要打印日志,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

if (conditionA()) {
if (conditionB()) {
if (conditionC()) {
if (conditionD()) {
// do something
} else {
// log
}
} else {
// log
}
} else {
// log
}
} else {
// log
}

这种情况下用 do-while(0) 可以进行非常好的重构,重构之后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

do {
if (!conditionA()) {
// log
break;
}
if (!conditionB()) {
// log
break;
}
if (!conditionC()) {
// log
break;
}
if (!conditionD()) {
// log
break;
}
} while (0);

参考文章:

CATALOG
  1. 1. 成员变量初始化
  2. 2. sort() 里的坑
  3. 3. 操作符短路
  4. 4. 别让循环停不下来
  5. 5. 理解 vector 的实现
  6. 6. 有时候真的不必用 std::unorder_map
  7. 7. 慎用short,char
  8. 8. 避免箭头型代码
  9. 9. 参考文章: