摘要
借着公司内和其他小组的一个分享,把自己几年来C++开发的一点思考总结一下。全篇没有高屋建瓴的观点,基本都是些细节方面的注意事项。希望能给大家提供一点帮助。
构建工具
C/C++世界里有不少的构建工具:make、autotools、scons、CMake、Bazel。但近几年比较流行的,也就是CMake和Bazel。所以这一部分,也就大概对比下这两个工具吧。
究竟该选择哪个工具,我觉得可以从如下几个方面来对比一下:
1、上手难度
因为Bazel采用了类似Python的语法,所以其学习曲线相比CMake要平缓一些。但当我们考虑上手难度时,除了学习曲线之外,还要考虑文档的完备性、该工具的通用性等各个角度。当综合考虑时,我觉得CMake是一个尽管保守但仍旧不错的选择。主要原因就在于,CMake几乎已经成为现在C++的事实标准。使用CMake,就意味着:
你可以把你熟悉CMake的技能用在折腾别的C++项目上。而这点之所以重要,是因为你在利用某个第三方库的时候,往往需要大概研究下它的编译过程。
CMake的官方文档和stackoverflow上的问答也比较完善。一旦遇到一个问题,往往通过搜索引擎能快速的得到答案。
另外,从设计理念上来看,CMake提供的解决方案是改革式的:它并没有提供一个全新的解决方案,而是和Make、Visual Studio或者其他现有的构建工具来结合使用的。而这就使得你无需丢弃在其他工具上所积累起来的开发经验——例如你熟悉make工具,哪怕是一个CMake维护的项目,你也可以毫不费力就知道如何来查看编译参数,以及控制编译并发度等等。
而对于Bazel则不是如此。Bazel完全以革命者的姿态完整提供了一整套解决方案,所有的使用细节你都要从头开始。加上文档的匮乏,这就使得你也得花上一段时间,才能熟悉Bazel。
2、thirdparty的管理
Bazel内置了对thirdparty源码级别依赖的支持https://docs.bazel.build/versions/master/external.html:
thirdparty可以是用Bazel构建的,也可以不是。对于非Bazel项目,你需要额外为其添加一个Bazel的描述文件。
thirdparty可以是一个本地项目,也可以是一个git仓库或者http链接
所以总的来看,Bazel对thirdparty支持还是非常友好的。
就这点对比来看,CMake其实做的是不太好的。CMake尽管也有ExternalProject https://cmake.org/cmake/help/latest/module/ExternalProject.html的feature,但根据实际经验来看,使用和维护都比较的复杂。所以我还是更倾向于写几个脚本来下载和编译这些thirdparty依赖。
这里可以拿我参与维护的Pegasus https://github.com/XiaoMi/rdsn/tree/master/thirdparty项目为例。在该项目中,我们依赖了几个不同类型的项目:
从构建工具上来看,这些依赖有使用CMake的,有使用make的,有使用autotools的
从来源上来看,有的依赖来自git仓库,有的来自http链接,有的则是从一个大的项目里面挑选了一个更小的模块使用
从代码的使用方式上来看,有的是直接拿来用,有的还需要稍微修改下源代码。
而通过shell脚本,这些各种各样的场景我们都能非常方便、直接、易维护的得以支持。
3、其它
Bazel和CMake当然还有些其它方面值得对比,但并非一些通用的点,这里就简单列举下,不再详细展开了:
IDE集成
缓存编译结果,从而加速编译过程
多语言混合变成的支持
分布式编译
跨平台的支持
再补充一个别人的讨论Q群:731611386
编程规范
强烈推荐Google C++ Style:https://google.github.io/styleguide/cppguide.html。尽管它禁止了很多C++ feature而被很多人黑的很惨,但从工程的角度而言,它的确提供了非常多极其中肯的建议。说到底,编程规范的存在,主要就是可以让水平参差不齐的工程师们,可以在一起协作出风格较为一致的项目来。
也存在一些工具可以对google规范进行检查:
clang-format https://clang.llvm.org/docs/ClangFormat.html
cpplint https://github.com/cpplint/cpplint
因为google的规范文档对C++ feature的取舍原因讲的非常好,这里就不再赘述了。唯一想补充的是异常:
C++在语法层面对异常支持不太友好:你无法通过函数签名来得知一个函数到底会抛出哪些异常。例如:
void GetSomeResource(const char* resource_name);
如果这个接口没有良好文档或注释,并且也没有代码可翻时,你在调用这个接口时很有可能会漏掉一些错误情况——因为它可能抛出异常。更要命的是,一个疏于捕获的异常一旦触发,线上的程序就会crash。
其实解释这么多,大家只要和Java中的异常机制对比一下,就高下立判了。对于这个话题,王垠的这篇博客http://www.yinwang.org/blog-cn/2017/05/23/kotlin值得一看的。
在运维Pegasus项目时,遇到过一个老版本glibc的bug:如果多个线程同时抛出异常,程序会陷入死循环。这个bug的发现也是个有趣的过程,后面我专门写篇文章展开吧。
在禁用异常后,程序就只能用错误码来进行错误处理。对于很多项目,大家都采用一套类似的范式,可以参考tensorflow的做法https://github.com/tensorflow/tensorflow/blob/master/tensorflow/core/lib/core/status.h
C++的新特性
如果能使用C++的新特性,当然是尽量使用的好。我自己在开发中,觉得非常方便必须使用的新特性有:
智能指针
右值,以及C++14中右值得capture
lambda, bind
initialize list
想补充说一下的是auto,我自己不是特别喜欢这个feature,也非常赞同google规范中的对auto的限制:仅当可以提高代码可读性时,使用auto
这里不由得就想扯起java 10中的var。虽然能方便开发,但觉得更多的是会被滥用。而一个可能被滥用的feature,还不如没有的好。
第三方utility
在做项目开发的时候,一般会有很多琐碎的需求,从而也需要很多utility工具包。这里把我遇到的一些需求整理一下:
算法和数据结构:stl, boost
错误码管理:参见tensorflowhttps://github.com/tensorflow/tensorflow/blob/master/tensorflow/core/lib/core/status.h
C语言的字符串封装:string_view https://github.com/abseil/abseil-cpp/blob/master/absl/strings/string_view.h
字符串的各种操作、转换、打印:可以多翻翻abseil https://github.com/abseil/abseil-cpp, 以及folly https://github.com/facebook/folly,另外也推荐fmtlibhttps://github.com/fmtlib/fmt
线程安全的、无锁的数据结构、线程池: folly
google全家桶:gtest,gflags, glog, protobuf, grpc
最后,也推荐下kudu这个项目,里面有自己实现的一些工具包https://github.com/cloudera/kudu/tree/master/src/kudu/util,以及对google开源项目中utility的整理https://github.com/cloudera/kudu/tree/master/src/kudu/gutil。
单元测试
每个程序员都讨厌写测试。就我自己而言,我觉的单元测试的目的有以下几个:
确保功能的实现和预期一致
防止程序在重构的时候出问题
给模块的使用者,提供使用示例
值得一提的是,对于C++项目,除了功能性测试之外,你最好还能让你的单元测试通过一些自动化工具的检测,如:
valgrind:检查内存泄露,以及非法访存
Address Sanitizer:检测非法访存https://github.com/google/sanitizers/wiki/AddressSanitizer
Thread Sanitizer:检测线程竞争https://clang.llvm.org/docs/ThreadSanitizer.html
写在最后
自己的整理这些内容时,脑子里反复萦绕的一个问题是:我们在开发一个项目时,所要遵守的各种流程和规范到底是不是真的有必要的?说的更直白一点就是,“代码洁癖”这东西到底有没有意义?
我的看法是:代码洁癖不是一个原则,而是在投入和产出上的一种权衡。如果仅仅快速试错,那么就不需要维持代码洁癖,因为你完全不知道你今天写的代码究竟能存活多久。而如果是一个马拉松式的项目,代码洁癖就值得维持,因为它对于项目的维护的确很有意义。
最后,贴一个C语言学习交流群:731611386