Aug 4, 2026

[CppNow][summary] Lock-free Programming is Dead - Long Live Lock-free Programming! - Fedor G Pikus - C++Now 2026

Deep Dive Summary: Lock-free Programming is Dead - Long Live Lock-free Programming!

Speaker: Fedor Pikus
Conference: C++Now 2026
Topic: Microarchitecture, Concurrency, Lock-Free Data Structures, and Low-Level C++ Optimization


Executive Overview

For decades, modern C++ concurrent programming followed a simple, widely accepted rule: Lock-free algorithms should be used for high-contention, performance-critical paths, while locks should be reserved for low-contention or non-critical code.

In this landmark C++Now 2026 presentation, Fedor Pikus demonstrates that advancements in modern CPU microarchitecture (x86-64 Intel/AMD, ARM Grace/Graviton, Apple Silicon) have completely inverted this paradigm:

  1. At High Contention: Properly engineered spin locks outperform lock-free (compare_exchange loops) and wait-free (fetch_add) atomic algorithms.
  2. At Low Contention: Taking a spin lock—even once every 100 iterations—severely degrades surrounding CPU out-of-order execution performance ("pipeline poisoning"), whereas atomics excel without stalling execution pipelines.
  3. The Optimal Modern Paradigm: High-performance concurrent data structures must combine both techniques—utilizing custom spin locks for high-contention index allocation/state transitions and atomic handoffs for low-contention payload access.

Progress Guarantees & Theoretical Definitions

Before diving into hardware microarchitecture, Pikus clarifies the classical computer science definitions of thread progress:

Guarantee CS Definition Typical C++ Primitive Hardware Reality
Wait-Free Every thread completes its operation in a bounded number of algorithmic steps. std::atomic::fetch_add Not constant time. Hardware cache-line invalidation forces sequential memory execution.
Lock-Free At least one thread makes progress overall; losers retry in a loop. std::atomic::compare_exchange_weak / strong High contention causes severe CAS-retry loops and cache coherence thrashing.
Lock-Based One thread holds exclusive access; all other contending threads wait/block. std::mutex, Custom Spin Locks OS mutexes incur context switch overhead, but well-tuned spin locks eliminate CAS retry overhead.

Key Distinction: Computer science definitions measure algorithmic steps, not CPU clock cycles. A "wait-free" instruction executes a single instruction step, but at the hardware level, cache coherency mechanisms force memory accesses to serialize, causing hardware-level waiting.


Microarchitectural Deep Dive: Why Atomics Fail Under High Contention

1. Cache Coherency and Read-For-Ownership (RFO)

Modifying any atomic variable requires exclusive access to its underlying 64-byte cache line:

  • To write to a cache line, a CPU core must issue a Read-For-Ownership (RFO) request across the interconnect.
  • The requesting core must wait for all other cores holding that cache line in a Shared (S) state to invalidate their local L1/L2 caches and send back an Acknowledgment (ACK) signal.
  • Under heavy multi-threaded contention, cores spend the majority of their clock cycles waiting for speed-of-light electrical signal propagation across the chip to complete RFO invalidation handshakes.

2. True Sharing vs. False Sharing

Experimentation proves that false sharing (multiple independent atomics residing on the same 64-byte cache line) and true sharing (all threads hammering the exact same atomic variable) suffer from the exact same latency penalty at high thread counts. The hardware bottleneck is the cache line granularity, not the specific integer being modified.


Engineering a High-Performance Spin Lock

To outperform atomics at high contention, a spin lock must be engineered specifically to respect hardware cache coherency protocols.

Critical Design Features:

  1. Pre-Read Probe (Test-and-Test-and-Set):
    • Before attempting an expensive atomic operation (atomic_exchange), the thread performs a relaxed, read-only load of the lock flag.
    • Reading allows the core to acquire the cache line in a Shared (S) state without revoking exclusive access from the thread currently holding the lock.
  2. Pre-Read Retries:
    • Performing ~8 relaxed reads on x86 before attempting an atomic swap ensures that in-flight RFO signals have time to settle, preventing premature cache-line stealing.
  3. Aggressive Back-off:
    • Unlocking a spin lock requires writing 0 to memory, which itself requires an RFO request. If waiting threads continuously hammer the lock with atomic writes, they steal the cache line from the lock holder, severely delaying the unlock operation.
    • Back-off logic (or yielding) keeps waiting threads from stealing the cache line during lock release.

The Low-Contention Asymmetry: "Pipeline Poisoning"

When benchmarking code that mixes parallel payload computation (e.g., local mathematical tasks) with synchronization (e.g., updating a shared counter):

  • High Contention Domain: Spin locks deliver up to 2.5x higher overall application throughput compared to atomics.
  • Low Contention Domain: When synchronization occurs rarely (e.g., 1 out of 100 iterations), using a spin lock causes program throughput to plummet compared to atomics.
High Contention (1:1 Parallel to Shared Work):
[Spin Lock]  ========================> 2.5x Throughput vs Atomics
[Atomic CAS] ========> 1.0x

Low Contention (100:1 Parallel to Shared Work):
[Atomic CAS] ========================> 1.0x (Fast Execution)
[Spin Lock]  =====> 0.25x (Severe Pipeline Poisoning)

