Catch bugs with static analysis
In a previous blog post a few examples were discussed demonstrating how the default behavior of an implicitly-defined copy constructor, copy assignment operator, and destructor can lead to bugs. While these bugs are simple enough to inspect manually, it raises the question, how can these bugs be caught on larger projects?
clang-tidy
is a linter which is part of the LLVM
ecosystem that catches bugs, among other things, via static analysis. This post describes how to
use the tool to lint the code examples in the previous blog post. The tool can be installed via
Homebrew and invoked using the script below.
$ brew install llvm
$ $(brew --prefix llvm)/bin/clang-tidy main.cpp -checks='modernize-*' -- -std=c++23
Consider the code shown below which has a memory leak:
class Int {
int* value;
public:
explicit Int(const int& value): value(new int(value)) {}
};
auto main(int argc, char** argv) -> int {
Int i(1);
Int j(i);
}
If it is not immediately clear that there is a memory leak in this code due to the implicitly
defined constructor, then please observe the output of clang-tidy
shown below:
$(brew --prefix llvm)/bin/clang-tidy main.cpp -checks="clang-analyzer-cplusplus.*" -- -std=c++23
1 warning generated.
main.cpp:11:1: warning: Potential leak of memory pointed to by 'i.value' [clang-analyzer-cplusplus.NewDeleteLeaks]
11 | }
| ^
main.cpp:9:9: note: Calling constructor for 'Int'
9 | Int i(1);
| ^~~~
main.cpp:5:43: note: Memory is allocated
5 | explicit Int(const int& value): value(new int(value)) {}
| ^~~~~~~~~~~~~~
main.cpp:9:9: note: Returning from constructor for 'Int'
9 | Int i(1);
| ^~~~
main.cpp:11:1: note: Potential leak of memory pointed to by 'i.value'
11 | }
| ^
Not only does this clearly warn us of the memory leak, but also that the memory leak is pointed at
by i.value
, that the memory is allocated in the constructor of Int
, and that the memory becomes
orphaned the variable i
goes out of scope. This should be everything necessary to understand that
~Int
needs to be explicitly defined to deallocate the allocated memory.
Now consider the code shown below which has a double free:
class Int {
int* value;
public:
explicit Int(const int& value): value(new int(value)) {}
~Int() {
delete value;
}
};
auto main(int argc, char** argv) -> int {
Int i(1);
Int j(i);
}
Again, we can see based on the output of clang-tidy
below exactly where the bug is:
$(brew --prefix llvm)/bin/clang-tidy main.cpp -checks="clang-analyzer-cplusplus.*" -- -std=c++23
1 warning generated.
main.cpp:8:9: warning: Attempt to free released memory [clang-analyzer-cplusplus.NewDelete]
8 | delete value;
| ^
main.cpp:13:9: note: Calling constructor for 'Int'
13 | Int i(1);
| ^~~~
main.cpp:5:43: note: Memory is allocated
5 | explicit Int(const int& value): value(new int(value)) {}
| ^~~~~~~~~~~~~~
main.cpp:13:9: note: Returning from constructor for 'Int'
13 | Int i(1);
| ^~~~
main.cpp:15:1: note: Calling '~Int'
15 | }
| ^
main.cpp:8:9: note: Memory is released
8 | delete value;
| ^~~~~~~~~~~~
main.cpp:15:1: note: Returning from '~Int'
15 | }
| ^
main.cpp:15:1: note: Calling '~Int'
main.cpp:8:9: note: Attempt to free released memory
8 | delete value;
| ^~~~~~~~~~~~
Specifically, the destructor which is deallocating the memory is being called twice on line 15.
First for the variable j
and second for the variable i
because variables are popped off of the
stack in the reverse order in which they are pushed on. In either case, it can be seen that the
memory is allocated only once in the constructor of Int
for the variable i
.
It is left as an exercise to the reader to run the final "Rule of zero" example through
clang-tidy
, which still finds several things to complain about when enabling all checks, that is
$(brew --prefix llvm)/bin/clang-tidy main.cpp -checks="*" -- -std=c++23
. It is worth mentioning
that clang-tidy
can be noisy, so it is important to be judicious about which checks are enabled.