C++ 经验谈

学习一门新的语言时,我觉得最重要的是学习该语言的惯例用法(idioms),即其编程范式。

很遗憾的是 C++ 是一门日渐进化并且日趋复杂的语言,是一个具有多重编程范型的语言,被视为一个语言联邦1

即使这样,我认为在日常 C++ 编程时,也应该选择一个相对固定的编程模型,实验并改进之。

在用 C++ 编程时,应该尽量保持谦卑、进取与怀疑。

下面主要记录一下我写 C++ 的不长的时间里的一些经验。

避免编译时依赖。

C++ 没有现代的 package, import 机制,而是使用了继承自 C 的 #include 头文件包含机制。C++ 的复杂和 C 的编译模型导致了 C++ 项目冗长的编译时间。在其他语言中 import/require 可以解决的问题,在 C++ 中必须要程序员自己付出努力。[2]

隐式 (implicit) 的编译依赖不像头文件依赖会导致编译期的错误,却会在运行期招致不可预估的致命错误。比如,当你在编译单元 CompilationUnitA 中 p = new(CompilationUnitB::ClassB) 时,你已经埋下了一颗地雷。以后当你改变 CompilationUnitB::ClassB 的成员而导致其大小 (size) 变化时,你编译 CompilationUnitB ,链接 CompilationUnitA ,运行,然后不知何时那颗地雷就会炸掉。我觉得就 new 的编译期行为,C++ 能做的更好,也应该做的更好。好吧,永远不要跨编译单元调用 new

虚函数调用、跨编译单元的栈上变量/全局变量同样是隐式的编译依赖。

跨编译单元(.so, .dll, .lib)时,区分接口与实现;同一编译单元内,不过度区分接口与实现。

不同的编译单元应该不存在实现依赖,例如: UnitA::ClassA 不应该 依赖于 UnitB::ClassB 的内存大小。UnitA 的头文件 UnitA.h 主要的用途是暴露 (export) UnitA 的接口,如 UnitA::ClassA 中的接口函数。受限于 C++ 对接口与实现之间的支持,UnitA.h 头文件可能写有 UnitA::ClassA 基于当前实现的某些私有数据,但其他编译单元不应依赖之。

跨编译单元的接口不应该是虚函数

跨编译单元的类应该提供显示的创建函数,并尽可能返回引用类型 (std::shared_ptr, etc.)。

以小的、具体的类代替模板(成员)变量(集合)。

你自己写的类,你可以限制其暴露的接口数量,以及取更有意义的接口名称。比如: std::vector<uint64> repo; 可以改成 class Repository repo; 。这会多一点代码,但我乐于如此,特别当 repo 需要一些辅助信息时,更是如此,保持上级类的干净。

多用库,善用 STL 和 boost 。

C++ 的基础库基本上都是模板库,需要根据适用场景进行再封装。

拒绝复杂。

C++ 已经很复杂了,别把自己搞晕了。只要能避免 C++ 的很多坑,就已经很不错了。

1 《Effective C++ 中文版》 第三版 条款01:视 C++ 为一个语言联邦 [2] 《Effective C++ 中文版》 第三版 条款31:将文件间的编译依存关系降至最低

推荐阅读: 孟岩 用C设计,用C++编码