Free Handbook · Every example compiled & verified

C++ + Tools

C++ as it is built at work: CMake, Conan packages, GoogleTest, clang-tidy and clang-format, sanitizers in CI, PostgreSQL, Qt, Docker and GitHub Actions.

0 / 145 lessons🔥 0 day streak
ShareXLinkedIn

Module 12 · what you'll be able to do

  • Write a CMakeLists.txt with a library, an executable, a test target and warnings, and build it out of source
  • Pull third-party libraries with Conan (or vcpkg) instead of copying source files around
  • Write GoogleTest unit tests and run them with ctest
  • Keep code consistent and catch bugs early with clang-format, clang-tidy and sanitizer builds
  • Query PostgreSQL from C++, build a small Qt window, and ship a C++ service in a Docker image built by GitHub Actions
01

The C++ toolchain at a glance

So far every program has been one main.cpp compiled with one command. A real C++ project is dozens of source files, several libraries, a test suite and at least three platforms, and the language itself has no standard build tool or package manager. Every team therefore assembles the same stack: a build system generator (almost always CMake), a package manager (Conan or vcpkg), a test framework (GoogleTest or Catch2), formatting and static analysis from LLVM (clang-format, clang-tidy), and CI that builds with sanitizers. These labs show the smallest real version of each.

ToolJobYou meet it when
CMake (+ Ninja)Describe targets once, generate builds for any compiler and IDEDay one of almost any C++ job
Conan / vcpkgDownload and build third-party librariesThe first time you need JSON, HTTP, a database driver
GoogleTest / Catch2Unit tests, run by ctestEvery pull request
clang-format / clang-tidyConsistent formatting; static analysis and modernisationCode review and CI
Sanitizers, Valgrind, perfMemory errors, races, profilingDebugging and performance work
QtCross-platform desktop and embedded UIsDesktop, automotive, industrial and medical software
Docker + GitHub ActionsReproducible builds and deploymentShipping services and CI
02

C++ + CMake: targets, not commands

CMake does not compile anything itself: it reads CMakeLists.txt and generates a build for Ninja, Make, Visual Studio or Xcode. Modern CMake is organised around targets (add_library, add_executable) and the properties attached to them: which sources, which include directories, which C++ standard, which libraries to link. PUBLIC properties propagate to anything that links the target; PRIVATE ones stay inside it.

C++ + CMake

A library, an app and a test target, with warnings on

The pricing logic lives in a library so both the app and the tests link the same code. target_compile_features(... cxx_std_20) requests C++20 for that target and everything using it. Build out of source: the build/ folder holds every generated file and can be deleted at any time. compile_commands.json is what clangd, clang-tidy and many IDEs read to understand your project.

cmake
cmake_minimum_required(VERSION 3.25)
project(shop LANGUAGES CXX)

set(CMAKE_EXPORT_COMPILE_COMMANDS ON)      # for clangd and clang-tidy

add_library(pricing src/pricing.cpp)
target_include_directories(pricing PUBLIC include)
target_compile_features(pricing PUBLIC cxx_std_20)
target_compile_options(pricing PRIVATE
    $<$<CXX_COMPILER_ID:MSVC>:/W4 /permissive->
    $<$<NOT:$<CXX_COMPILER_ID:MSVC>>:-Wall -Wextra -Wpedantic>)

add_executable(shop src/main.cpp)
target_link_libraries(shop PRIVATE pricing)

enable_testing()
find_package(GTest REQUIRED)
add_executable(pricing_tests tests/pricing_test.cpp)
target_link_libraries(pricing_tests PRIVATE pricing GTest::gtest_main)
include(GoogleTest)
gtest_discover_tests(pricing_tests)

# cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug
# cmake --build build
# ctest --test-dir build --output-on-failure
03

C++ + Conan: third-party libraries

C++ has no built-in package manager, so for years teams copied library sources into their repositories. Conan and vcpkg fix that: you list dependencies with versions, and the tool downloads prebuilt binaries (or builds them) for your compiler and settings, then hands CMake a ready find_package. Conan is common in enterprise and embedded work; vcpkg (from Microsoft) is popular on Windows and integrates with CMake presets.

C++ + Conan

A conanfile.txt with fmt, nlohmann_json and GoogleTest

conan install resolves the three packages for your profile (compiler, version, build type), then writes a CMake toolchain file. Point CMake at it and find_package(nlohmann_json) just works. Pin exact versions so every machine and CI run builds the same thing. The vcpkg equivalent is a vcpkg.json manifest with the same three names.

