Modernizing Legacy C++: The Qlog Journey


Introduction

Is it actually worth it?

That’s the question I asked myself when I looked at qlog. It was a “stable” (read: ancient) library written in C++11/14. It worked. It was fast. But looking at the code felt like staring into a time capsule of bad decisions and workaround macros.

Modernizing a legacy C++ codebase is never just about changing the compiler flag to -std=c++23. To be honest, it’s usually a trainwreck of uncovering hidden dependencies, wrestling with template metaprogramming that no one understands anymore, and rethinking architectural decisions that probably made sense in 2014.

This isn’t a tutorial. It’s a log of my suffering (and eventual triumph) modernizing qlog to C++23.

The Challenge

qlog was built for speed. It relied heavily on template metaprogramming for compile-time string manipulation and lock-free queues.

But here’s the thing: the codebase had aged like milk. It used manual memory management, custom string wrappers that were effectively just std::string but worse, and C++ features that made it hard to read.

My goal was simple (or so I thought):

  1. Upgrade to C++23.
  2. Refactor core components using modern features like Concepts and std::format.
  3. Stop using make and actually use CMake like a civilized human being.

Key Refactorings

1. Compile-Time Strings (The “Good” Part)

One of the few things qlog got right was zero-allocation string formatting. But the implementation? Absolute macro hell.

In C++20, Class Non-Type Template Parameters (CNTTP) changed the game. I refactored StringCT to accept string literals directly as template arguments.

Before:

// SCT("Hello") -> expands to some ungodly recursive template mess

After (C++20 CNTTP):

template <FixedString S>
struct StringLiteralToCT { ... };

using Hello = StringLiteralToCT<"Hello">;

It’s cleaner. It’s more expressive. It’s actually readable. For once, a C++ feature that simplifies things instead of adding more angle brackets.

2. Lock-Free Queues and Atomics

The LockFreeQueue implementation relied on std::atomic with explicit memory orderings. While correct, it was verbose.

I updated this to use std::atomic_ref where it made sense. I also ensured that alignment requirements (hardware_destructive_interference_size) were respected. False sharing is a silent performance killer, and to be honest, I was paranoid about it.

3. Taming the Namespace Beast

One of the most frustrating bugs I encountered was a namespace visibility issue where ::common was not being found inside a nested namespace.

It turned out to be a subtle interaction between header inclusions and macro expansions. The fix? Being explicit with global namespace qualification (::common). It was a stupid bug, but it reminded me that in C++, explicit is always better than implicit.

4. Concepts: The Guardrails We Needed (And One We Didn’t)

I decided to go all-in on C++20 Concepts. Why rely on obscure SFINAE error messages when you can have the compiler yell at you in plain English?

I defined CompileTimeString to ensure my CNTTP hacks were actually receiving string literals. I added TimeType to constrain the logger. It felt good. It felt correct.

Then I tried to enforce TriviallyDestructible on the lock-free queue.

Boom. Build failed.

Turns out, my FormattedMessage class has a virtual destructor (because polymorphism). That makes it non-trivially destructible. But the queue doesn’t call destructors on pop (for speed).

So I had a choice:

  1. Make the queue slower by calling destructors.
  2. Rewrite the entire message hierarchy to avoid polymorphism.
  3. Delete the concept and pretend I never saw the error.

I chose option 3. Sometimes, “modern” C++ clashes with “fast” C++. And in HFT, fast always wins.

The Build System

Moving from a custom Makefile to CMake was… refreshing.

I know people hate CMake, but compared to a brittle, hand-rolled Makefile? It’s a masterpiece. Setting up Google Benchmark and GTest was trivial. And getting GitHub Actions to run CI on every push gave me that “green checkmark” dopamine hit I desperately needed.

Conclusion

Overall, modernizing qlog was a reminder that C++ has come a long way.

Features like std::format, Concepts, and CNTTP make the language feel almost… modern. It’s still C++, so you’re still managing memory and fighting the compiler, but the tools are sharper.

Is the code faster? Maybe slightly. Is it cleaner? Absolutely. Was it worth the headache?

Yeah. I think so.

In the next post, I’ll dive deep into the technical implementation of the lock-free queue. That’s where the real magic (and horror) lives.