Mateusz Patyk avatar
building things that fly (and compile fast)

MateuszPatyk

C++ enthusiast with a strong interest in cross-platform development, compilers, build systems and tooling.

Kraków, PL · 35 repos

stack

C++ CMake / Meson Qt / QML Python MATLAB Arduino / Embedded WebAssembly LTO / PGO Sanitizers Clang-Tidy / Cppcheck

selected projects

view all 35 repos →

writing

LTO + PGO: the free performance nobody enables

Link Time Optimization and Profile-Guided Optimization solve two different problems that compound nicely. LTO defers inlining and dead-code elimination until link time, when the compiler can finally see across translation units — a call from a.cpp into a function defined in b.cpp can now be inlined, something a normal per-TU build never gets a chance to do. PGO goes further: it doesn't guess which branches are hot, it measures them. You build an instrumented binary, run it against a representative workload, and feed the resulting profile back into a second, real build — so the compiler lays out hot paths contiguously and stops wasting icache on your error-handling branches.

Wiring it into CMake is a few flags, not a rewrite:

add_compile_options(-flto=auto)
add_link_options(-flto=auto)

# pass 1 — instrumented
add_compile_options(-fprofile-generate)
add_link_options(-fprofile-generate)
# ...run the binary against real workloads...

# pass 2 — optimized, using the collected profile
add_compile_options(-fprofile-use -fprofile-correction)

The catch is entirely about the training run. A profile built from unit tests optimizes for unit tests, not production traffic — if your workload doesn't represent what actually ships, PGO will happily make the wrong path fast. Budget for the extra build time too: two full compiles plus a link step that's noticeably slower under LTO. Worth it for a hot inner loop shipped to millions of invocations; probably not worth it for a CLI tool run twice a day.

Cppcheck + Clang-Tidy: static analysis that lives in your build, not your CI logs

Cppcheck and Clang-Tidy overlap less than people assume. Cppcheck is fast, low false-positive, and good at the things a compiler genuinely can't see — null derefs, out-of-bounds access, resource leaks — without needing a full, correctly-configured build. Clang-Tidy needs your real compile flags (via compile_commands.json) but pays for it with much deeper checks: modernization suggestions, bug-prone patterns, and clang-static-analyzer-grade dataflow checks. Running both catches more than either alone, for the cost of two tool invocations.

CMake has first-class support for wiring both in directly as part of the build, not a separate CI step someone has to remember to run:

set(CMAKE_CXX_CPPCHECK "cppcheck;--enable=warning,performance")
set(CMAKE_CXX_CLANG_TIDY "clang-tidy;-checks=modernize-*,bugprone-*")