Hardware Profiling & Root Cause Analysis (Intel VTune / Linux Perf)

By inspecting CPU hardware performance counters (RESOURCE_STALLS.STORE_BUFFER), Pikus identified the exact microarchitectural bottleneck:

  1. Store Buffer Stalls: Spin locks force massive store buffer stalls in x86 execution pipelines. On x86 architectures, memory writes exit the store buffer in strict program retirement order.
  2. Implicit vs. Explicit Dependencies:
    • Atomics (fetch_add, CAS): Fuse control and data into a single variable. The CPU out-of-order execution engine can inspect the instruction stream and recognize explicit data dependencies, allowing non-dependent payload instructions to flow around the atomic operation.
    • Locks: Separate control (the lock flag) from data (the guarded payload). Because the CPU cannot reason through implicit memory dependencies across bidirectional acquire/release memory barriers, it cannot predict safety across the critical section. As a result, the out-of-order execution pipeline flushes and stalls until the lock and unlock operations fully retire.

Practical Application: The Hybrid MPMC Ring-Buffer Queue

To prove these findings, Fedor Pikus constructed a high-throughput Multi-Producer Multi-Consumer (MPMC) ring-buffer queue designed around hybrid synchronization:

+-----------------------------------------------------------------------+
|                         MPMC QUEUE DESIGN                             |
+-----------------------------------------------------------------------+
| High-Contention Domain  -->  Spin Lock (Guards Head / Tail Indices)  |
| Separate Cache Lines    -->  Prevents Producer/Consumer Thrashing     |
+-----------------------------------------------------------------------+
| Low-Contention Domain   -->  Atomic Slot Keys (std::atomic<Key>)    |
| Exclusive Slot Access   -->  Zero Lock Overhead for Payload Handoff   |
+-----------------------------------------------------------------------+

Performance Benchmark Results:

  • Throughput: Dramatically outperforms traditional pure lock-free MPMC queues across Intel Granite Rapids, AMD Zen 5, Nvidia Grace, and Apple Silicon.
  • Average Latency: Significantly lower mean and 95th/99th percentile latency compared to pure lock-free implementations.
  • Tail Latency Exception (99.99%+): Pure lock-free queues only win at extreme tail latencies where thread preemption risks affect lock-based structures.

Duration & Power Considerations: When Spin Locks Fail

While spin locks excel at short critical sections, they carry severe operational trade-offs if critical section processing times grow long:

  1. Busy-Waiting Energy Waste: A thread waiting on a spin lock burns 100% CPU core utilization, drawing maximum power, generating heat, and causing thermal throttling.
  2. Thread Preemption Disaster: If the thread holding a spin lock is preempted by the OS scheduler (context-switched out), all contending threads will actively spin for their entire OS time slice doing zero productive work while waiting for the lock holder to be rescheduled.
  3. The Industrial Solution (Adaptive / Two-Phase Locks): Production systems (e.g., database kernels, runtime engines) utilize adaptive locks:
    • Phase 1: Spin for a short, bounded duration (~50–100 iterations using CPU pause hints like _mm_pause() or YIELD).
    • Phase 2: If the lock remains unacquired, yield execution to the OS kernel via a futex sleep system call.

Architectural Anomalies & Microarchitectural Surprises

  1. Cache-Line Separation of Lock and Payload: Placing the spin lock flag and the guarded data variable on different cache lines improves high-contention throughput. It prevents waiting threads (doing relaxed pre-reads on the lock flag) from invalidating the lock holder's cache line while it modifies the payload variable.
  2. AMD Zen 4 / Zen 5 Near-Memory Atomics: AMD Zen 4 introduced hardware execution ALUs directly inside the L3 memory controller. If a core attempts a fetch_add without owning the cache line, it offloads the operation directly to the L3 controller, bypassing L1/L2 cache-line invalidation cycles.
  3. Apple Silicon CAS Back-off: Apple M-series chips use a power-efficient, high-latency directory interconnect. Implementing explicit back-off inside a compare_exchange loop on Apple Silicon improves throughput by 10x, elevating CAS performance close to spin lock levels.

Summary Principles for Modern C++ Concurrency

  1. High Contention: Abandon pure lock-free CAS loops. Use properly engineered spin locks featuring relaxed pre-read probes, iteration limits, and back-off logic.
  2. Low Contention: Avoid locks completely. Use atomic primitives (std::atomic) to prevent CPU store buffer flushes and out-of-order pipeline stalls.
  3. Hybrid Architecture: Structure concurrent data structures to use spin locks for high-contention indexing (e.g., ring-buffer head/tail allocation) and atomic state flags for low-contention data transfer.
  4. Bounded Work: Keep spin lock critical sections strictly bounded to a few dozen nanoseconds; fall back to adaptive OS-backed locks (futex) if processing times can exceed thread time-slices.

#include <atomic>
#include <chrono>
#include <new>
#include <thread>

#if defined(__x86_64__) || defined(_M_X64)
#include <emmintrin.h> // For _mm_pause()
#endif

// Align to prevent false sharing with adjacent cache lines
class alignas(std::hardware_destructive_interference_size) AdaptiveSpinLock {
public:
    AdaptiveSpinLock() noexcept = default;

