工作中常用到的 C++ 功能子集

用 C++ 语言也有一些年头了,在此总结一些工作中比较实用的 C++ 的功能子集。这里的主要使用场景是系统编程,不包括各种通用类库。

Advanced C

在工作中使用 C++ 更多的是将其作为一种更加先进的 C 语言来使用。C++ 比之 C 语言带来了以下好用的特性:

  • RAII

  • Smart Pointers

  • Move Semantics

  • Namespace

  • Access Control (private, protected & public)

  • Generics (Template)

  • Structured Exception Handling

  • Lambda Expression

  • Enum class

Smart Pointer 使得 C++ 可以在一定程度上自动管理对象的生命周期。RAII 使得我们可以将任意资源的分配和回收与对象的生命周期相关联。两者结合,使得我们可以在一定程度上自动管理资源的生命周期。这使得 C++ 一跃成为可以和 Python & Java 一争长短的现代开发语言。

RAII 将资源的管理与对象的生命周期互相绑定,但是有时我们需要暂时解除这种绑定并将他们重新绑定。此时我们就需要 Move Semantics,这带来了资源所有权的 hand-off 能力。

比之 Namespace + Access Control,其实我更想要的是 Module,但是这个 proposal 也是难产了好久了 [1]。有了 Module 之后,我还希望一个语法糖:扩展方法。也就是 f(obj, …​) 等同于 obj.f(…​) 这样的功能。这样有些方法就没必要非要写在 class 里面了。

C++ 有了泛型是极好的,但是仍有两个缺点。一个是所有的泛型都要写到头文件里,这也是没有 Module [1] 的一个缺陷。另一个是没有一个好的办法约束泛型参数,Concept 这个 proposal 也是难产了好久了 [2]。此外 C++ 的 Template 能力太强,不做库开发的话强烈建议不要使用模板元编程,只用其中泛型相关的部分即可。需要一些元编程的部分怎么办呢?单独在构建过程中加一个预处理阶段,使用其他 template engine 去展开 source code 即可,不容易出错,可读性强,而且报错信息友好。

C++ 有结构化异常处理。我知道有些人宁愿把错误码和结果一起返回,也不愿意用异常处理机制。但是我只想说,异常处理谁用谁知道。此外,异常处理在 normal execution path 上是 zero cost 的 [3]

Lambda Expression 替代了函数指针,这个就不多说了,这么多年才把这个功能做对也是不容易了。

Enum class 也终于让 enum 不再只是个常数定义的另一种写法了。

面向对象编程

面向对象编程(OOP)一般是没啥用的,也不应该用。但是 C++ 中泛型用起来太麻烦了,而且 RTTI 太弱了,以至于大多数情况下我们都是用继承来实现多态(子类型多态),而非泛型。

Java 这种号称纯 OOP 的语言,其实大家用到的基于继承关系的多态功能也很少,更多的时候是搞出来一个接口,然后大家都用接口交互,或者是泛型约束于接口。实际上 Java 中的接口起到的更多的是 C++ 中 Concepts [2] 的作用。只有在用容器去装这些*不同类型*的实例(Instance)的时候,才真正用到一点点子类型多态。这种场景并不多见,更多情况下,在配置好了的状态下,启动应用后,某个容器其实只需要装某个特定类型的实例。然而也不是没有用容器装多个类型的实例的场景,只是说比较少,而且大多数都可以通过让容器去装 lambda 函数的方式来搞定。实在搞不定的只能弄个接口类型出来了,谁让 C++ 中没有 Object 类型呢,毕竟你又不能 delete (void *)p

而且 OOP 还搞出来不少问题,其中比较显著的问题都通过 GoF 的《设计模式》一书总结了,然而不用 OOP 就没这些问题 [4]

但是 OOP 在有些场景下还是挺有用的,比如说 UI,比如说容器。但是用的时候得小心点,OOP 总是诱惑你在不应该 OO 的地方 OO,这是基于你与生俱来的在现实世界中学习到的先验知识认为应该有继承关系,但是在数学上不存在子类型关系的地方带来的认知冲突。

总之,C++ 有 OOP 的能力还是很好的,只是用的时候要非常非常的小心,并且还要注意 C++ 中值语义(Value Semantics)和对象语义(Object Semantics)的区别 [5]

其他

C++ 中还有一些比较容易混淆的概念(暂时只想起来一个):

  • constconstexpr

C++ 中的 const 干了太多事情了。现在只需记住一个原则,Java 的 final 在 C++ 中应该用 constexpr,为一个对象创建只读版本的接口时才使用 const

使用 C++ 提供动态链接库接口是一件非常麻烦的事情。我的建议是不要用 pimpl 模式和 Windows COM 那样的做法;简单一点,直接包装出一个 C 版本的 API 就好了。

没事别把大对象放在栈上,new 一个对象没多大开销,用 unique_ptr 也可以解决生命周期的问题。

未来

我比较期待 C++ 提供这些功能: