C++
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C++
-
Section 4: Memory Management
-
Section 5: Templates and Generic Programming
-
Section 6: The Standard Template Library
-
Section 7: Modern C++ Features
-
Section 8: Error Handling
-
Section 9: Multithreading
-
Section 10: Operator Overloading and Type Conversion
-
Section 11: Advanced Topics
-
Section 12: Tooling and Build Systems
-
Section 13: Design Patterns in C++
-
Section 14: Interfacing with C and Systems Programming
-
Section 15: Networking and IPC Basics
-
Section 16: Graphics and Game Programming Basics
-
Section 17: The Boost Libraries
-
Section 18: Data Structures and Algorithms in C++
-
Section 19: Practical Projects
-
Section 20: More Concurrency Patterns
-
Section 21: More OOP and Design Practice
-
Section 22: File I/O and Streams
-
Section 23: More Standard Library
-
Section 24: Practice Exercises
-
Section 25: Interview and Algorithm Practice
-
Section 26: Compiler and Language Internals
-
Section 27: GUI and Application Frameworks Overview
-
Section 28: Testing and Quality Practices
-
Section 29: Numerics Library
-
Section 30: Concepts Library (C++20)
-
Section 31: Ranges Library (C++20) In Depth
-
Section 32: More Utility Library
77: std::variant for Type-Safe Unions
A few years ago, I was auditing a legacy codebase for a physics engine. I found a section where the original author had used a C-style union to store either a Vector3 or a Quaternion to save a few bytes of memory per object. The problem was that they relied on a separate enum to track which type was currently active. During a late-night refactor, someone changed the way the objects were initialized, and a specific edge case started leaving that enum unset. The program didn't crash immediately; it just started treating quaternions as vectors. The resulting "jitter" in the simulation was nearly impossible to debug because the memory looked "fine," but the logic was fundamentally broken.
This is exactly why std::variant was introduced in C++17. It gives us the memory efficiency of a union, but it's "type-safe." That means the variant actually knows which type it's holding, and it won't let you accidentally treat a string as an integer without throwing an exception or returning a null pointer. I call it a "discriminated union," and it's a lifesaver when you're dealing with data that can be one of several distinct types.
Moving Beyond the Danger of C-Style Unions
When you define a std::variant<int, std::string, double>, the compiler allocates enough space for the largest of those types, plus a small amount of overhead to keep track of the "index" (which type is currently active). Unlike a raw union, std::variant properly calls constructors and destructors. If your variant holds a std::string and you assign an int to it, the variant handles the cleanup of the string automatically. You don't have to manually track the active member with a side-car enum.
#include <variant>
#include <string>
#include <iostream>
struct Click { int x, y; };
struct KeyPress { int key_code; };
struct Resize { int width, height; };
// A UIEvent can be any one of these three types
using UIEvent = std::variant<Click, KeyPress, Resize>;
void processEvent(const UIEvent& event) {
// We can check the index or use std::holds_alternative
if (std::holds_alternative<Click>(event)) {
auto& click = std::get<Click>(event);
std::cout << "Clicked at " << click.x << "," << click.y << "\n";
}
}
Now, std::get is powerful, but it's risky. If you call std::get<Click> on a variant that currently holds a Resize, it throws a std::bad_variant_access exception. If you're not in a position to use try-catch blocks, you can use std::get_if, which returns a pointer to the value if the type matches, or nullptr if it doesn't. I personally prefer get_if for performance-critical paths where exceptions are too heavy.
Dispatching Logic with std::visit
While if (std::holds_alternative...) works, it gets incredibly clunky once you have five or six possible types. You end up with a giant if-else chain that's a pain to maintain. The "pro" way to handle variants is through std::visit. This allows you to pass a "visitor" (usually a function object or a set of lambdas) that defines how to handle every possible type in the variant.
The most elegant pattern here is the "overload" helper. It's a tiny bit of template magic that lets you pass multiple lambdas to std::visit. It looks like this:
template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts> overloaded(Ts... ts) <!-- deduction guide --> { return {ts...}; }
void handleEvent(const UIEvent& event) {
std::visit(overloaded {
[](const Click& c) { std::cout << "Click: " << c.x << "\n"; },
[](const KeyPress& k) { std::cout << "Key: " << k.key_code << "\n"; },
[](const Resize& r) { std::cout << "Resize: " << r.width << "\n"; }
}, event);
}
I love this approach because the compiler enforces completeness. If you add a ScrollEvent to your UIEvent variant but forget to add a corresponding lambda in the std::visit call, the code simply won't compile. This turns a potential runtime crash into a compile-time fix, which is exactly where we want our bugs to be caught.
📋 Practical Task
Implementing a Type-Safe Command Parser
You are building a command system for a text-adventure game. The game needs to handle different types of commands: Move (which takes a direction string), Take (which takes an item name string), and Wait (which takes no arguments).
Your task is to:
- Define three structs:
Move,Take, andWait. - Create a
std::variantcalledGameCommandthat can hold any of these three types. - Implement a function
executeCommand(const GameCommand& cmd). - Inside
executeCommand, usestd::visitand theoverloadedpattern to print a specific message for each command (e.g., "Moving to the North", "Picking up the Golden Key", or "Waiting patiently..."). - In
main, create astd::vector<GameCommand>containing at least one of each command type and loop through the vector, callingexecuteCommandfor each.
There are no comments for now.