    // Non-copyable, non-movable
    AdaptiveSpinLock(const AdaptiveSpinLock&) = delete;
    AdaptiveSpinLock& operator=(const AdaptiveSpinLock&) = delete;

    void lock() noexcept {
        // Phase 1: Fast path (Uncontended)
        if (!state_.exchange(true, std::memory_order_acquire)) {
            return;
        }

        // Phase 2: Active Spin Loop with Pre-Read (Test-and-Test-and-Set)
        int spin_count = 0;
        constexpr int MAX_SPINS = 64; // Max active CPU spinning iterations

        while (true) {
            // Pre-read probe: Read in a relaxed loop to stay in 'Shared' (S) cache state.
            // Avoids sending RFO (Read-For-Ownership) signals while waiting.
            while (state_.load(std::memory_order_relaxed)) {
                pause_cpu(spin_count);

                // Phase 3: Adaptive Fallback to OS-backed Sleeping
                // If spinning takes too long, stop burning CPU/power and park the thread.
                if (++spin_count > MAX_SPINS) {
                    // C++20/23 std::atomic::wait uses OS futex/synch primitives natively
                    state_.wait(true, std::memory_order_relaxed);
                }
            }

            // Attempt to acquire lock via atomic exchange once pre-read detects key is free
            if (!state_.exchange(true, std::memory_order_acquire)) {
                return; // Lock acquired successfully
            }
        }
    }

    bool try_lock() noexcept {
        // First check relaxed state to avoid cache invalidation on failure
        if (state_.load(std::memory_order_relaxed)) {
            return false;
        }
        return !state_.exchange(true, std::memory_order_acquire);
    }

    void unlock() noexcept {
        // Unconditionally release lock
        state_.store(false, std::memory_order_release);

        // Notify any waiting thread that was parked in state_.wait()
        state_.notify_one();
    }

private:
    // Flushes execution pipeline & saves power during short spinning
    static void pause_cpu(int spin_count) noexcept {
        if (spin_count < 16) {
#if defined(__x86_64__) || defined(_M_X64)
            _mm_pause(); // Informs x86 pipeline of spin loop; reduces power & store buffer pressure
#elif defined(__aarch64__)
            asm volatile("yield" ::: "memory"); // ARM yield hint
#endif
        } else {
            // Hint to OS scheduler to run other threads on the same core/time-slice
            std::this_thread::yield();
        }
    }

    // False = Unlocked, True = Locked
    std::atomic<bool> state_{false};
};

Aug 3, 2026

[benchmark] how `void DoNotOptimize(Tp const& value)` works under the hood

Reference:
How does Google's `DoNotOptimize()` function enforce statement ordering
[gcc] Constraint Modifier Characters
[google menchmark] void DoNotOptimize()

code by example:
https://godbolt.org/z/EzTr4afEM


How Google's DoNotOptimize Enforces Statement Ordering

This document summarizes the StackOverflow thread How does Google's DoNotOptimize function enforce statement ordering?, focusing in detail on the accepted answer by Peter Cordes (answer 69288151).


🧩 Summary of Ordering Mechanics

Mechanism Targeted Ordering Implementation Compiler's Interpretation
Opaque "memory" Clobber Ordering between DoNotOptimize() and external calls (e.g., time()) asm volatile("" : ... : : "memory") Treated as an opaque, non-inline function call that can read/write any globally-reachable memory, stopping compile-time reordering.
Data Dependency ("+r,m") Ordering of the benchmarked computation itself (input -> output) Input/Output constraints on the variable Forces the compiler to materialize values, forget compile-time constants (defeating constant propagation), and execute the code.

1. The "memory" Clobber acts as an Opaque Function Call

The inline assembly in DoNotOptimize() contains a "memory" clobber.

  • Opaque External Call Simulation: To the compiler's optimizer, a "memory" clobber is treated just like an external, non-inline function call (whose source code is hidden). The compiler must assume this block can read or write any globally-reachable object.
  • Preventing Reordering with time(): Since time() itself is not defined inline in C++ headers, the compiler cannot see its definition and treats it as an opaque external function. Because both time() and DoNotOptimize() (due to its "memory" clobber) are treated as opaque operations that could modify global state, the compiler is strictly forbidden from reordering them. It is the exact same reason a compiler will not reorder two sequential calls to unknown functions like puts("first"); puts("second");.

2. The "+r,m" Constraint & Data Dependency

While the "memory" clobber handles ordering against other opaque calls, the actual benchmark computation (e.g., run_bench()) is ordered using strict C++ data dependencies.

  • Forcing Materialization: The "+r,m" constraint declares a read/write operand. This forces the compiler to materialize the target variable's value in a register or memory, meaning the variable must actually be computed up to that point.
  • Defeating Constant Propagation: Because of the + (read/write) modifier, the compiler must assume that the assembly block could arbitrarily change the variable's value. Consequently, the compiler has to forget all compile-time knowledge of that variable (such as it being a constant, positive, etc.). This prevents the optimizer from constant-folding or optimizing away the benchmarked code entirely.
  • Enforcing the Pipeline:
    1. DoNotOptimize(input) forces the input to be computed and clears compiler assumptions.
    2. The compiler runs the benchmark calculation, because the output depends on this newly materialized input.
    3. DoNotOptimize(output) forces the output of the benchmark to be materialized in memory or a register, ensuring the compiler doesn't optimize away the entire benchmark calculation as an "unused result".

