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):
- Upgrade to C++23.
- Refactor core components using modern features like Concepts and
std::format. - Stop using
makeand 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:
- Make the queue slower by calling destructors.
- Rewrite the entire message hierarchy to avoid polymorphism.
- 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.