本系列是开源书C++ Best Practises的中文版,全书从工具、代码风格、安全性、可维护性、可移植性、多线程、性能、正确性等角度全面介绍了现代 C++项目的最佳实践。本文是该系列的第一篇。
前言
C++最佳实践: 支持 Fork 的编码标准文档
本文档旨在收集对 C++最佳实践所进行的协作性讨论,是*《Effective C++》(Meyers)* 和*《C++ Coding Standards》(Alexandrescu, Sutter)* 等书籍的补充。在讨论如何确保整体代码质量的同时,补充了一些没有讨论到的较低级别的细节,并提供了具体的风格建议。
在任何情况下,简单明了都是首选。本文所举示例是为了说明为什么一种选择比另一种更受欢迎。在必要情况下,也会用文字说明。
本文档由 Jason Turner 编写,根据知识共享署名-非商业4.0国际许可协议授权。
免责声明
本文档的编写基于个人经验,你不需要完全同意其中的观点。本文档保存于GitHub上,任何人都可以 fork 供自己使用,或者提交修改建议与大家分享。
本文档启发 O'Reilly 发布了视频: Learning C++ Best Practices
工具
应该在开发过程的早期建立用于执行这些工具的自动化框架,检出源代码、构建和执行测试所使用的命令不应超过 2-3 个,一旦测试完成,应该对代码的状态和质量有接近完整的了解。
源码管理
对于任何软件开发项目来说,源码管理都是绝对必要的,如果还没有,那就开始使用。
- GitHub —— 允许无限制的公共存储库和私有存储库,支持最多 3 个协作者。
- Bitbucket —— 允许无限制的私人存储库,最多 5 个协作者,免费。
- SourceForge —— 仅支持托管开放源码。
- GitLab —— 免费提供无限的公共和私有存储库,包括无限的 CI 执行器(CI Runner)。
- Visual Studio Online (http://www.visualstudio.com/what-is-visual-studio-online-vs) —— 无限的公共存储库,私有存储库收费,支持 git 或 TFVC。另外提供: 问题跟踪、项目计划(包括 Scrum 等多个敏捷模板)、集成托管构建,所有特性都可以集成到 Microsoft Visual Studio 中,仅支持 Windows。
构建工具
使用广泛接受的行业标准构建工具,可以防止在做探索、链接新库、打包产品等等工作时重复发明轮子。例子包括:
- CMake
- 对于构建性能,请考虑: https://github.com/sakra/cotire
- 对于增强可用性,请考虑: https://github.com/toeb/cmakepp
- 使用 https://cmake.org/cmake/help/v3.6/command/target_compile_features.html 作为 C++标准 flag
- 考虑使用 https://github.com/cheshirekow/cmake_format 自动格式化 CMakeLists.txt 文件
- CMake 特定最佳实践请参考后续的延伸阅读部分
cmake --build
提供了平台无关的通用编译接口- Waf
- FASTBuild
- Ninja —— 可以极大优化大型项目的增量构建时间,可以作为 CMake 的 target。
- Bazel —— 基于网络工件缓存和远程执行的快速增量构建
- Buck —— 类似于 Bazel,对 iOS 和 Android 有很好的支持
- gyp —— 谷歌 chromium 的构建工具
- maiken —— 具有 maven 配置风格的跨平台构建工具
- Qt Build Suite —— 基于 Qt 的跨平台构建工具
- meson —— 快速、对用户友好的开源构建系统
- premake
请记住,这不仅是构建工具,也是编程语言。请尽量维护良好整洁的构建脚本,并遵循正在使用的工具的推荐实践。
包管理器
包管理是 C++的重要主题,目前还没有明确的赢家。请考虑使用包管理器来帮助跟踪项目的依赖关系,从而帮助新人更容易开始参与项目。
- Conan —— 跨平台 C++依赖管理器
- hunter —— CMake 驱动的跨平台包管理器,适用于 C/C++
- C++ Archive Network (CPPAN) —— 跨平台 C++依赖管理器
- qpm —— Qt 的包管理器
- build2 —— 类 Cargo 的 C++包管理器
- Buckaroo —— 真正去中心化的跨平台依赖管理器,适用于 C/C++等等
- Vcpkg —— 微软 C++库管理器,支持 Windows, Linux 和 MacOS
持续集成
选择了构建工具之后,接下来需要设置持续集成环境。
在更改被推送到存储库时会触发持续集成(CI)工具自动构建源代码,可以私有部署 CI 工具或使用托管的 CI 系统。
- Travis CI
- 能与很好的与 C++一起工作
- 设计与 GitHub 一起使用
- GitHub 公共存储库可以免费使用
- AppVeyor
- 支持 Windows、MSVC 和 MinGW
- GitHub 公共存储库可以免费使用
- Hudson CI / Jenkins CI
- 需要 Java 应用服务器
- 支持 Windows、OS X 和 Linux
- 可以通过许多插件进行扩展
- TeamCity
- 对开源项目免费
- Decent CI
- 简单持续集成,可以将结果发布到 GitHub
- 支持 Windows、OS X 和 Linux
- 使用ChaiScript
- Visual Studio Online (http://www.visualstudio.com/what-is-visual-studio-online-vs)
- 与 Visual Studio Online 的源代码库紧密集成
- 使用 MSBuild (Visual Studio 的构建引擎),可在 Windows、OS X 和 Linux 上使用
- 提供托管的构建代理,也允许用户提供构建代理
- 可以在 Microsoft Visual Studio 中控制和监控
- 通过 Microsoft Team Foundation Server 进行内部安装
- GitLab
- 使用自定义 Docker 镜像,因此可用于 C++
- 有免费的共享执行器
- 提供简单的覆盖率结果分析
如果在 GitHub 上有开源、公开托管的项目:
- 现在就把 Travis Ci 和 AppVeyor 整合起来。关于如何在基于 C++ cmake 的应用程序中启用的简单示例,请参考: https://github.com/ChaiScript/ChaiScript/blob/master/.travis.yml
- 启用覆盖工具(Codecov 或 Coveralls)
- 启用Coverity Scan
这些工具都是免费的,设置起来也相对容易。一旦把它们都设置好,就可以对项目进行持续的构建、测试、分析和报告,并且免费。
编译器
启用所有可用、合理的告警选项,有些告警选项只在启用了优化的情况下才有效,或者优化级别越高,效果越好,例如 GCC 中的-Wnull-dereference
。
应该使用尽可能多的编译器,每个编译器对标准的实现略有不同,支持多个编译器将有助于确保实现最可移植、最可靠的代码。
GCC / Clang
-Wall -Wextra -Wshadow -Wnon-virtual-dtor -pedantic
-Wall -Wextra
合理、标准-Wshadow
如果变量声明覆盖了父上下文中的变量,则警告用户-Wnon-virtual-dtor
如果带有虚函数的类有非虚析构函数,则警告用户,有助于捕获难以跟踪的内存错误-Wold-style-cast
对 C 风格的类型转换发出警告-Wcast-align
警告有潜在性能问题的强制类型转换-Wunused
警告任何未使用的东西-Woverloaded-virtual
如果重载(而不是覆盖)虚函数,则发出警告-Wpedantic
如果使用了非标准的 C++则发出警告(所有版本的 GCC, Clang >= 3.2)-Wconversion
对可能丢失数据的类型转换发出警告-Wsign-conversion
对影响到符号的类型转换发出警告(Clang 所有版本,GCC >= 4.3)-Wmisleading-indentation
如果代码中有缩进,但没有对应的代码块,则发出警告(仅在 GCC >= 6.0 中)-Wduplicated-cond
如果if
/else
分支有重复条件,则发出警告(仅在 GCC >= 6.0 中)-Wduplicated-branches
如果if
/else
分支有重复的代码,则发出警告(仅在 GCC >= 7.0 中)-Wlogical-op
在可能需要按位操作的地方使用逻辑操作时发出警告(仅在 GCC 中)-Wnull-dereference
如果检测到空解引用将发出警告(仅在 GCC >= 6.0 中)-Wuseless-cast
如果执行强制转换到相同的类型,则会发出警告(仅在 GCC >= 4.8 中)-Wdouble-promotion
如果float
隐式提升为double
则发出警告(GCC >= 4.6, Clang >= 3.8)-Wformat=2
对输出格式化函数(即printf
)的安全问题发出警告-Wlifetime
显示对象生命周期问题(目前只有 Clang 的特殊分支)
考虑使用-Weverything
,并且只在需要的情况下禁用少数警告。-Weffc++
警告模式可能太吵了,但如果对项目适用,也可以使用。
MSVC
/permissive-
—— 执行标准一致性/W4 /w14640
—— 使用并考虑以下内容(参见下面的描述)
/W4
一切合理的警告/w14242
'identifier': 从'type1'到'type1'的转换,可能丢失数据/w14254
'operator': 从“type1:field_bits”到“type2:field_bits”的转换,可能丢失数据/w14263
'function': 成员函数不重写任何基类虚成员函数/w14265
'classname': 类有虚函数,但析构函数不是该类的虚实例,可能无法正确析构/w14287
'operator': 无符号/负常数不匹配/we4289
nonstandard extension used: 'variable': 在 for 循环中声明的循环控制变量在 for 循环作用域之外使用/w14296
'operator': 表达式总是'布尔值(boolean_value)'/w14311
'variable': 指针从'type1'转换到'type2'时被截断/w14545
逗号前的表达式计算的是缺少参数列表的函数/w14546
逗号前的函数调用缺少参数列表/w14547
'operator': 逗号前的运算符无效,预期运算符有副作用/w14549
'operator': 逗号前的运算符无效,想要“运算符”吗?/w14555
表达式没有效果,表达式预期带有副作用/w14619
pragma warning: 没有警告号码/w14640
在线程不安全的静态成员初始化时启用警告/w14826
从'type1'到'type_2'的转换会扩展符号,可能会导致意外的运行时行为/w14905
宽字符串字面量转换为'LPSTR'/w14906
字符串字面量转换为'LPWSTR'/w14928
非法的拷贝初始化,已隐式应用多个用户定义转换
不建议
/Wall
会对标准库中包含的文件发出警告,有太多额外的警告,因此没什么用。
通用
一开始就设置非常严格的警告,在项目开始后试图提高警告级别可能会很痛苦。
考虑使用将警告视为错误的设置,例如 MSVC 中的/Wx
,以及 GCC/Clang 中的-Werror
。
基于 LLVM 的工具
基于 LLVM 的工具与能够输出编译命令数据库的构建系统(例如 cmake)配合得最好,例如:
$ cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON .
如果没用这样的构建系统,可以考虑Build EAR,它可以与现有构建系统挂钩,并生成编译命令数据库。
CMake 现在也提供了在正常编译期间调用clang-tidy
的内置支持。
静态检查
最好的选择是将静态分析器作为自动化构建系统的一部分运行,cppcheck 和 clang 可以满足免费选项的要求。
Coverity Scan
Coverity提供免费(开源)静态分析工具包,可以用于与Travis CI和AppVeyor集成的每个提交。
PVS-Studio
PVS-Studio是用于检测用 C、C++和 C#编写的程序源代码中的 bug 的工具,对个人学术项目、开源非商业项目和个人开发者的独立项目都是免费的,可以在 Windows 和 Linux 环境下工作。
Cppcheck
Cppcheck是免费、开源的。它努力争取零误报,并且做得很好。因此,应该启用所有警告: --enable=all
。
备注:
- 为了正确工作,需要格式完整的头文件路径,所以在使用前不要忘记传递:
--check-config
。 - 查找未使用的头文件时
-j
不能大于 1。 - 如果需要检查所有的代码,请记住为带有大量 #ifdef 的代码添加
--force
。
cppclean
cppclean是开源静态分析器,专注于发现 C++源代码中导致大型代码库开发缓慢的问题。
CppDepend
CppDepend通过分析和可视化代码依赖关系、定义设计规则、进行影响分析以及比较不同版本的代码,简化了对复杂 C/C++代码库的管理,对开源贡献者是免费的。
Clang 的静态分析器
Clang 的分析程序的默认选项适用于各个平台,可以直接通过CMake使用,也可以通过基于llvm的工具中的clang-check
和clang-tidy
调用。
此外,CodeChecker可以作为 clang 的静态分析前端。clang-tidy
可以通过Clang Power Tools扩展轻松的和 Visual Studio 一起使用。
MSVC 的静态分析器
可以通过/analyze
命令行选项启用,可以使用默认选项。
Flint / Flint++
Flint和Flint++是根据 Facebook 编码标准分析 C++代码的 linter。
OCLint
OCLint是免费、自由、开源的静态代码分析工具,可以通过许多不同的方式提高 C++代码的质量。
ReSharper C++ / CLion
这两种来自JetBrains的工具都提供了一定程度的静态分析和自动修复功能,为开源项目负责人提供了免费许可证选项。
Cevelop
基于 Eclipse 的Cevelop IDE 提供了各种静态分析和重构/代码修复工具。例如,可以用 C++的constexprs
替换宏,重构命名空间(提取/内联using
,限定名称),并将代码重构为 C++11 的统一初始化语法。Cevelop 是免费的。
Qt Creator
Qt Creator 可以插入 clang 静态分析器。
clazy
clazy是基于 clang 的分析 Qt 使用情况的工具。
IKOS
IKOS是开源静态分析器,由 NASA 开发。它以抽象解释为基础,用 C++编写,使用 LLVM 为 C 和 C++提供了分析器。源代码可以在Github上找到。
运行时检查
代码覆盖率分析
覆盖率分析工具应该在测试执行时运行,以确保整个应用程序都被测到。不幸的是,覆盖率分析需要禁用编译器优化,这将导致测试执行时间大大延长。
- Codecov
- 与 Travis CI 和 AppVeyor 集成
- 对于开源项目免费
- Coveralls
- 与 Travis CI 和 AppVeyor 集成
- 对于开源项目免费
- LCOV
- 有很多配置项
- Gcovr
- kcov
- 可与 codecov 和 coveralls 集成
- 不需要特殊的编译器 flag,只需要 debug 符号,就可以输出代码覆盖率报告
- OpenCppCoverage
- Windows 上的开源代码覆盖率工具
Valgrind
Valgrind是运行时代码分析器,可以检测内存泄漏、竞争条件和其他相关问题,支持各种 Unix 平台。
Dr Memory
和 Valgrind 类似。http://www.drmemory.org
GCC / Clang Sanitizers
这些工具提供了许多与 Valgrind 相同的特性,但内置在编译器中,易于使用,并提供问题报告。
- AddressSanitizer
- MemorySanitizer
- ThreadSanitizer
- UndefinedBehaviorSanitizer
注意可用的 sanitizer 选项,包括运行时选项。https://kristerw.blogspot.com/2018/06/useful-gcc-address-sanitizer-checks-not.html
Fuzzy 分析器
如果项目接受用户定义的输入,可以考虑运行模糊输入测试。
这些工具都使用覆盖率报告来寻找新的代码执行路径,并尝试为代码提供新的输入。它们可以发现崩溃、挂起以及一些没有被考虑到的输入。
- american fuzzy lop
- LibFuzzer
- KLEE —— 可以为单独的函数提供模测试
变异测试
这些工具获取在单元测试运行期间执行的代码,并改变执行的代码。如果测试在有突变的情况下仍然通过,那可能意味着在测试套件中存在有缺陷的测试。
控制流保护
MSVC 的控制流保护(Control Flow Guard)增加了高性能的运行时安全检查。
检查 STL 实现
_GLIBCXX_DEBUG
与 GCC 的 libstdc++的实现。参见Krister的博客文章。
堆分析
- https://epfl-vlsc.github.io/memoro —— 一个详细的堆分析器
忽略警告
如果团队一致认为编译器或分析器对不正确或不可避免的错误发出警告,则团队需要尽可能只在最小的范围内禁用特定的错误警告。
在对一段代码禁用该警告后,请确保重新启用该警告,没人希望禁用的警告被泄露到其他代码中。
测试
上面提到的 CMake 有一个用于执行测试的内置框架,请确保使用的任何构建系统都能够执行内置测试。
为了进一步帮助执行测试,请考虑使用某个单元测试库,如Google Test、Catch、CppUTest或Boost.Test,以帮助组织测试。
单元测试
单元测试针对的是可以独立测试的小代码块和独立功能。
集成测试
对于提交的每个特性或 bug 修复,都应该启用测试。参见上文介绍的代码覆盖率分析。这些测试比单元测试级别更高,但仍然应该被限制在单个特性的范围内。
逆向测试
不要忘记确保测试代码中的错误处理,并且确保其能够正常工作。如果目标是 100%的代码覆盖率,很明显这些错误场景也需要被覆盖的。
调试
uftrace
uftrace可以用来生成程序执行的函数调用图。
rr
rr是一个免费、开源的反向调试器,支持 C++。
其他工具
Lizard
Lizard提供了针对 C++代码库运行复杂性分析的非常简单的接口。
Metrix++
Metrix++可以识别并报告代码中最复杂的部分,从而帮助我们减少复杂代码,帮助编译器更好的理解和优化代码。
ABI Compliance Checker
ABI Compliance Checker (ACC)可以分析两个库版本,并生成关于 API 和 C++ ABI 变化的详细兼容性报告,可以帮助库开发人员发现无意的破坏性更改,以确保向后兼容性。
CNCC
Customizable Naming Convention Checker(可自定义的命名约定检查器)可以报告代码中不遵循特定命名约定的标识符。
ClangFormat
ClangFormat可以自动检查并纠正代码格式,以匹配组织约定。可以参考关于clang-format的系列文章。
SourceMeter
SourceMeter提供了免费版本,可以为代码提供许多不同的度量,也可以调用 cppcheck。
Bloaty McBloatface
Bloaty McBloatface是用于类 unix 平台的二进制大小分析器。