3. Key Misconceptions Corrected by the Answer

  • Reference Arguments and the Stack: The original poster suspected that reference arguments force variables onto the stack (since you cannot have a pointer to a register). However, because DoNotOptimize() is forced to inline (always_inline), the by-reference argument is optimized away entirely. The assembly operates directly on the underlying C++ variable.
  • Register vs. Memory Choice: The compiler can always choose to load a variable's value into a register before the asm statement and store it back afterward, even with the "m" constraint option.
  • Clang Workaround: The use of "r,m" (instead of "rm") specifically bypasses a Clang missed-optimization bug where Clang aggressively defaults to memory spilling for "rm" constraints even when a register is readily available.

[C++26] Reflection minute


#include <iostream>

// 1. Define the master list of colors (X Macro)
// This is the single source of truth. Adding a color here 
// automatically updates both the enum and the string converter below.
#define COLORS \
    FUNC(Red)  \
    FUNC(Blue) \
    FUNC(Green)

// 2. Generate the Enum Class
enum class Color {
#define FUNC(name) name,
    COLORS
#undef FUNC
};

// 3. Generate a Helper Function to convert Enum to String using '#' (stringification)
const char* ColorToString(Color color) {
    switch (color) {
#define FUNC(name) case Color::name: return #name;
        COLORS
#undef FUNC
        default: return "Unknown";
    }
}

int main() {
    // Instantiate color variables
    Color r = Color::Red;
    Color b = Color::Blue;
    Color g = Color::Green;

    // Output and verify the mapping works correctly
    std::cout << "Color r is: " << ColorToString(r) << " (Enum value: " << static_cast<int>(r) << ")\n";
    std::cout << "Color b is: " << ColorToString(b) << " (Enum value: " << static_cast<int>(b) << ")\n";
    std::cout << "Color g is: " << ColorToString(g) << " (Enum value: " << static_cast<int>(g) << ")\n";

    return 0;
}

Aug 2, 2026

[cmake] phases and generator expressions

Demystifying Modern CMake: Interface Libraries, Generator Expressions, and Build Phases

Category: C++ & Build Systems
Reading Time: 8 min read


If you've been working with C++ in recent years, you've likely encountered CMake code snippets like this header-only library setup:

add_library(vactor INTERFACE)
add_library(vactor::vactor ALIAS vactor)

target_include_directories(vactor INTERFACE
    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/..>
    $<INSTALL_INTERFACE:include>
)

While this looks short, it packs in three critical Modern CMake concepts: Interface targets, Generator Expressions, and explicit separation of Build vs. Install environments. Let's break down how this works under the hood.


1. Header-Only Libraries with INTERFACE

In traditional CMake, every target compiled source files into .a, .so, or .lib binaries. But header-only C++ libraries don't generate binaries.

By specifying INTERFACE in add_library(vactor INTERFACE), you tell CMake:

"This library has no binary files to compile directly. It only acts as a container for properties (include directories, compile flags, options) that downstream targets will inherit."

How does CMake know which header file to compile?
It doesn't—and doesn't need to! CMake doesn't pass header files directly to compilers. Instead, it passes directory flags (like -I /path/to/headers) to the compiler. The compiler's C++ preprocessor then finds #include <vactor/vactor.hpp> when parsing code.


2. Why the ALIAS Pattern?

Creating an alias target like add_library(vactor::vactor ALIAS vactor) is a modern best practice:

  • Namespaced Uniformity: When installing libraries and importing them via find_package(), CMake targets are usually namespaced (Package::Target). Using an ALIAS ensures internal subprojects use the exact same target name as external consumers.
  • Early Error Catching: If you misspell an un-namespaced target in target_link_libraries(), CMake might assume it's a raw system library name and defer the error to link-time. Namespaced targets with :: trigger immediate CMake configure-time errors.

3. Decoding Generator Expressions ($<...>)

The syntax $<KEYWORD:VALUE> is a CMake Generator Expression. Standard variables like ${MY_VAR} are evaluated sequentially when CMake reads the script. Generator expressions, however, are deferred evaluation rules.

In our snippet:

  • $<BUILD_INTERFACE:...>: Evaluates to the given path ONLY when building within the local source tree or as a subproject via add_subdirectory().
  • $<INSTALL_INTERFACE:...>: Evaluates to the given path ONLY when exported and imported via find_package().

Without this separation, absolute build directory paths from your local machine would leak into installed config files, breaking the build on end-user machines!


4. The Four Phases of CMake

To fully grasp generator expressions, it helps to understand that CMake execution spans four separate execution phases:

  1. Configure Phase (cmake -S . -B build)
    CMake reads CMakeLists.txt line-by-line, runs system checks, evaluates standard variables (${VAR}) and if() logic, constructing the internal target graph.
  2. Generate Phase (End of cmake -S . -B build)
    CMake evaluates all $<...> generator expressions using the full target graph and writes native build files (Ninja rules, Makefiles, or MSBuild .vcxproj files).
  3. Build Phase (cmake --build build)
    The underlying build tool (Ninja, Make, MSVC) executes the compiler (g++/clang++) using the generated command-line flags.
  4. Runtime Phase
    The user runs the compiled executable. CMake is no longer active.

