Testing & Tooling
What the compiler doesn't catch, and how to find out where the time actually goes.
Intermediate
TEST(SuiteName, TestName) block plus a handful of assertion macros gets you
most of the way there.
// sqrt.h
double squareRoot(double a) {
double b = sqrt(a);
return (b != b) ? -1 : b; // b != b is true only for NaN — the classic NaN-check idiom
}
// sqrt_test.cpp
#include "sqrt.h"
#include <gtest/gtest.h>
TEST(SquareRootTest, PositiveNumbers) {
ASSERT_EQ(6, squareRoot(36.0));
ASSERT_EQ(0, squareRoot(0.0));
}
TEST(SquareRootTest, NegativeNumbersReturnSentinel) {
ASSERT_EQ(-1.0, squareRoot(-15.0));
}
int main(int argc, char **argv) {
testing::InitGoogleTest(&argc, &argv);
return RUN_ALL_TESTS();
}
ASSERT_EQ stops the current test immediately on failure; EXPECT_EQ
records the failure but keeps running the rest of the test, useful when you want to see every
assertion's result in one run rather than stopping at the first failure.
cppcheck finds classes of bugs the compiler doesn't reliably reject — the
code below has three real bugs, and at least the use-after-free and uninitialized-read cases
(1 and 2) are the kind of thing g++ -Wall -Wextra can miss entirely (recent GCC/Clang
versions may warn on the constant out-of-bounds index in case 3 via -Warray-bounds, but the other
two are true blind spots for the compiler's warnings, which is the point a static analyzer makes):
void useAfterFree(int *p) {
delete p;
int j = *p; // (1) use-after-free — p was just deleted
}
void uninitializedRead() {
int uninitialized;
if (uninitialized == uninitialized) { // (2) reading a variable that was never given a value
std::cout << "always true, for the wrong reason\n";
}
}
void outOfBounds() {
int ages[3];
ages[0] = 18; ages[1] = 21; ages[2] = 35;
ages[3] = 40; // (3) out-of-bounds write — valid indices are 0..2
}
cppcheck directly — the
buggy functions are deliberately never called from main(), since running them is
the opposite of the point:
TopNotchNote/cpp/testing_bug_patterns_demo.cpp
delete doesn't null the pointer for you (see
Pointers & References for the dangling-pointer
discussion this connects to). A static analyzer is a cheap first pass specifically for the
bug categories the type system can't rule out.
+
example, instead of relying on an implicit conversion). The second usually can't be
eliminated — the function has to return something — but modern compilers routinely
elide the copy entirely (guaranteed copy elision since C++17 for several common cases), and
where they can't, move semantics makes the fallback cheap
rather than a full deep copy.