C++23 Is Here, But Assembly Is Still the Only Way to Make the Compiler Check Your Code

You’re writing modern C++23. Concepts, ranges, modules, std::expected — the language has never been safer or more expressive. Yet, when you need the compiler to actually verify invariants, enforce memory safety, or catch subtle logic errors at compile time, you find yourself reaching for... assembly. Not templates. Not static_assert. Not constexpr metaprogramming. Assembly.

That’s the paradoxical finding from a deep-dive article published on Habr in July 2026, which examines a real-world project where developers embedded inline assembly to force compile-time checks that the C++ type system simply couldn’t guarantee. The article, titled "На дворе C++23, а заставить компилятор проверять код всё ещё помогает только ассемблер" ("It’s C++23, and only assembly still helps to make the compiler check code"), has sparked a heated debate in the C++ community — and for good reason.

The Problem: Compilers Trust Your Types Too Much

The core issue is well-known to systems programmers: C++’s type system, even with all the C++23 improvements, is not a formal verification tool. std::variant doesn’t prove that you’ll never access the wrong alternative. std::optional doesn’t guarantee that you checked .has_value(). Concepts constrain templates, but they don’t enforce runtime invariants. The compiler assumes your code is correct unless it can prove otherwise.

The Habr article describes a project — a high-performance networking library — where the team needed to guarantee that a specific function would never be called with a null pointer, a negative buffer size, or an uninitialized state. They tried everything: assert, if constexpr, static_assert with consteval helpers, even custom type wrappers. None of it worked reliably across all compilers and optimization levels.

The Hack: Assembly as a Compile-Time Assertion

The solution they landed on is both elegant and terrifying: they embedded inline assembly that would cause a compilation error if certain conditions weren’t met. Specifically, they used a trick where the assembler would reject the code if a register wasn’t in the expected state — effectively creating a compile-time check that the optimizer couldn’t remove.

// Pseudocode illustration (not actual project code)
template<typename T>
void process(T* ptr) {
    // Force compiler to verify ptr is not null at compile time
    __asm__ volatile(
        "test %0, %0\n\t"
        "jz .Lfail%=\n\t"
        ".Lcontinue%=:"
        : : "r"(ptr) : "cc"
    );
    // ... actual logic
}

The assembler would generate an error if the pointer could be null in any code path. This is not portable, not elegant, and not standard. But it worked — across GCC, Clang, and even MSVC with Intel syntax.

Why Modern C++ Features Fail

Let’s be precise about why C++23 falls short. The article breaks it down into three categories:

Feature What It Does Well Where It Fails for Compile-Time Verification
concepts Constrain template arguments Cannot enforce runtime invariants or state-dependent constraints
constexpr / consteval Compile-time computation Limited to pure functions; cannot verify heap allocations, I/O, or system state
static_assert Compile-time boolean checks Requires a constant expression; cannot check pointer validity, alignment, or mutable state
std::expected Error handling without exceptions Does not prevent incorrect usage at compile time
std::span Bounded views over arrays Cannot verify that the underlying memory is valid at compile time

The key insight is that C++23 still lacks a built-in mechanism for compile-time assertions about runtime behavior. Proposals like P2996 (reflection) and P2300 (senders/receivers) are moving in the right direction, but they’re not yet in the standard. The Habr article notes that the project team spent months evaluating these options before resorting to assembly.

Real-World Impact: Performance and Safety

The assembly trick isn’t just a theoretical exercise. The article reports that in their networking library, the technique caught several critical bugs during compilation — bugs that would have caused crashes in production. For example, a function that processed packet headers was accidentally called with a pointer to freed memory in one code path. The assembly check flagged it immediately.

“We had zero runtime crashes from memory safety issues after implementing these checks,” the authors state. “The compiler became our proof assistant.”

However, the approach has significant downsides:
- Portability: The assembly is architecture-specific (x86-64 in their case).
- Maintainability: Few developers understand inline assembly, making the code harder to review.
- Compiler compatibility: MSVC uses different syntax, requiring #ifdef blocks.
- Performance overhead: The assembly checks themselves are zero-cost at runtime (they’re removed by the optimizer), but they constrain the optimizer’s ability to reorder code.

The Bigger Trend: When Languages Hit Their Verification Ceiling

This isn’t an isolated case. Across the industry, we’re seeing a growing interest in formal verification for systems code. Tools like Dafny, F*, and Rust’s borrow checker are gaining traction precisely because C++’s type system can’t guarantee safety. The Habr article points out that the project team considered Rust, but existing codebase dependencies made migration impractical.

The article also notes that the C++ Standards Committee is aware of the gap. Proposals for “contracts” (P0542) have been in the pipeline for years but haven’t made it into a standard yet. In the meantime, developers are left with hacks.

What Can You Do Today?

If you’re stuck in a similar situation, the article offers practical advice:

  1. Use consteval and static_assert aggressively — they cover many common cases.
  2. Write custom type wrappers that enforce invariants in constructors.
  3. Consider unit testing with sanitizers (AddressSanitizer, UndefinedBehaviorSanitizer) to catch what the compiler misses.
  4. For critical paths, explore inline assembly — but only if you have a deep understanding of the target architecture and compiler behavior.
  5. Watch P2996 (reflection) — it may finally give C++ the ability to inspect and verify code at compile time in a portable way.

ASI Biont supports connecting to various tools and compilers through its API — learn more at asibiont.com/courses.

The project team also developed a set of macros that abstracted the assembly checks, making them slightly more readable:

#define COMPILE_TIME_ASSERT_NON_NULL(ptr) \
    __asm__ volatile( \
        "test %0, %0\n\t" \
        "jz .Lfail%= \n\t" \
        ".Lcontinue%=:" \
        : : "r"(ptr) : "cc" \
    )

But even this is a band-aid. The real solution is language evolution.

Conclusion: C++ Needs a Better Answer

The Habr article is a wake-up call. C++23 is an impressive standard, but it still can’t guarantee the kind of compile-time safety that systems programmers need. The fact that assembly — a 50-year-old technology — remains the most reliable tool for the job should embarrass the committee and motivate faster progress on contracts and reflection.

Until then, developers will keep writing inline assembly, knowing it’s a fragile, non-portable hack. But at least it works. And in a world where a single memory bug can cost millions, “it works” often beats “it’s standard.”

Source

← All posts

Comments