unit testing with GoogleTest

GoogleTest (gtest) is the most common C++ unit-testing framework — a 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();
}
            
g++ -std=c++17 sqrt_test.cpp -lgtest -pthread -o test | | link against gtest (installed separately) and run the resulting binary. Note: link -lgtest only here, NOT -lgtest_main — the source above defines its own main(), and gtest_main supplies its own main() too, so linking both causes a "multiple definition of main" error. (If you'd rather not write your own main(), delete the int main(){...} block above and link -lgtest -lgtest_main -pthread instead.) |'tt1'
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.

test-driven development, briefly

The TDD cycle: write a failing test for behavior that doesn't exist yet (red), write the minimum code to make it pass (green), then clean up the implementation with the test suite as a safety net (refactor). The value isn't the ritual — it's that the test suite ends up actually covering the behavior you built, rather than being written after the fact against whatever the code happened to do.

what a static analyzer catches

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 --enable=all main.cpp | https://cppcheck.sourceforge.io/ | flags all three of the bugs above, plus dozens of other classes of defect, without executing the code |'tt2'
All three bugs in one file, ready to hand to 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
None of these are compile errors — array bounds aren't checked at compile time for a plain C array, an uninitialized local just holds whatever garbage was already in that stack slot, and 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.

profiling before optimizing

Algorithm choice dominates micro-tuning — picking O(n log n) over O(n²) matters more than loop unrolling ever will. And performance is rarely spread evenly through a codebase: it's the 80/20 rule in its original form, a small fraction of the code accounts for most of the runtime. Guessing which fraction, without a profiler, is unreliable even for experienced engineers — measure first, on more than one representative input (a profiler run against a single small dataset can point you at the wrong bottleneck entirely).
g++ -pg -O2 -o main main.cpp | | build with gprof instrumentation enabled |'tt3'
perf record -g ./main | | sample-based profiling on Linux — lower overhead than instrumentation-based profiling |'tt4'
See the C++ Cheat Sheet's compiler-inspection commands for viewing preprocessed output and assembly directly, and Inheritance & Polymorphism's vtable-dump command for inspecting virtual dispatch specifically.

where the small, invisible costs actually come from

A useful mental model once the algorithm is right: true temporary objects never appear in the source code — they're created implicitly, either by an implicit type conversion needed to make a function call succeed, or by a function returning an object by value. The first case is avoidable by design (overloading for the exact types you're called with, as in Operator Overloading's mixed-type + 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.

where to go from here

C++ Cheat Sheet — the compiler commands this page builds on.
C++ Best Practices — the idioms a static analyzer and a code reviewer both look for.
Move Semantics — why the "temporary object" cost above usually isn't as bad as it sounds today.

reference

GoogleTest documentation
cppcheck.sourceforge.io