ini
# conanfile.txt
[requires]
fmt/11.1.4
nlohmann_json/3.12.0
gtest/1.16.0

[generators]
CMakeDeps
CMakeToolchain

[layout]
cmake_layout

# conan profile detect            # once per machine
# conan install . --build=missing -s build_type=Debug
# cmake --preset conan-debug
# cmake --build --preset conan-debug
#
# In CMakeLists.txt:
#   find_package(nlohmann_json REQUIRED)
#   target_link_libraries(shop PRIVATE nlohmann_json::nlohmann_json fmt::fmt)
04

C++ + GoogleTest: unit tests

GoogleTest is the most widely used C++ test framework. A test is a TEST(Suite, Name) block with assertions: EXPECT_* records a failure and continues, ASSERT_* stops the test. EXPECT_THROW checks exceptions, and parameterised tests run one body over many inputs. ctest runs every discovered test. Catch2 and doctest are lighter alternatives with the same ideas.

C++tests/pricing_test.cpp
#include <gtest/gtest.h>
#include <stdexcept>
#include "pricing.h"   // double gross(double net, double taxRate); double discount(int qty);

TEST(Pricing, AddsTaxToNetPrice) {
    EXPECT_DOUBLE_EQ(gross(100.0, 0.18), 118.0);
}

TEST(Pricing, BulkDiscountFromTenItems) {
    EXPECT_EQ(discount(9), 0.0);
    EXPECT_EQ(discount(10), 0.05);
}

TEST(Pricing, RejectsNegativePrice) {
    EXPECT_THROW(gross(-1.0, 0.18), std::invalid_argument);
}

// $ ctest --test-dir build --output-on-failure
// 100% tests passed, 0 tests failed out of 3

The idea needs no framework. While learning, a check helper verifies pure functions just as well, and it is exactly what you would write in a coding round:

C++main.cpp
#include <cmath>
#include <iostream>
#include <stdexcept>
#include <string>

double gross(double net, double taxRate) {
    if (net < 0) throw std::invalid_argument("net must not be negative");
    return net * (1 + taxRate);
}

double discount(int qty) { return qty >= 10 ? 0.05 : 0.0; }

int failures = 0;
void check(const std::string& name, bool ok) {
    std::cout << (ok ? "PASS " : "FAIL ") << name << '\n';
    if (!ok) ++failures;
}

int main() {
    check("adds tax", std::abs(gross(100.0, 0.18) - 118.0) < 1e-9);
    check("no discount at 9", discount(9) == 0.0);
    check("discount at 10", discount(10) == 0.05);
    bool threw = false;
    try { gross(-1.0, 0.18); } catch (const std::invalid_argument&) { threw = true; }
    check("rejects negative", threw);
    return failures == 0 ? 0 : 1;   // non-zero exit fails a CI step
}
Outputcompiled & run with real C++
PASS adds tax
PASS no discount at 9
PASS discount at 10
PASS rejects negative

Floating-point results are compared with a tolerance, never with == after arithmetic (EXPECT_DOUBLE_EQ does this for you in GoogleTest). Returning non-zero from main on failure is what makes a test runner fail the build.

05

C++ + LLVM tools: clang-format and clang-tidy

clang-format rewrites code to a style defined in a .clang-format file, so formatting is never discussed in code review again. clang-tidy is a static analyser with hundreds of checks: likely bugs (bugprone-*), modernisation (modernize-use-override, modernize-use-nullptr), performance (performance-unnecessary-copy-initialization) and the C++ Core Guidelines (cppcoreguidelines-*). Both read compile_commands.json from the CMake build.

C++ + LLVM

A .clang-tidy config and the commands CI runs

Enable a broad set of check families, then switch off the few that do not fit your codebase. WarningsAsErrors makes CI fail on new findings. clang-tidy --fix applies the automatic fixes (for example adding override or replacing NULL with nullptr); review the diff before committing. Most editors run both tools on save through clangd.

yaml
# .clang-tidy
Checks: >
  bugprone-*,
  performance-*,
  modernize-*,
  readability-*,
  cppcoreguidelines-*,
  -modernize-use-trailing-return-type,
  -readability-magic-numbers,
  -cppcoreguidelines-avoid-magic-numbers
WarningsAsErrors: 'bugprone-*,performance-*'
HeaderFilterRegex: '^(src|include)/'