Summary: Generator Expression Quick Reference

Generator Expression Description / Usage
$<CONFIG:Debug> Evaluates to 1 if current target configuration is Debug, else 0.
$<COMPILE_LANGUAGE:CXX> Evaluates to 1 if current file is compiled with C++ compiler.
$<TARGET_FILE:target> Returns full output path to the target binary file.
$<CXX_COMPILER_ID:GNU> Evaluates to 1 if using GCC compiler.

By leveraging INTERFACE targets and generator expressions, you ensure your C++ libraries stay portable, clean, and easy to consume across both local and installed environments!


Comprehensive Guide to CMake Generator Expressions ($<...>)

CMake Generator Expressions are evaluated during build system generation (the Generate Phase) [cite: 1.1.4, 1.2.1]. They allow conditional compilation, string transformation, target queries, and cross-platform flag customization [cite: 1.1.2, 1.2.5].


1. Conditional & Logical Expressions

Boolean generator expressions evaluate to 1 (true) or 0 (false) [cite: 1.2.5]. They are most commonly used inside conditional output blocks like $<$<CONDITION>:true_string> [cite: 1.1.4, 1.2.5].

Conditional Output

Expression Description
$<$<CONDITION>:string> Evaluates to string if CONDITION is 1, otherwise evaluates to an empty string [cite: 1.1.4, 1.2.5].
$<IF:condition,true_val,false_val> Evaluates to true_val if condition is 1, or false_val if 0 [cite: 1.1.4].

Logical Operators

Expression Description
$<BOOL:string> Converts string to 0 or 1 using standard CMake boolean logic (e.g., OFF, FALSE, 0, empty evaluate to 0) [cite: 1.1.1, 1.2.5].
$<AND:cond1,cond2,...> Evaluates to 1 if all conditions evaluate to 1, otherwise 0 [cite: 1.2.5].
$<OR:cond1,cond2,...> Evaluates to 1 if at least one condition evaluates to 1, otherwise 0 [cite: 1.2.5].
$<NOT:condition> Evaluates to 0 if condition is 1, otherwise 1 [cite: 1.2.5].

2. Comparisons (Strings, Numbers, Versions)

String & List Comparisons

Expression Description
$<STREQUAL:str1,str2> 1 if str1 and str2 are equal (case-sensitive), else 0 [cite: 1.2.5].
$<IN_LIST:item,list> 1 if item is present in the semicolon-separated list, else 0 [cite: 1.2.5].

Numeric Comparisons

Expression Description
$<EQUAL:val1,val2> 1 if numbers val1 and val2 are equal, else 0 [cite: 1.2.5].
$<LESS:val1,val2> 1 if val1 is strictly less than val2, else 0.
$<GREATER:val1,val2> 1 if val1 is strictly greater than val2, else 0.
$<LESS_EQUAL:val1,val2> 1 if val1 is less than or equal to val2, else 0.
$<GREATER_EQUAL:val1,val2> 1 if val1 is greater than or equal to val2, else 0.

Version Comparisons

Expression Description
$<VERSION_EQUAL:v1,v2> 1 if version v1 equals v2, else 0 [cite: 1.2.5].
$<VERSION_LESS:v1,v2> 1 if version v1 is less than v2, else 0 [cite: 1.2.5].
$<VERSION_GREATER:v1,v2> 1 if version v1 is greater than v2, else 0 [cite: 1.2.5].
$<VERSION_LESS_EQUAL:v1,v2> 1 if v1 is less than or equal to v2, else 0 [cite: 1.2.5].
$<VERSION_GREATER_EQUAL:v1,v2> 1 if v1 is greater than or equal to v2, else 0 [cite: 1.2.5].

3. Platform, Compiler & Language Queries

These queries allow writing cross-platform CMake configurations that adjust compiler flags and build settings automatically [cite: 1.2.5].

Compiler & Platform Identification

Expression Description
$<CONFIG:config_name> 1 if current build configuration matches config_name (e.g., Debug, Release) [cite: 1.2.5].
$<PLATFORM_ID:id_list> 1 if the host/target platform matches any ID in the list (e.g., Linux, Windows, Darwin) [cite: 1.2.5].
$<C_COMPILER_ID:id_list> 1 if the C compiler matches an ID in the list (e.g., GNU, Clang, MSVC) [cite: 1.2.5].
$<CXX_COMPILER_ID:id_list> 1 if the C++ compiler matches an ID in the list [cite: 1.2.5].
$<C_COMPILER_VERSION:ver> 1 if C compiler version matches ver [cite: 1.2.5].
$<CXX_COMPILER_VERSION:ver> 1 if C++ compiler version matches ver [cite: 1.2.5].
$<COMPILE_LANGUAGE:lang> 1 if the source file currently being compiled uses language lang (e.g., C, CXX, CUDA) [cite: 1.2.1, 1.2.3].
$<COMPILE_LANG_AND_ID:lang,ids> 1 if language matches lang AND compiler ID matches ids.
$<COMPILE_FEATURES:features> 1 if all specified compile features are available for the target [cite: 1.2.2].

4. Target & Artifact Queries

These expressions extract build artifact metadata, output filenames, and locations dynamically across all platforms [cite: 1.1.3].

File Paths & Artifact Names

