With modern C++ continuously expanding the horizons of compile-time evaluation, C++26 takes another massive leap forward by introducing constexpr support for std::atomic. However, this raises an intriguing question when it comes to synchronization primitives: What does constexpr void wait(T old) actually do during constant evaluation?

At runtime, std::atomic::wait() enters a loop and blocks the calling thread until an atomic notification unblocks it (or it unblocks spuriously). But compile-time evaluation is strictly single-threaded. Does calling wait() deadlock the compiler? Let's break down how this works under the hood.

The Core Mechanism of std::atomic::wait()

To understand compile-time behavior, we first have to look at the conceptual definition of wait(). The standard specifies that atomic<T>::wait(T old) roughly behaves like this:

while (this->load() == old) {
    // Block execution until unblocked by notify_one(),
    // notify_all(), or a spurious wake-up.
}

At runtime, another thread is expected to modify the atomic variable and call notify_one() or notify_all() to break the loop. But during constant evaluation (at compile time), there is no multithreading and no concurrent memory access.

What Happens at Compile Time?

During constant evaluation, wait() branches into one of two fundamental scenarios:

Scenario 1: The Atomic Value Does Not Match old

If the value currently stored in the atomic variable is already different from old, wait() does not need to block at all. It evaluates the condition, sees that load() != old, and immediately returns.

#include <atomic>

constexpr bool test_wait() {
    std::atomic<int> a{42};
    // The current value is 42, which is != 0.
    // wait() does not block and returns immediately.
    a.wait(0);
    return true;
}

static_assert(test_wait()); // Compiles successfully!

In this scenario, constant evaluation succeeds seamlessly without blocking or invoking any complex synchronization primitives.

Scenario 2: The Atomic Value Matches old

If the atomic value is equal to old, the conceptual loop must block and wait for another thread to change the state. Because no other thread exists at compile time, the atomic variable will never change, and no notification can ever be sent.

  • No Spurious Wakeups: Constant evaluation must be completely deterministic and free of platform-dependent behavior. A compiler cannot randomly decide to "spuriously wake up" during constant evaluation.
  • Deadlock Prevention: Rather than freezing or deadlocking the compiler process indefinitely, this results in an infinite loop without side effects. In C++, infinite loops during constant evaluation violate core constant expression rules, causing the compiler to abort with an error (e.g., exceeding the maximum evaluation step limit).
constexpr bool test_deadlock() {
    std::atomic<int> a{42};
    // The current value is 42, which matches 'old'.
    // Since nothing can change this value, it cannot proceed.
    a.wait(42); // Compilation Error: not a constant expression!
    return true;
}

// static_assert(test_deadlock()); // Fails to compile

Why Make wait() constexpr in the First Place?

If waiting when the value equals old causes a compilation failure, why bother making wait() (and notify_*) constexpr?

  1. Uniform Interface for Generic Code: Modern C++ aims to minimize the split between compile-time and runtime code. Marking synchronization primitives as constexpr allows templated and generic data structures (such as custom lock-free queues, latches, or algorithms) to be instantiated and tested in constexpr contexts without needing verbose if consteval branches.
  2. C++ Standard Library Philosophy: In modern C++, a function is permitted to be declared constexpr as long as there is at least one valid set of inputs and object states that allows constant evaluation to succeed. Because wait(old) succeeds whenever load() != old, it qualifies for constexpr.
  3. Completeness of std::atomic: As part of the ongoing evolution of C++26 (following papers like P2689 and P3309), the goal is to make the entire std::atomic interface usable during constant evaluation. Functions like notify_one() and notify_all() simply become no-ops at compile time.

Summary

At compile time, constexpr std::atomic<T>::wait(old) simply acts as a conditional guard:

  • If value != old: It evaluates cleanly and returns immediately.
  • If value == old: Because no concurrent thread can change the value, it creates an infinite evaluation condition, triggering a compile-time error rather than deadlocking the compiler process.