Set those before you declare targets and every compile invocation runs the checkers alongside the compiler, warnings landing in the same build log as everything else. The real work is tuning the checks list — both tools are noisy on defaults, and a wall of unfiltered warnings gets ignored within a week. Start narrow (a handful of checks that catch real bugs you've actually hit), get the team to trust the signal, then widen it. A suppressions file for pre-existing violations lets you turn this on for legacy code without a giant fix-everything PR blocking adoption.

Sanitizers: the tests your unit tests don't run

A green test suite proves your code does what you told it to on the inputs you thought to try. It says nothing about the buffer you wrote one byte past, the signed overflow that's technically undefined behavior, or the data race that only shows up under load. Sanitizers instrument the binary itself to catch that class of bug — the ones that pass every assertion and still corrupt memory.

AddressSanitizer catches out-of-bounds access and use-after-free. UndefinedBehaviorSanitizer catches integer overflow, misaligned pointers, invalid enum values — the stuff the standard says is UB and the compiler is free to miscompile around. ThreadSanitizer catches data races. All three are a compile flag away:

add_compile_options(-fsanitize=address,undefined -fno-omit-frame-pointer)
add_link_options(-fsanitize=address,undefined)

They don't compose freely — ASan and TSan can't run in the same binary — so the practical setup is a separate CMake build type per sanitizer, run as its own CI job against the same test suite you already have. No new tests to write; existing coverage just starts catching a category of bug it was blind to. Runtime overhead is real (ASan roughly 2x, TSan 5–15x), which is exactly why these run in CI and not in the production binary — but as a default job on every PR, sanitizer builds tend to catch memory bugs unit tests structurally cannot see.

CRTP: how the standard library gets polymorphism without a vtable

TL;DR: a class template that inherits from a base parameterized on itself gets compile-time (static) polymorphism — same interface-sharing benefit as virtual functions, zero vtable, zero indirect call, full inlining. Cost: no runtime type erasure, so no heterogeneous containers of the base type, and every distinct derived type is its own base instantiation. std::enable_shared_from_this is CRTP, already living in your codebase.

The cost virtual functions don't advertise

A class with any virtual function carries a vtable pointer — one word per object, not per class. Every call through a base pointer is an indirect call: load the vptr, load the slot, jump. The compiler can't inline it (usually — devirtualization exists but is fragile across translation units), can't reorder around it as freely, and the indirection is a branch target the CPU has to predict. None of this matters for a function called a few times a second. It matters a great deal in a loop that calls it ten million times, which is exactly the shape of code in a physics step, a parser's inner dispatch, or a hot serialization path.

CRTP exists to buy the same "shared interface, per-type behavior" shape virtual functions give you, resolved entirely at compile time.

The mechanism

The base class is a template. The derived class passes itself as the template argument:

template <typename Derived>
class Shape {
public:
    double area() const {
        return static_cast<const Derived*>(this)->area_impl();
    }
};

class Circle : public Shape<Circle> {
public:
    explicit Circle(double r) : r_(r) {}
    double area_impl() const { return 3.14159265 * r_ * r_; }
private:
    double r_;
};

Nothing here is dynamic. Shape<Circle>::area() is a distinct, fully-typed function — the compiler knows at the call site that this is a Circle, so the static_cast is a no-op and area_impl() is an ordinary, inlinable function call. Compile Shape<Circle>::area() once, specialized for Circle; compile a different one, specialized for Square, if you have one. There is no shared, single, runtime-dispatched area() — there never was one to share.

It's already in the standard library

std::enable_shared_from_this<T> is the canonical example living in every C++ codebase that touches shared_ptr. You write class Widget : public std::enable_shared_from_this<Widget>, and the base — parameterized on your exact derived type — can hand back a correctly-typed shared_ptr<Widget> from inside a member function via shared_from_this(), without ever needing a virtual function or knowing about Widget at the point the base was written. That's the whole pattern, already load-bearing in code you've probably called without noticing it was CRTP.

The pattern that actually earns its keep: mixins

The more common real use isn't performance-critical shape hierarchies — it's giving a type a family of related operations for free, from one written definition. The classic example is comparison operators: define operator== once on your type, get !=, <, >, <=, >= derived from it and one more, with no runtime cost and no repetition per type:

template <typename Derived>
class EqualityComparable {
public:
    friend bool operator!=(const Derived& a, const Derived& b) {
        return !(a == b);
    }
};

class Point : public EqualityComparable<Point> {
public:
    int x, y;
    friend bool operator==(const Point& a, const Point& b) {
        return a.x == b.x && a.y == b.y;
    }
};
// Point now has operator!= for free, generated at compile time,
// with zero runtime indirection and no vtable added to Point.

Stack a few of these — EqualityComparable, Printable, Hashable — and you get a la carte, compile-time-composed capability without multiple inheritance from unrelated abstract interfaces and without a single virtual call in sight. This is the same idea C++20 concepts and things like <=> partially subsume for comparisons specifically, but the general mixin technique still applies to anything you want to bolt onto a type mechanically.

Where it bites you

When to actually reach for it

Use it when you know at compile time which concrete type you're working with and the interface-sharing is the only thing you wanted from inheritance — mixins, static-dispatch hot loops, library code like enable_shared_from_this that needs to hand back a correctly-typed handle. Don't use it as a default replacement for virtual functions; reach for CRTP when profiling (or the shape of the problem) tells you the indirection actually costs something, not preemptively. Virtual dispatch exists because sometimes you genuinely don't know the concrete type until runtime — in that case CRTP can't help you, because "compile-time polymorphism" is a contradiction in terms for a decision that's inherently made at runtime.