前言
本文给出了一个自定义clang-tidy checker的实例,并介绍了引入该实例的背景和原因,旨在帮助读者以此为蓝本快速开发own clang-tidy checker。
本文并不对clang-tidy的基本使用做过多介绍,不熟悉的读者可以阅读官方文档。
此外,本文同样可以视作对write our own checkers的补充说明。
具体问题
问题背景
在近期工作中,我们的c++研发偶遇了一个问题:common code在某些编译条件下匹配到了不符预期的函数重载,因而引入了较难排查的运行期错误。
具体而言,有demo code如下:
1 | // demo.cpp |
问题分析
在上述代码中,我们使用了一个第三方库中的类ThirdPartyClass
,该类有多个构造函数。
类似的,我们使用了一个第三方库中函数thirdPartyFunction
,该函数有多个重载。
除此之外,我们还定义了一个MyString
类型,该类型在不同编译条件下分别是std::string
和std::wstring
的别名。
我们预期,在使用MyString str.c_str()
作为实参时,ThirdPartyClass
的构造函数和thirdPartyFunction
的函数总是调用形参是const char*
的重载版本。
如果传入的是const wchar_t*
,则应当抛出一个build error,以显式地提示开发当前需要对字符串执行编码转换。
遗憾的是,当我们使用std::wstring
作为MyString
并传入c_str()
时,ThirdPartyClass
的构造函数和thirdPartyFunction
的函数将会调用形参是bool
的重载版本,原因很简单:任何指针类型都可以隐式转换为bool
类型。
在没有任何重载版本匹配const wchar_t*
的情况下,编译器会选择bool
的重载版本,并且导致后续触发运行期错误(构造了错误的对象或者调用了错误的函数)。由于编译器往往认为这种转换是合理的,因此也不会有任何警告信息。
以下分别为使用std::string
和std::wstring
作为MyString
时的运行输出:1
2
3
4
5
6
7
8 use std::string
clang++ ./demo.cpp && ./a.out
Bool constructor called with value: 1
Const char* constructor called with value: Hello, World
Const char* constructor called with value: Hello, World # meet our expectation
Bool function called with value: 1
Const char* function called with value: Hello, World
Const char* function called with value: Hello, World # meet our expectation
1 | use std::wstring |
解决方案
我们可以通过 加强培训
+ code review
+ 高覆盖度单元测试
来避免这种问题,但以上方式需要付出巨大的成本和心智负担。
因此,我们希望简单轻松地解决此问题,至少我们期待可以将错误从运行期提前到编译期。
以下为一些可能的解决方案:
- 修改第三方库
针对这个case,我们可以考虑修改第三方库,为ThirdPartyClass
添加一个const wchar_t*
的构造函数,为thirdPartyFunction
添加一个const wchar_t*
的重载版本。
如此,当我们使用std::wstring
作为MyString
时,编译器将会选择形参为const wchar_t*
的最优重载,从而避免运行期错误。 - 自定义clang-tidy checker
我们可以考虑自定义一个clang-tidy checker,用于检查所有存在const wchar_t*
在函数调用或对象构造过程中转为bool
的场景,并且在检测到相关场景后抛出build warning(或者更进一步地,视作build error)。
方案1的优点无需赘述,但其缺点也显而易见:
- 大多数场景下,第三方库无法直接修改
- 后续引入新的第三方库时,需要重复这个过程以避免再次出现类似问题
因此本文将关注于更加通用化的方案2,即使用自定义clang-tidy checker。
自定义clang-tidy checker
前置知识
实现思路
clang-tidy新增checker有2种方式
- 新建checkers,并将其编译成动态链接库,然后通过令clang-tidy以
-load
参数加载plugin - 新建checkers,并将checkers编译进clang-tidy二进制文件中
方案1也就是所谓的Out-of-tree check plugins
。
显然,相较于方案2需要rebuild clang-tidy,方案1更加灵活。
此外,由于中文互联网社区对方案1的描述较少,因此本文将仅关注方案1,并给出方案1的demo级具体实现。
方案2可参考为clang-tidy添加自定义check。
具体实现
1 | 目录结构 |
1 | # CMakeLists.txt |
1 | // WCharToBoolConversionCheck.h |
1 | // WCharToBoolConversionCheck.cpp |
test
test script
1 | build_and_test.sh |
test result
1 | ./build_and_test.sh |
附录
运行环境
Target: arm64-apple-darwin22.6.0
Homebrew clang version: 17.0.5
cmake version: 3.27.0
src获取与运行
- installed llvm && clang
- git clone git@github.com:zsmj2017/ClangTidyCustomizedCheckersExample.git
- cd ClangTidyCustomizedCheckersExample
- run ./build_and_test.sh or write your own test script