Type Safe Sum Types (std::variant vs Polymorphism)
Object Oriented Programming and RAII

4.5 Type Safe Sum Types (std::variant vs Polymorphism)

Polymorphism is the classic Object Oriented way to implement runtime dispatch, but virtual table lookups carry pointer dereference latencies. C++17 introduced type safe sum types via std::variant, providing a stack allocated, zero redirect alternative to class hierarchies.

Let's look at variant layouts, visitor operations, and polymorphism trade offs.

What are Sum Types? (std::variant)

A std::variant is a type safe replacement for standard C style unions. While a union allows you to store different types in the same memory space but does not track which type is currently active (making it easy to read garbage data), std::variant enforces strict safety by keeping a record of the active type.

#include <iostream>
#include <variant>
#include <string>

struct Circle { double radius; };
struct Square { double side; };

// A Shape can be a Circle OR a Square, but never both!
using Shape = std::variant<Circle, Square>;

int main() {
    Shape s{Circle{5.0}};

    // Using std::holds_alternative to query type
    if (std::holds_alternative<Circle>(s)) {
        std::cout << "Circle radius: " << std::get<Circle>(s).radius << '\n';
    }

    // Using std::visit to apply dynamic logic based on active variant type
    std::visit([](auto&& arg) {
        using T = std::decay_t<decltype(arg)>;
        if constexpr (std::is_same_v<T, Circle>) {
            std::cout << "Visited Circle: " << arg.radius << '\n';
        } else if constexpr (std::is_same_v<T, Square>) {
            std::cout << "Visited Square: " << arg.side << '\n';
        }
    }, s);
    return 0;
}
Using std::variant and std::visit in C++17.

Variant Memory Layout vs Virtual Tables

Under the hood, a std::variant is a tagged union. The compiler reserves inline stack space equal to the size of the largest alternative type, plus an internal index (type tag) that records which alternative is active. The index is at least one byte and is often padded for alignment, so the total size is not simply max(sizeof(Ts)...) + 1.

Because the memory is inline (no heap allocation for the variant object itself), there is no virtual pointer dereferencing, and access patterns are often more cache-friendly than pointer-chasing polymorphism. When combined with std::visit, the compiler can generate branch tables that are predictable on many workloads, measure before assuming they beat virtual dispatch in your hot path.

std::any and std::monostate

std::any (C++17) is a container that can hold a value of any* type. Unlike std::variant, which requires listing all possible types in advance, std::any dynamically manages any value, though it relies on heap allocation when values exceed small storage optimization limits.

* std::monostate is a helper type used with std::variant when you want a distinct empty state. Put it first when you need a default-constructible variant with no active value yet, for example, std::variant<std::monostate, int, std::string> v; starts empty.

* The Storage Locker Metaphor: Virtual polymorphism is like dialing a switchboard operator who redirects you to different offices (vptr jumps). std::variant is like opening a physical labeled storage locker. The locker has dedicated slots for a circle or a square. You inspect the label on the door (type tag) and pull the shape out directly with no middleman.

Chapter 4 Knowledge CheckQuestion 1 of 3

Why is it dangerous to delete a derived class object through a base class pointer if the base class destructor is NOT virtual?

Finished reading this lesson?