Expression Description
$<TARGET_FILE:target> Full path to the main binary file produced by target (e.g., /usr/lib/libfoo.so or C:/app.exe) [cite: 1.1.3, 1.2.1].
$<TARGET_FILE_NAME:target> Filename of the target binary file (e.g., app.exe).
$<TARGET_FILE_DIR:target> Directory containing the target binary file [cite: 1.2.1].
$<TARGET_LINKER_FILE:target> Full path to the file used for linking against target (.lib, .a, .so) [cite: 1.2.1].
$<TARGET_LINKER_FILE_NAME:target> Filename of the linker file.
$<TARGET_LINKER_FILE_DIR:target> Directory containing the linker file.
$<TARGET_SONAME_FILE:target> Full path to the file with soname (.so.1) [cite: 1.2.1].
$<TARGET_PDB_FILE:target> Full path to the Visual Studio .pdb debug symbols file.

Property Queries & Existence

Expression Description
$<TARGET_PROPERTY:target,prop> Value of property prop on target [cite: 1.2.1].
$<TARGET_PROPERTY:prop> Value of property prop on the target being evaluated [cite: 1.2.1].
$<TARGET_NAME_IF_EXISTS:target> Returns target if target exists, else empty string.
$<TARGET_EXISTS:target> 1 if target exists, else 0.
$<TARGET_GENEX_EVAL:target,expr> Evaluates expr in the context of target [cite: 1.2.1].

5. String & List Manipulations

String Transformations

Expression Description
$<LOWER_CASE:string> Converts string to lowercase [cite: 1.1.1].
$<UPPER_CASE:string> Converts string to uppercase [cite: 1.1.1].
$<MAKE_C_IDENTIFIER:string> Converts string into a valid C identifier (replaces non-alphanumeric chars with _).

List Transformations & Operations

Expression Description
$<JOIN:list,glue> Joins elements in list with the delimiter string glue [cite: 1.2.1].
$<REMOVE_DUPLICATES:list> Removes duplicate entries from list.
$<FILTER:list,operator,regex> Filters list entries using an INCLUDE or EXCLUDE regex operator.
$<LIST:ACTION,list,...> Executes list sub-commands (LENGTH, GET, SUBLIST, FIND, TRANSFORM, etc.) [cite: 1.1.1].

6. Path & Interface Expressions

Path Operations

Expression Description
$<PATH:HAS_PARENT_PATH,path> 1 if path has a parent directory, else 0.
$<PATH:GET_FILENAME,path> Extracts the filename portion from path.
$<PATH:GET_PARENT_PATH,path> Extracts the parent directory path from path.
$<PATH:NORMAL_PATH,path> Returns normalized clean path (resolving . and ..).

Build & Install Interfaces

Expression Description
$<BUILD_INTERFACE:paths...> Included only when building within the source/build tree [cite: 1.1.4].
$<INSTALL_INTERFACE:paths...> Included only when consumed from an installed package via find_package() [cite: 1.1.1, 1.1.4].

7. Escaping & Special Characters

Because CMake uses characters like , and > for parsing generator expressions, escaping expressions are required when passing literal special characters inside generator expressions [cite: 1.1.2, 1.2.5].

Generator Expression Literal Evaluated String
$<ANGLE-R> >
$<COMMA> ,
$<SEMICOLON> ;
$<LOWER_THAN> <
$<GREATER_THAN> >

8. Summary Example

Combining multiple generator expressions for modern target configuration:

target_compile_options(my_app PRIVATE
    # Enable strict warnings for GCC/Clang only in Debug build
    $<$<AND:$<OR:$<CXX_COMPILER_ID:GNU>,$<CXX_COMPILER_ID:Clang>>,$<CONFIG:Debug>>:-Wall;-Wextra;-Werror>

    # Disable exceptions when compiling C++ code on MSVC
    $<$<AND:$<CXX_COMPILER_ID:MSVC>,$<COMPILE_LANGUAGE:CXX>>:/EHa->
)



Complete Reference: Built-in CMake Generator Expressions ($<...>)

CMake Generator Expressions are evaluated during build system generation (the Generate Phase). They allow conditional compilation, string transformation, target queries, and cross-platform flag customization.


1. Conditional & Logical Keys

Evaluates conditions or performs boolean operations (0 or 1).

Expression Description
$<$<CONDITION>:value> Conditional output (outputs value if CONDITION is 1).
$<IF:cond,true_val,false_val> Conditional branch selector.
$<BOOL:string> Converts string to boolean 0 or 1.
$<AND:cond1,cond2,...> Logical AND operator.
$<OR:cond1,cond2,...> Logical OR operator.
$<NOT:cond> Logical NOT operator.

2. Comparison Keys

String & List Comparisons

Expression Description
$<STREQUAL:str1,str2> Case-sensitive equality check.
$<EQUAL:str1,str2> Same as STREQUAL (string comparison).
$<IN_LIST:item,list> Checks if item exists inside a CMake list.

Numeric Comparisons

Expression Description
$<EQUAL:num1,num2> Numeric equality.
$<LESS:num1,num2> Numeric less than (<).
$<GREATER:num1,num2> Numeric greater than (>).
$<LESS_EQUAL:num1,num2> Numeric less than or equal (<=).
$<GREATER_EQUAL:num1,num2> Numeric greater than or equal (>=).