# .clang-format
# BasedOnStyle: Google
# IndentWidth: 4
# ColumnLimit: 100

# clang-format --dry-run --Werror src/*.cpp include/*.h   # CI: fail if unformatted
# clang-format -i src/*.cpp include/*.h                   # local: fix in place
# clang-tidy -p build src/*.cpp                           # uses build/compile_commands.json
06

C++ + PostgreSQL: parameterised queries with libpqxx

C++ services talk to PostgreSQL through libpqxx, the official C++ wrapper over the C library libpq. The pattern is the same as in any language: open a connection, start a transaction, run parameterised queries (never string-concatenated SQL), commit. RAII does the cleanup: if an exception leaves the scope before commit(), the transaction is rolled back automatically.

C++ + PostgreSQL

Query and insert inside a transaction

Dependency: libpqxx/7.10.1 from Conan (or vcpkg install libpqxx). exec_params sends $1 as a bound parameter, so a hostile email string cannot change the query. row[0].as<long>() converts a column to a C++ type and throws if it cannot. For SQL itself, see SQL Mastery.

C++
#include <iostream>
#include <pqxx/pqxx>

int main() {
    try {
        pqxx::connection conn{"postgresql://shop:secret@localhost:5432/shop"};
        pqxx::work tx{conn};

        tx.exec_params("INSERT INTO customers (email) VALUES ($1) ON CONFLICT DO NOTHING",
                       "[email protected]");

        pqxx::result rows = tx.exec_params(
            "SELECT id, email FROM customers WHERE email LIKE $1 ORDER BY id", "%@example.com");
        for (const auto& row : rows)
            std::cout << row[0].as<long>() << ' ' << row[1].as<std::string>() << '\n';

        tx.commit();                       // without this, the destructor rolls back
    } catch (const std::exception& e) {
        std::cerr << "database error: " << e.what() << '\n';
        return 1;
    }
}
07

C++ + Qt: a desktop window

Qt is the leading cross-platform C++ framework for desktop and embedded user interfaces: widgets or the declarative QML, plus networking, databases, threads and more. Its signal-and-slot mechanism connects events (a click) to handlers (a lambda). Qt powers automotive dashboards, medical devices, industrial HMIs and desktop applications, and it is a common requirement in C++ job postings outside finance and games.

C++ + Qt

A widget window with a button connected to a lambda

Build with CMake: find_package(Qt6 REQUIRED COMPONENTS Widgets), qt_standard_project_setup(), and target_link_libraries(app PRIVATE Qt6::Widgets). connect ties the button's clicked signal to a lambda. Widgets given a parent are deleted by that parent, which is Qt's ownership model; only the top-level window lives on the stack.

C++
#include <QApplication>
#include <QLabel>
#include <QPushButton>
#include <QVBoxLayout>
#include <QWidget>

int main(int argc, char* argv[]) {
    QApplication app(argc, argv);

    QWidget window;
    window.setWindowTitle("Counter");
    auto* layout = new QVBoxLayout(&window);   // owned by window
    auto* label = new QLabel("Clicked 0 times");
    auto* button = new QPushButton("Click me");
    layout->addWidget(label);
    layout->addWidget(button);

    int clicks = 0;
    QObject::connect(button, &QPushButton::clicked, [&] {
        label->setText(QString("Clicked %1 times").arg(++clicks));
    });

    window.resize(260, 120);
    window.show();
    return app.exec();                          // event loop
}
08

C++ + Docker: build once, ship a small image

A multi-stage Dockerfile gives every developer and CI runner the same compiler and libraries, and keeps the final image small: build in an image with the toolchain, then copy only the compiled binary into a slim runtime image. For services, this is also where you choose between dynamic linking (smaller binary, needs the runtime libraries) and static linking (one self-contained file).

C++ + Docker

A multi-stage Dockerfile for a CMake project

The build stage installs a compiler, CMake and Ninja; --config Release with -O2 is what ships. The runtime stage is a plain Debian slim image with only the C++ runtime library the binary needs. Running as a non-root user is a cheap security win. The resulting image is tens of megabytes instead of the gigabyte-plus build image.

