I remember sitting in a windowless server room during a production outage in 2016, staring at a core dump that made absolutely no sense. A legacy codebase was using raw C-style unions to manage state, and some junior dev had accidentally written an integer into a memory slot that the rest of the system expected to be a pointer. The result wasn’t just a crash; it was a silent, non-deterministic corruption that drifted through the system for hours before anyone noticed. This is the fundamental danger of ignoring variant and type safe unions in modern C++. Most tutorials treat them like a minor syntactic convenience, but in a high-performance environment, they are the difference between a predictable state machine and a ticking time bomb.
I’m not here to give you a lecture on the theoretical elegance of sum types. I want to talk about how you actually implement them without tanking your instruction cache or introducing unnecessary overhead. I’ll show you the specific rules the compiler uses to enforce these boundaries and, more importantly, where the abstractions leak. We’re going to look at the mechanical reality of how `std::variant` handles its discriminator and why choosing the right tool is about managing complexity, not just following a style guide.
Table of Contents
Sum Types vs Product Types Where Your Logic Fails

Most developers treat every data structure as a container, but they fail to distinguish between how those containers actually constrain logic. When you build a `struct` containing an `int`, a `double`, and a `bool`, you’re building a product type. The total number of possible states is the product of all possible values for each member. It’s additive, expansive, and—crucially—it allows for states that shouldn’t exist, like a “valid” flag being false while the data is simultaneously present.
This is where the distinction between sum types vs product types becomes a matter of correctness rather than semantics. A sum type (like `std::variant`) represents an “OR” relationship: the object is either an `int` or a `double`. It collapses the state space. If you try to model mutually exclusive states using a product type with a manual `enum` tag, you’re just building a brittle, manual version of a tagged union implementation. You’re essentially begging for a bug where the tag says “Integer” but the memory holds a floating-point bit pattern. In a sum type, the type system enforces the relationship; in a product type, you’re just hoping your logic holds up.
Tagged Unions Implementation the Manual Labor You Shouldnt Do

Before `std::variant` arrived, we had to build our own tagged unions. This meant creating a `struct` that paired a raw `union` with an `enum` to track which member was currently active. On paper, it’s simple. In practice, it’s a minefield. You are responsible for manually updating that tag every time you write to the union, and more importantly, every time you read from it. If you update the data but forget to flip the tag, or if you read a member while the tag says something else, you’ve just introduced undefined behavior that no static analyzer is going to catch for you.
The real danger lies in the maintenance burden. When you’re handling polymorphic data structures this way, every single `switch` statement becomes a liability. You’re essentially trying to replicate pattern matching in functional programming using nothing but manual `if` checks and integer comparisons. There is no compiler assistance to ensure you’ve handled every possible case. If you add a new type to your union but miss one `switch` block buried in a corner of your codebase, you won’t get a compiler error—you’ll get a silent logic failure that only shows up when a specific, rare state hits your production environment.
Five Ways to Stop Shooting Yourself in the Foot
- Stop using raw unions for anything other than byte-level casting; if you aren’t implementing a custom allocator or a hardware-mapped interface, `std::variant` is your baseline.
- Always check `index()` or use `std::holds_alternative` before accessing a variant; assuming the type is what you think it is is how you end up debugging a segfault at 3 AM.
- Prefer `std::visit` over manual `if-else` chains or `switch` statements on type indices; the compiler can actually check if your visitor is exhaustive, which is one of the few ways to catch logic errors before they hit the build pipeline.
- Mind the exception safety of your types; if a type’s constructor throws while `std::variant` is trying to switch its active member, you’re entering a world of undefined behavior that no debugger will easily explain.
- Watch your stack usage; a `std::variant` is only as large as its largest member plus the discriminator, so don’t bury a massive, bloated struct inside a variant that gets passed around by value in a hot loop.
The Bottom Line
Stop manually managing type tags with enums and unions; you aren’t a compiler, and you will eventually forget to update a switch statement, leaving a stale type to corrupt your state.
Use `std::variant` to enforce sum type semantics, ensuring that your code can only ever exist in one valid state at a time, rather than a messy combination of unrelated fields.
Treat type safety as a latency-reduction strategy; catching a logic error via `std::get` or `std::visit` is infinitely cheaper than debugging a silent memory corruption in a production environment.
Stop Guessing, Start Typing
The choice is simple. You can continue manually managing tags and offsets in a raw union, praying that your `switch` statements stay in sync with your data structures, or you can let the standard library handle the heavy lifting. We’ve seen how product types fail when they try to represent mutually exclusive states, and we’ve seen how manual tagged unions turn your codebase into a minefield of undefined behavior. Using `std::variant` isn’t just about syntactic sugar; it’s about moving the burden of correctness from your fallible human brain to the compiler’s type system. If you aren’t using type-safe alternatives, you aren’t writing robust code—you’re just delaying a crash.
At the end of the day, C++ is a language that gives you enough rope to hang yourself if you don’t respect its complexity. But when you master the rules—when you understand exactly how the object model treats a variant versus a raw buffer—you stop fighting the tool and start wielding it. Don’t just aim for code that compiles; aim for code that is mathematically sound. Build your systems on foundations that the compiler can actually verify, and you’ll spend far less time debugging memory corruption and far more time actually solving interesting problems.