Version Comparisons

Expression Description
$<VERSION_EQUAL:v1,v2> Version string equality (=).
$<VERSION_LESS:v1,v2> Version string less than (<).
$<VERSION_GREATER:v1,v2> Version string greater than (>).
$<VERSION_LESS_EQUAL:v1,v2> Version string less than or equal (<=).
$<VERSION_GREATER_EQUAL:v1,v2> Version string greater than or equal (>=).

3. Platform, Compiler & Query Keys

Build Environment & Platform

Expression Description
$<CONFIG:cfg_list> Checks build configuration (e.g., Debug, Release).
$<PLATFORM_ID:id_list> Checks target platform ID (e.g., Linux, Windows, Darwin).
$<POLICY:policy_id> Checks CMake policy status (NEW/OLD).

Compiler Identification

Expression Description
$<C_COMPILER_ID:id_list> Checks C compiler ID (e.g., GNU, Clang, MSVC).
$<CXX_COMPILER_ID:id_list> Checks C++ compiler ID.
$<CUDA_COMPILER_ID:id_list> Checks CUDA compiler ID.
$<OBJC_COMPILER_ID:id_list> Checks Objective-C compiler ID.
$<OBJCXX_COMPILER_ID:id_list> Checks Objective-C++ compiler ID.
$<Fortran_COMPILER_ID:id_list> Checks Fortran compiler ID.
$<HIP_COMPILER_ID:id_list> Checks HIP compiler ID.
$<C_COMPILER_VERSION:ver> Checks C compiler version.
$<CXX_COMPILER_VERSION:ver> Checks C++ compiler version.
$<CUDA_COMPILER_VERSION:ver> Checks CUDA compiler version.
$<OBJC_COMPILER_VERSION:ver> Checks Objective-C compiler version.
$<OBJCXX_COMPILER_VERSION:ver> Checks Objective-C++ compiler version.
$<Fortran_COMPILER_VERSION:ver> Checks Fortran compiler version.
$<HIP_COMPILER_VERSION:ver> Checks HIP compiler version.

Language & Features

Expression Description
$<COMPILE_LANGUAGE:lang> Active source file language (e.g., C, CXX, CUDA).
$<COMPILE_LANG_AND_ID:lang,compiler_ids> Checks language AND compiler ID simultaneously.
$<COMPILE_FEATURES:features> Checks required target compile features.
$<LINK_LANGUAGE:lang> Linker language used for the binary target.
$<LINK_LANG_AND_ID:lang,compiler_ids> Checks link language AND linker/compiler ID.

4. Target & Artifact Property Keys

Paths & File Locations

Expression Description
$<TARGET_FILE:target> Full path to primary binary.
$<TARGET_FILE_NAME:target> Filename of primary binary.
$<TARGET_FILE_DIR:target> Directory containing primary binary.
$<TARGET_FILE_BASE_NAME:target> Base filename without prefix/extension.
$<TARGET_FILE_PREFIX:target> Target output prefix (e.g., lib).
$<TARGET_FILE_SUFFIX:target> Target output suffix (e.g., .so, .exe).
$<TARGET_LINKER_FILE:target> Full path to linker library file (.a, .lib, .so).
$<TARGET_LINKER_FILE_NAME:target> Filename of link library file.
$<TARGET_LINKER_FILE_DIR:target> Directory containing link library file.
$<TARGET_LINKER_FILE_BASE_NAME:target> Base filename of link library.
$<TARGET_LINKER_FILE_PREFIX:target> Linker library prefix.
$<TARGET_LINKER_FILE_SUFFIX:target> Linker library suffix.
$<TARGET_SONAME_FILE:target> Full path to binary file with soname (.so.1).
$<TARGET_SONAME_FILE_NAME:target> Filename of soname file.
$<TARGET_SONAME_FILE_DIR:target> Directory containing soname file.
$<TARGET_PDB_FILE:target> Full path to Visual Studio PDB debug file.
$<TARGET_PDB_FILE_NAME:target> Filename of MSVC PDB file.
$<TARGET_PDB_FILE_DIR:target> Directory containing MSVC PDB file.
$<TARGET_PDB_FILE_BASE_NAME:target> Base name of MSVC PDB file.
$<TARGET_BUNDLE_DIR:target> Directory of macOS Application Bundle.
$<TARGET_BUNDLE_CONTENT_DIR:target> Content directory of macOS Application Bundle (Contents/).

Target Metadata & Properties

Expression Description
$<TARGET_PROPERTY:target,prop> Retrieves property prop from target.
$<TARGET_PROPERTY:prop> Retrieves property prop from current target.
$<TARGET_EXISTS:target> Checks if a target exists (1 or 0).
$<TARGET_NAME_IF_EXISTS:target> Returns target name if exists, else empty string.
$<TARGET_GENEX_EVAL:target,expr> Evaluates expression in context of target.
$<GENEX_EVAL:expr> Evaluates expression recursively.
$<TARGET_POLICY:policy_id> Evaluates policy in target context.
$<TARGET_OBJECTS:target> List of .o/.obj object files from an OBJECT library target.
$<LINK_ONLY:target> Includes link options for target without inheriting compile interface definitions.

5. String & List Manipulation Keys

String Transformations