dockerfile
# build stage
FROM debian:bookworm AS build
RUN apt-get update && apt-get install -y --no-install-recommends g++ cmake ninja-build \
    && rm -rf /var/lib/apt/lists/*
WORKDIR /src
COPY . .
RUN cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release \
    && cmake --build build --target shop

# runtime stage
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y --no-install-recommends libstdc++6 \
    && rm -rf /var/lib/apt/lists/* && useradd --create-home app
COPY --from=build /src/build/shop /usr/local/bin/shop
USER app
ENTRYPOINT ["shop"]

# docker build -t shop:1.0 .
# docker run --rm shop:1.0
09

C++ + GitHub Actions: CI with a compiler matrix and sanitizers

C++ CI earns its keep by building on more than one compiler and platform (code that compiles with clang may not with MSVC) and by running the tests under sanitizers, which catch undefined behaviour that normal test runs miss. A matrix runs the same steps across operating systems and configurations in parallel.

C++ + GitHub

Build and test on Linux, macOS and Windows, plus an ASan job

The matrix job builds and tests on three platforms. The sanitizer job adds -fsanitize=address,undefined through CMAKE_CXX_FLAGS, so any memory error in a test fails the build. A formatting check with clang-format --dry-run --Werror and a clang-tidy step can be added as further jobs.

yaml
# .github/workflows/ci.yml
name: ci
on: [push, pull_request]

jobs:
  build:
    strategy:
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - name: Configure
        run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
      - name: Build
        run: cmake --build build --config Release
      - name: Test
        run: ctest --test-dir build -C Release --output-on-failure

  sanitizers:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_FLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer"
      - run: cmake --build build
      - run: ctest --test-dir build --output-on-failure
10

C++ + Linux: profiling and memory checking

Performance is usually why a project chose C++, so measuring it is part of the job. On Linux, perf samples where CPU time goes with almost no overhead, Valgrind finds memory errors and leaks in an unmodified binary (slowly), and heaptrack shows who allocates. Always measure an optimised build (-O2 -g): a debug build's profile is misleading.

C++ + Linux

perf, Valgrind and a timing harness

perf record -g captures call stacks; perf report lists the hottest functions. valgrind --leak-check=full reports every leaked block with the stack that allocated it. For micro-benchmarks, Google Benchmark handles warm-up and statistics; a quick std::chrono timer around a loop is fine for a first look, but always compare two versions on the same machine and input.

bash
# build optimised with symbols
cmake -S . -B build-prof -DCMAKE_BUILD_TYPE=RelWithDebInfo && cmake --build build-prof

# where does the CPU time go?
perf record -g ./build-prof/shop --input big.csv
perf report --stdio | head -40

# memory errors and leaks without recompiling
valgrind --leak-check=full --show-leak-kinds=definite ./build-prof/shop --input small.csv

# who allocates the most?
heaptrack ./build-prof/shop --input big.csv
CMake target
A library or executable declared with add_library or add_executable, with its sources, include paths, standard and links attached as properties.
PUBLIC / PRIVATE
Whether a target property propagates to targets that link it (PUBLIC) or stays internal (PRIVATE).
compile_commands.json
A file listing the exact compile command for each source file; used by clangd, clang-tidy and IDEs.
Package manager
Conan or vcpkg: downloads and builds third-party libraries for your compiler and settings.
Static analysis
Finding bugs without running the code, e.g. clang-tidy checks.
Multi-stage build
A Dockerfile that compiles in one image and copies only the binary into a smaller runtime image.
Quick check

In CMake, why write target_link_libraries(app PRIVATE pricing) rather than adding src/pricing.cpp to the app's sources?

Frequently asked questions

What build system do C++ projects use?
CMake is the de facto standard: it generates builds for Ninja, Make, Visual Studio and Xcode from one CMakeLists.txt. Some large organisations use Bazel or Meson, and Visual Studio solutions are common in Windows-only code, but CMake is the one to learn first.
How do I add libraries to a C++ project?
Use a package manager: Conan or vcpkg download and build libraries such as fmt, nlohmann_json, Boost or GoogleTest for your compiler, and integrate with CMake so find_package and target_link_libraries work. For very small libraries, CMake's FetchContent can also download and build a dependency during configuration.
Which C++ test framework should I use?
GoogleTest is the most widely used and the safest choice for a resume; Catch2 and doctest are lighter alternatives with simpler syntax. All three integrate with CMake and ctest, and the testing habits (small pure functions, clear assertions, tests in CI) matter more than the framework.

Finish the C++ handbook, then get hired

Sit the exam for your certificate, run your resume through the ATS checker, and see the jobs that ask for exactly this.

Check my resume
Found this course useful? Share it.
ShareXLinkedIn

Comments

0

Join the conversation. Sign in to leave a comment — we'd love to hear your thoughts.