Expression Description
$<LOWER_CASE:str> Lowercase conversion.
$<UPPER_CASE:str> Uppercase conversion.
$<MAKE_C_IDENTIFIER:str> Converts string into C-style identifier.

List Transformations

Expression Description
$<JOIN:list,glue> Joins list with separator string.
$<REMOVE_DUPLICATES:list> Removes duplicates from list.
`$<FILTER:list,INCLUDE\ EXCLUDE,regex>` Filters list using regular expressions.
$<LIST:action,list,...> Generic list operations (LENGTH, GET, SUBLIST, FIND, TRANSFORM, etc.).

6. Path & Interface Keys

Path Operations

Expression Description
$<PATH:HAS_PARENT_PATH,path> Checks parent directory existence.
$<PATH:GET_FILENAME,path> Extracts filename component.
$<PATH:GET_EXTENSION,path> Extracts extension component.
$<PATH:GET_STEM,path> Extracts filename without extension.
$<PATH:GET_RELATIVE_PART,path> Extracts relative portion.
$<PATH:GET_PARENT_PATH,path> Extracts parent path.
$<PATH:GET_ROOT_NAME,path> Extracts root drive/server name.
$<PATH:GET_ROOT_DIRECTORY,path> Extracts root path directory.
$<PATH:GET_ROOT_PATH,path> Extracts full root path.
$<PATH:NORMAL_PATH,path> Normalizes path syntax.
$<PATH:RELATIVE_PATH,path,base_dir> Computes relative path.
$<PATH:ABSOLUTE_PATH,path,base_dir> Computes absolute path.

Target Interfaces

Expression Description
$<BUILD_INTERFACE:paths...> Used during local build/subproject tree context.
$<INSTALL_INTERFACE:paths...> Used when package is installed and imported via find_package().
$<BUILD_LOCAL_INTERFACE:paths...> Used only in current build tree (not inherited transitivity).

7. Escaping & Output Characters

Special keys used to pass reserved syntactic characters inside generator expressions.

Generator Expression Description
$<ANGLE-R> Right angle bracket >
$<COMMA> Literal comma ,
$<SEMICOLON> Literal semicolon ;
$<LOWER_THAN> Left angle bracket <
$<GREATER_THAN> Right angle bracket >

Aug 1, 2026

[C++][coroutine] Symmetric Transfer - what problem it solves

The Core Problem: Stack Overflow via Asymmetric Transfer

In early C++ Coroutines (Coroutines TS), resuming a coroutine meant calling .resume() inside `await_suspend()`. When coroutines synchronously complete in a loop or tail-recurse, every .resume()  call pushes a new C++ stack frame without popping the old one, leading to stack overflow.

// ASYMMETRIC TRANSFER (Naive Approach)

void await_suspend(std::coroutine_handle<> h) {
    // Calling .resume() pushes a new stack frame.
    // If done in a deep loop or recursive chain, stack space explodes!
    other_coro_.resume(); 
}


Stack Frame Accumulation:

[ loop_coroutine$resume ]

  └─> [ task::awaiter::await_suspend ]

        └─> [ child_coroutine$resume ]

              └─> [ final_awaiter::await_suspend ]

                    └─> [ loop_coroutine$resume ]  <-- STACK OVERFLOW!



The Solution: Symmetric Transfer

Symmetric transfer allows `await_suspend()` to return a std::coroutine_handle<> instead of void.


Returning a handle suspends the current coroutine frame, pops the current stack frame, and transfers execution directly to the returned handle via a tail call.

Stack usage remains O(1) regardless of how many synchronous suspension/resumes occur.

// SYMMETRIC TRANSFER (Modern C++20)

std::coroutine_handle<> await_suspend(std::coroutine_handle<> h) {
    // Return the handle to transfer control directly.
    // The compiler generates a tail-call: pops current stack frame, then resumes target.
    return other_coro_; 

}


Key Implementations

A. The Awaiter (`task::operator co_await`)

When `co_await child_task;` executes, transfer control directly to the child's handle:

struct task_awaiter {
    std::coroutine_handle<promise_type> child_coro_;
    bool await_ready() noexcept { return false; }

    // Symmetric Transfer: Returns child handle to resume
    std::coroutine_handle<> await_suspend(std::coroutine_handle<> awaiting_coro) noexcept {
        // 1. Store caller as continuation in child's promise
        child_coro_.promise().continuation = awaiting_coro;
        
        // 2. Return child handle -> tail-call into child_coro_
        return child_coro_; 

    }
    void await_resume() noexcept {}
};


B. The Final Suspend (`promise_type::final_suspend`)

When a child coroutine finishes at `co_return`, transfer control back to its continuation (the caller):

struct final_awaiter {
    bool await_ready() noexcept { return false; }

    // Symmetric Transfer: Returns caller's handle to resume
    std::coroutine_handle<> await_suspend(std::coroutine_handle<promise_type> me) noexcept {
        // Returns parent handle -> tail-call back to parent coroutine
        return me.promise().continuation; 
    }
    void await_resume() noexcept {}

};

struct promise_type {
    std::coroutine_handle<> continuation{std::noop_coroutine()};
    final_awaiter final_suspend() noexcept { return {}; }
    // ...

};

Summary Matrix



[C++] coroutine memory layout