Table of Contents

Around the time C++17 was being standardized I saw magical terms like “discriminated union”, “type-safe union” or “sum type” floating around. Later it appeared to mean the same type: “variant”.

Let’s see how this brand new std::variant from C++17 works and where it might be useful.

Update in early 2023 with notes about C++20, C++23 and other smaller refinements/wording.

Something better than union  

But lest first start with something old… unions

Unions are rarely used in the client code, and most of the time, they should be avoided.

For example, there’s a “common” trick with floating-point operations:

union SuperFloat {
    float f;
    int i;
};

int RawMantissa(SuperFloat f) { 
    return f.i & ((1 << 23) - 1); 
}
int RawExponent(SuperFloat f) {
    return (f.i >> 23) & 0xFF; 
}

However, while the above code might work in C99, due to stricter aliasing rules it’s undefined behaviour in C++!

There’s an existing Core Guideline Rule on that C.183:

C.183: Don’t use a union for type punning:

It is undefined behaviour to read a union member with a different type from the one with which it was written. Such punning is invisible, or at least harder to spot than using a named cast. Type punning using a union is a source of errors.

There’s also additional issue with unions: they’re very simple and crude. You don’t have a way to know what’s the currently used type and what’s more they won’t call destructors of the underlying types. Here’s an example from cppreference/union that clearly illustrate how hard it might be:

#include <iostream>
#include <string>
#include <vector>

union S
{
    std::string str;
    std::vector<int> vec;
    ~S() { } // what to delete here?
};

int main()
{
    S s = {"Hello, world"};
    // at this point, reading from s.vec is undefined behavior
    std::cout << "s.str = " << s.str << '\n';

    // you have to call destructor of the contained objects!
    s.str.~basic_string<char>();

    // and a constructor!
    new (&s.vec) std::vector<int>;

    // now, s.vec is the active member of the union
    s.vec.push_back(10);
    std::cout << s.vec.size() << '\n';

    // another destructor
    s.vec.~vector<int>();
}

Play with the code @Coliru

As you see, the S union needs a lot of maintenance from your side. You have to know which type is active and adequately call destructors/constructors before switching to a new variant.

That’s the reason you probably won’t see a lot of unions that use “advanced” types such as vectors, strings, containers, etc, etc. Union is mostly for basic types.

What could make unions better?

  • the ability to use complex types
    • and the full support of their lifetime: if you switch the type then a proper destructor is called. That way we don’t leak.
  • a way to know what’s the active type

Before C++17 you could use some third-party library…. or use boost::variant. But now you have std::variant.

Basic demo of std::variant  

Here’s a basic demo of what you can do with this new type:

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

struct SampleVisitor
{
    void operator()(int i) const { 
        std::cout << "int: " << i << "\n"; 
    }
    void operator()(float f) const { 
        std::cout << "float: " << f << "\n"; 
    }
    void operator()(const std::string& s) const { 
        std::cout << "string: " << s << "\n"; 
    }
};

int main()
{
    std::variant<int, float, std::string> intFloatString;
    static_assert(std::variant_size_v<decltype(intFloatString)> == 3);

    // default initialized to the first alternative, should be 0
    std::visit(SampleVisitor{}, intFloatString);

    // index will show the currently used 'type'
    std::cout << "index = " << intFloatString.index() << std::endl;
    intFloatString = 100.0f;
    std::cout << "index = " << intFloatString.index() << std::endl;
    intFloatString = "hello super world";
    std::cout << "index = " << intFloatString.index() << std::endl;

    // try with get_if:
    if (const auto intPtr (std::get_if<int>(&intFloatString)); intPtr) 
        std::cout << "int!" << *intPtr << "\n";
    else if (const auto floatPtr (std::get_if<float>(&intFloatString)); floatPtr) 
        std::cout << "float!" << *floatPtr << "\n";

    if (std::holds_alternative<int>(intFloatString))
        std::cout << "the variant holds an int!\n";
    else if (std::holds_alternative<float>(intFloatString))
        std::cout << "the variant holds a float\n";
    else if (std::holds_alternative<std::string>(intFloatString))
        std::cout << "the variant holds a string\n";  

    // try/catch and bad_variant_access
    try 
    {
        auto f = std::get<float>(intFloatString); 
        std::cout << "float! " << f << "\n";
    }
    catch (std::bad_variant_access&) 
    {
        std::cout << "our variant doesn't hold float at this moment...\n";
    }

    // visit:
    std::visit(SampleVisitor{}, intFloatString);
    intFloatString = 10;
    std::visit(SampleVisitor{}, intFloatString);
    intFloatString = 10.0f;
    std::visit(SampleVisitor{}, intFloatString);
}

Play with the code @Coliru

the output:

int: 0
index = 0
index = 1
allocating 18 bytes
index = 2
the variant holds a string
our variant doesn't hold float at this moment...
string: hello super world
global op delete called
int: 10
float: 10

We have several things showed in the example above:

  • You know what’s the currently used type via index() or check via holds_alternative.
  • You can access the value by using get_if or get (but that might throw bad_variant_access exception)
  • Type Safety - the variant doesn’t allow to get a value of the type that’s not active
  • If you don’t initialize a variant with a value, then the variant is initialized with the first type. In that case the first alternative type must have a default constructor.
  • No extra heap allocation happens
  • You can use a visitor to invoke some action on a currently hold type.
  • The variant class calls destructors and constructors of non-trivial types, so in the example, the string object is cleaned up before we switch to new variants.

When to Use  

I’d say that unless you’re doing some low-level stuff, possibly only with simple types, then unions might still be ok. But for all other uses cases, where you need variant types, std::variant is a way to go!

Some possible uses:

  • All the places where you might get a few types for a single field: so things like parsing command lines, ini files, language parsers, etc, etc.
  • Expressing efficiently several possible outcomes of a computation: like finding roots of equations
  • Error handling - for example you can return variant<Object, ErrorCode>. If the value is available, then you return Object otherwise you assign some error code (as of C++23 you can use std::expected).
  • State machines
  • Polymorphism without vtables and inheritance (thanks to visiting pattern)

A Functional Background  

It’s also worth mentioning that variant types (also called a tagged union, a discriminated union, or a sum type) comes from the functional language world and Type Theory.

After a little demo and introduction, we can now talk about some more details… so read on.

The Series  

This article is part of my series about C++17 Library Utilities. Here’s the list of the other topics that I’ll cover:

Plus other 110+ articles with the C++17 tag @C++Stories.

Resources about C++17 STL:

std::variant Creation  

There are several ways you can create and initialize std::variant:

// default initialization: (type has to has a default ctor)
std::variant<int, float> intFloat;
std::cout << intFloat.index() << ", value " << std::get<int>(intFloat) << "\n";

// monostate for default initialization:
class NotSimple
{
public:
    NotSimple(int, float) { }
};

// std::variant<NotSimple, int> cannotInit; // error
std::variant<std::monostate, NotSimple, int> okInit;
std::cout << okInit.index() << "\n";

// pass a value:
std::variant<int, float, std::string> intFloatString { 10.5f };
std::cout << intFloatString.index() << ", value " << std::get<float>(intFloatString) << "\n";

// ambiguity
// double might convert to float or int, so the compiler cannot decide

//std::variant<int, float, std::string> intFloatString { 10.5 };

// ambiguity resolved by in_place
std::variant<long, float, std::string> longFloatString { std::in_place_index<1>, 7.6 }; // double!
std::cout << longFloatString.index() << ", value " << std::get<float>(longFloatString) << "\n";

// in_place for complex types
std::variant<std::vector<int>, std::string> vecStr { std::in_place_index<0>, { 0, 1, 2, 3 }};
std::cout << vecStr.index() << ", vector size " << std::get<std::vector<int>>(vecStr).size() << "\n";

// copy-initialize from other variant:
std::variant<int, float> intFloatSecond { intFloat };
std::cout << intFloatSecond.index() << ", value " << std::get<int>(intFloatSecond) << "\n";

Play with the code here @Compiler Explorer

The output:

0, value 0
0
1, value 10.5
1, value 7.6
0, vector size 4
0, value 0

Notes:

  • By default, a variant object is initialized with the first type,
    • if that’s not possible when the type doesn’t have a default constructor, then you’ll get a compiler error
    • you can use std::monostate to pass it as the first type in that case
  • You can initialize it with a value, and then the best matching type is used
    • if there’s an ambiguity, then you can use a version std::in_place_index to explicitly mention what type should be used.
  • std::in_place also allows you to create more complex types and pass more parameters to the constructor

About std::monostate  

In the example you might notice a special type called std::monostate. It’s just an empty type that can be used with variants to represent empty state. The type might be handy when the first alternative doesn’t have a default constructor. In that situation you can place std::monostate as the first alternative.

Changing values  

There are four ways to change the current value of the variant:

  • the assignment operator
  • emplace
  • get and then assign a new value for the currently active type
  • a visitor

The important part is to know that everything is type safe and also the object lifetime is honoured.

std::variant<int, float, std::string> intFloatString { "Hello" };
    
intFloatString = 10; // we're now an int
    
intFloatString.emplace<2>(std::string("Hello")); // we're now string again
    
// std::get returns a reference, so you can change the value:
std::get<std::string>(intFloatString) += std::string(" World");
    
intFloatString = 10.1f;
if (auto pFloat = std::get_if<float>(&intFloatString); pFloat)
    *pFloat *= 2.0f;

See the live example @Coliru

Output:

Hello
10
Hello
Hello World
20.2

Object Lifetime  

When you use union, you need to manage the internal state: call constructors or destructors. This is error prone and easy to shoot yourself in the foot. But std::variant handles object lifetime as you expect. That means that if it’s about to change the currently stored type then a destructor of the underlying type is called.

std::variant<std::string, int> v { "Hello A Quite Long String" };
// v allocates some memory for the string
v = 10; // we call destructor for the string!
// no memory leak

Or see this example with a custom type:

class MyType
{
public:
    MyType() { std::cout << "MyType::MyType\n"; }
    ~MyType() { std::cout << "MyType::~MyType\n"; }
};

class OtherType
{
public:
    OtherType() { std::cout << "OtherType::OtherType\n"; }
    OtherType(const OtherType&) { 
        std::cout << "OtherType::OtherType(const OtherType&)\n"; 
    }
    ~OtherType() { std::cout << "OtherType::~OtherType\n"; }
};

int main() 
{
    std::variant<MyType, OtherType> v;
    v = OtherType();

    return 0;
}

This will produce the following output:

MyType::MyType
OtherType::OtherType
MyType::~MyType
OtherType::OtherType(const OtherType&)
OtherType::~OtherType
OtherType::~OtherType

Play with the code @Coliru

At the start, we initialize with a default value of type MyType; then we change the value with an instance of OtherType, and before the assignment, the destructor of MyType is called. Later we destroy the temporary object and the object stored in the variant.

Accessing the Stored Value  

From all of the examples, you’ve seen so far you might get an idea how to access the value. But let’s make a summary of this important operation.

First of all, even if you know what’s the currently active type you cannot do:

std::variant<int, float, std::string> intFloatString { "Hello" };
std::string s = intFloatString;

// error: conversion from 
// 'std::variant<int, float, std::string>' 
// to non-scalar type 'std::string' requested
// std::string s = intFloatString;

So you have to use helper functions to access the value.

You have std::get<Type|Index>(variant) which is a non member function. It returns a reference to the desired type if it’s active (You can pass a Type or Index). If not then you’ll get std::bad_variant_access exception.

std::variant<int, float, std::string> intFloatString;
try 
{
    auto f = std::get<float>(intFloatString); 
    std::cout << "float! " << f << "\n";
}
catch (std::bad_variant_access&) 
{
    std::cout << "our variant doesn't hold float at this moment...\n";
}

The next option is std::get_if. This function is also a non-member and won’t throw. It returns a pointer to the active type or nullptr. While std::get needs a reference to the variant, std::get_if takes a pointer. I’m not sure why we have this inconsistency.

if (const auto intPtr = std::get_if<0>(&intFloatString)) 
    std::cout << "int!" << *intPtr << "\n";

However, probably the most important way to access a value inside a variant is by using visitors.

Visitors for std::variant  

With the introduction of std::variant we also got a handy STL function called std::visit.

It can call a given “visitor” on all passed variants.

Here’s the declaration:

template <class Visitor, class... Variants>  
constexpr visit(Visitor&& vis, Variants&&... vars);

And it will call vis on the currently active type of variants.

If you pass only one variant, then you have to have overloads for the types from that variant. If you give two variants, then you have to have overloads for all possible pairs of the types from the variants.

A visitor is “a callable that accepts every possible alternative from every variant “.

Let’s see some examples:

// a generic lambda:
auto PrintVisitor = [](const auto& t) { std::cout << t << "\n"; };

std::variant<int, float, std::string> intFloatString { "Hello" };
std::visit(PrintVisitor, intFloatString);

In the above example, a generic lambda is used to generate all possible overloads. Since all of the types in the variant supports << then we can print them.

In the another case we can use a visitor to change the value:

auto PrintVisitor = [](const auto& t) { std::cout << t << "\n"; };
auto TwiceMoreVisitor = [](auto& t) { t*= 2; };

std::variant<int, float> intFloat { 20.4f };
std::visit(PrintVisitor, intFloat);
std::visit(TwiceMoreVisitor, intFloat);
std::visit(PrintVisitor, intFloat);

Generic lambdas can work if our types share the same “interface”, but in most of the cases, we’d like to do some different actions based on an active type.

That’s why we can define a structure with several overloads for the operator ():

struct MultiplyVisitor
{
    float mFactor;

    MultiplyVisitor(float factor) : mFactor(factor) { }

    void operator()(int& i) const {
        i *= static_cast<int>(mFactor);
    }

    void operator()(float& f) const {
        f *= mFactor;
    }

    void operator()(std::string& ) const {
        // nothing to do here...
    }
};

std::visit(MultiplyVisitor(0.5f), intFloat);
std::visit(PrintVisitor, intFloat);

In the example, you might notice that I’ve used a state to hold the desired scaling factor value.

With lambdas, we got used to declaring things just next to its usage. And when you need to write a separate structure, you need to go out of that local scope. That’s why it might be handy to use overload construction.

Overload  

With this utility you can write all several lambdas for all matching types in one place:

std::visit
(
  overload
  (
    [](const int& i) { PRINT("int: " + i); },
    [](const std::string& s) { PRINT("it's a string: " + s); },
    [](const float& f) { PRINT("float" + f); }
  ),
  yourVariant;
);

Currently this helper is not part of the library (it might get into with C++20), but the code might look like that:

template<class... Ts> struct overload : Ts... { using Ts::operator()...; };
template<class... Ts> overload(Ts...) -> overload<Ts...>;

Those two lines look like a bit of magic :) But all they do is they create a struct that inherits all given lambdas and uses their Ts::operator(). The whole structure can be now passed to std::visit.

For example:

std::variant<int, float, std::string> intFloatString { "Hello" };
std::visit(overload{
    [](int& i) { i*= 2; },
    [](float& f) { f*= 2.0f; },
    [](std::string& s) { s = s + s; }
}, intFloatString);
std::visit(PrintVisitor, intFloatString);
// prints: "HelloHello"

Play with the code @Coliru

Here’s a full blog post explaining the feature: 2 Lines Of Code and 3 C++17 Features - The overload Pattern - C++ Stories.

And other articles:

Other std::variant Operations  

Just for the sake of completeness:

  • You can compare two variants of the same type:
    • if they contain the same active alternative then the corresponding comparison operator is called.
    • If one variant has an “earlier” alternative then it’s “less than” the variant with the next active alternative.
  • Variant is a value type, so you can move it.
  • std::hash on a variant is also possible.

Exception Safety Guarantees  

So far everything looks nice and smooth… but what happens when there’s an exception during the creation of the alternative in a variant?

For example

class ThrowingClass
{
public:
    explicit ThrowingClass(int i) { if (i == 0) throw int (10); }
    operator int () { throw int(10); }
};

int main(int argc, char** argv) 
{
    std::variant<int, ThrowingClass> v;

    // change the value:
    try
    {
        v = ThrowingClass(0);
    }
    catch (...)
    {
        std::cout << "catch(...)\n";
        // we keep the old state!
        std::cout << v.valueless_by_exception() << "\n";   
        std::cout << std::get<int>(v) << "\n"; 
    }

    // inside emplace
    try
    {
        v.emplace<0>(ThrowingClass(10)); // calls the operator int
    }
    catch (...)
    {
        std::cout << "catch(...)\n";
        // the old state was destroyed, so we're not in invalid state!
        std::cout << v.valueless_by_exception() << "\n";   
    }

    return 0;
}

Play with the code @Coliru

The output:

catch(...)
0
0
catch(...)
1

In the first case - with the assignment operator - the exception is thrown in the constructor of the type. This happens before the old value is replaced in the variant, so the variant state is unchanged. As you can see we can still access int and print it.

However, in the second case - emplace - the exception is thrown after the old state of the variant is destroyed. Emplace calls operator int to replace the value, but that throws. After that, the variant is in a wrong state, as we cannot recover.

Also note that a variant that is “valueless by exception” is in an invalid state. Accessing a value from such variant is not possible. That’s why variant::index returns variant_npos, and std::get and std::visit will throw bad_variant_access.

Performance & Memory Considerations  

std::variant uses the memory in a similar way to union: so it will take the max size of the underlying types. But since we need something that will know what’s the currently active alternative, then we need to add some more space.

Plus everything needs to honour the alignment rules.

Here are some basic sizes:

std::cout << "sizeof string: " 
          << sizeof(std::string) << "\n";

std::cout << "sizeof variant<int, string>: " 
          << sizeof(std::variant<int, std::string>) << "\n";

std::cout << "sizeof variant<int, float>: " 
          << sizeof(std::variant<int, float>) << "\n";

std::cout << "sizeof variant<int, double>: " 
          << sizeof(std::variant<int, double>) << "\n";

On GCC 8.1, 32 bit I have:

sizeof string: 32
sizeof variant<int, string>: 40
sizeof variant<int, float>: 8
sizeof variant<int, double>: 16

Play with the code @Coliru

What’s more interesting is that std::variant won’t allocate anyextra space! No dynamic allocation happens to hold variants. and the discriminator.

While you pay some extra space for all the type-safe functionality, it shouldn’t cost you regarding runtime performance.

Migration From boost::variant  

Boost Variant was introduced around the year 2004, so it was 13 years of experience before std::variant was added into the Standard. The STL type takes from the experience of the boost version and improves it.

Here are the main changes:

Feature Boost.Variant (1.67.0) std::variant
Extra memory allocation Possible on assignment, see Design Overview - Never Empty No
visiting apply_visitor std::visit
get by index no yes
recursive variant yes, see make_recursive_variant no
duplicated entries no yes
empty alternative boost::blank std::monostate

You can also see the slides from Variants - Past, Present, and Future - David Sankel - CppCon 2016 Where there is more discussion about the changes and the proposal.

Or the video @Youtube

Examples of std::variant  

After we learned most of the std::variant details, we can now explore a few examples. So far, the code I used was a bit artificial, but in this section, I tried to look for some real-life examples.

Error Handling  

The basic idea is to wrap the possible return type with some ErrorCode, and that way allow to output more information about the errors. Without using exceptions or output parameters. This is similar to what std::expected might be in C++23..

enum class ErrorCode
{
    Ok,
    SystemError,
    IoError,
    NetworkError
};

std::variant<std::string, ErrorCode> FetchNameFromNetwork(int i)
{
    if (i == 0)
        return ErrorCode::SystemError;

    if (i == 1)
        return ErrorCode::NetworkError;

    return std::string("Hello World!");
}

int main() 
{
    auto response = FetchNameFromNetwork(0);
    if (std::holds_alternative<std::string>(response))
        std::cout << std::get<std::string>(response) << "n";
    else
        std::cout << "Error!\n";

    response = FetchNameFromNetwork(10);
    if (std::holds_alternative<std::string>(response))
        std::cout << std::get<std::string>(response) << "n";
    else
        std::cout << "Error!\n";

    return 0;
}

Play with the example @Coliru

In the example, I’m returning ErrorCode or a valid type - in this case, a string.

In C++23 you can use std::expected. See at std::expected - cppreference.com.

Computing Roots of an Equation  

Sometimes the computation might give us several options, for example, real roots of the equation. With variant, we can wrap all the available options and express clearly how many roots can we find.

using DoublePair = std::pair<double, double>
using EquationRoots = std::variant<DoublePair, double, std::monostate>; 

EquationRoots FindRoots(double a, double b, double c)
{
    if (a == 0.0)
        return std::nan("");

    auto d = b*b-4*a*c; // discriminant
    if (d > 0.0)
    {
        double root1 = (-b + std::sqrt(d)) / (2*a);
        double root2 = (-b - std::sqrt(d)) / (2*a);
        return std::make_pair(root1, root2);
    }
    else if (d == 0.0)
        return (-1*b)/(2*a);
    
    return std::monostate();
}

struct RootPrinterVisitor
{
    void operator()(const DoublePair& arg) 
    { 
        std::cout << "2 roots: " << arg.first << " " << arg.second << '\n'; 
    }
    void operator()(double arg) 
    { 
        if (std::isnan(arg))
            std::cout << "not a quadratic equation!" << arg << '\n'; 
        else
            std::cout << "1 root found: " << arg << '\n'; 
    }
    void operator()(std::monostate) 
    { 
        std::cout << "No real roots found.\n"; 
    }
};

int main() 
{
    std::visit(RootPrinterVisitor{}, FindRoots(10,0,-2));
    std::visit(RootPrinterVisitor{}, FindRoots(2,0,-1));
    std::visit(RootPrinterVisitor{}, FindRoots(1,2,1));
    std::visit(RootPrinterVisitor{}, FindRoots(1,2,3));
    std::visit(RootPrinterVisitor{}, FindRoots(0,2,3));
}

Play with the code @Compiler Explorer.

The output:

2 roots found: 0.447214 -0.447214
2 roots found: 0.707107 -0.707107
1 root found: -1
No real roots found.
not a quadratic equation! nan

Parsing a Command Line  

Command line might contain text arguments that might be interpreted in a few ways:

  • as integer
  • as boolean flag
  • as a string (not parsed)

We can build a variant that will hold all the possible options.

Here’s a simple version with intand string:

class CmdLine
{
public:
    using Arg = std::variant<int, std::string>;

private:
    std::map<std::string, Arg> mParsedArgs;

public:
    explicit CmdLine(int argc, char** argv) { ParseArgs(argc, argv); }

    // ...
};

And the parsing code:

CmdLine::Arg TryParseString(char* arg)
{
    // try with int first
    int iResult = 0;
    auto res = std::from_chars(arg, arg+strlen(arg), iResult);
    if (res.ec == std::errc::invalid_argument)
    {
        // if not possible, then just assume it's a string
        return std::string(arg);
    }

    return iResult;
}

void CmdLine::ParseArgs(int argc, char** argv)
{
    // the form: -argName value -argName value
    // unnamed? later...
    for (int i = 1; i < argc; i+=2)
    {
        if (argv[i][0] != '-') // super advanced pattern matching! :)
            throw std::runtime_error("wrong command name");

        mParsedArgs[argv[i]+1] = TryParseString(argv[i+1]);
    }
}

At the moment of writing, std::from_chars in GCC only supports integers, in MSVC floating point support is on the way. But the idea of the TryParseString is to try with parsing the input string to the best matching type. So if it looks like an integer, then we try to fetch integer. Otherwise, we’ll return an unparsed string. Of course, we can extend this approach.

Example how we can use it:

try
{
    CmdLine cmdLine(argc, argv);

    auto arg = cmdLine.Find("paramInt");
    if (arg && std::holds_alternative<int>(*arg))
        std::cout << "paramInt is " 
                  << std::get<int>(*arg) << "\n";

    arg = cmdLine.Find("textParam");
    if (arg && std::holds_alternative<std::string>(*arg))
        std::cout << "textParam is " 
                  << std::get<std::string>(*arg) << "\n";    
}
catch (std::runtime_error &err)
{
    std::cout << err.what() << "\n";
}

Play with the code @Coliru

Parsing a Config File  

I don’t have a code for that, but the idea comes from the previous example of a command line. In the case of a configuration file, we usually work with pairs of <Name, Value>. Where Value might be a different type: string, int, array, bool, float, etc.

In my experience I’ve seen examples where even void* was used to hold such unknown type so we could improve the design by using std::variant if we know all the possible types, or leverage std::any.

State Machines  

How about modelling a state machine? For example door’s state:

We can use different types of states and the use visitors as events:

struct DoorState 
{
    struct DoorOpened {};
    struct DoorClosed {};
    struct DoorLocked {};

    using State = std::variant<DoorOpened, DoorClosed, DoorLocked>;

    void open()
    {
        m_state = std::visit(OpenEvent{}, m_state);
    }

    void close()
    {
        m_state = std::visit(CloseEvent{}, m_state);
    }

    void lock()
    {
        m_state = std::visit(LockEvent{}, m_state);
    }

    void unlock()
    {
        m_state = std::visit(UnlockEvent{}, m_state);
    }

    State m_state;
};

And here are the events:

struct OpenEvent 
{
    State operator()(const DoorOpened&){ return DoorOpened(); }
    State operator()(const DoorClosed&){ return DoorOpened(); }
    // cannot open locked doors
    State operator()(const DoorLocked&){ return DoorLocked(); } 
};

struct CloseEvent 
{
    State operator()(const DoorOpened&){ return DoorClosed(); }
    State operator()(const DoorClosed&){ return DoorClosed(); }
    State operator()(const DoorLocked&){ return DoorLocked(); }
};

struct LockEvent 
{
    // cannot lock opened doors
    State operator()(const DoorOpened&){ return DoorOpened(); }
    State operator()(const DoorClosed&){ return DoorLocked(); }
    State operator()(const DoorLocked&){ return DoorLocked(); }
};

struct UnlockEvent 
{
    // cannot unlock opened doors
    State operator()(const DoorOpened&){ return DoorOpened(); }
    State operator()(const DoorClosed&){ return DoorClosed(); }
    // unlock
    State operator()(const DoorLocked&){ return DoorClosed(); } 
};

Play with the code using the following example: @Coliru

Would you like to see more?
I extended the code from this section and explored Finite State Machines with std::variant. See the first about "enum style FSM into variant-based" or the second article, which are available for C++ Stories Premium/Patreon members. See all Premium benefits here.

The idea is based on the blog posts:

Polymorphism  

Most of the time in C++ we can safely use runtime polymorphism based on v-table approach. You have a collection of related types - that shares the same interface, and you have a well defined virtual method that can be invoked.

But what if you have “unrelated” types that don’t share the same base class? What if you’d like to quickly add new functionality without changing the code of the supported types?

In such situations, we have a handy pattern of Visitor. I’ve even described in my older post.

With std::variant and std::visit we can build the following example:

class Triangle
{
public:
    void Render()  { std::cout << "Drawing a triangle!\n"; }
};

class Polygon
{
public:
    void Render() { std::cout << "Drawing a polygon!\n"; }
};

class Sphere
{
public:
    void Render() { std::cout << "Drawing a sphere!\n"; }
};

int main() 
{
    std::vector<std::variant<Triangle, Polygon, Sphere>> objects { 
        Polygon(), 
        Triangle(),
        Sphere(),
        Triangle()
    };

    auto CallRender = [](auto& obj) { obj.Render(); };

    for (auto& obj : objects)
        std::visit(CallRender, obj);

}

Play with the code: @Coliru

The output:

Drawing a polygon!
Drawing a triangle!
Drawing a sphere!
Drawing a triangle!

In the above example, I’ve shown only the first case of invoking a method from unrelated types. I wrap all the possible shape types into a single variant and then use a visitor to dispatch the call to the proper type.

If you’d like, for example, to sort objects, then we can write another visitor, that holds some state. And that way you allow to have more functionality without changing the types.

I also have another article where I explain this kind of polymorphism in detail: Runtime Polymorphism with std::variant and std::visit - C++ Stories.

You can explore more about this pattern and its advantages in:

Other Uses  

There are many many more example, see this tweet:

do you have any real-life examples of std::variant?#cpp #cpp17

— Bartlomiej Filipek (@fenbf) 24 maja 2018

You can open this tweet and follow the discussion.

C++20 & C++23 improvements  

After a couple of years with C++20 and C++23 std::variant got a few updates. For example thanks to P2231 implemented against C++20, we can use std::variant in constant expressions:

#include <iostream>
#include <variant>

constexpr std::variant<int, double> foo(int x) {
    std::variant<int, double> oi { x * 0.1 };
    if (x > 10)
        oi = x;
    return oi;
}

int main() {
   constexpr auto dv = foo(1);
   static_assert(std::holds_alternative<double>(dv));
   constexpr auto iv = foo(100);
   static_assert(std::holds_alternative<int>(iv));
}

See at Compiler Explorer.

Additionally there was also an important fix for cases like:

variant<string, bool> x = "abc";             // holds bool !!

Now with A sane std::variant converting constructor you can expect that "abc" string literal will converst into std::string.

And you can look at other features like: std::visit() for classes derived from std::variant and std::variant and std::optional should propagate copy/move triviality.

Wrap Up  

After reading this post, you should be equipped with all the knowledge required to use std::variant in your projects!

While a similar type has been available for years - in the form of boost.variant - I’m happy to see the official STL version. That way we can expect more and more code that uses this handy wrapper type.

Here are the things to remember about std::variant:

  • It holds one of several alternatives in a type-safe way
  • No extra memory allocation is needed. The variant needs the size of the max of the sizes of the alternatives, plus some little extra space for knowing the currently active value.
  • By default, it initializes with the default value of the first alternative
  • You can assess the value by using std::get, std::get_if or by using a form of a visitor.
  • To check the currently active type you can use std::holds_alternative or std::variant::index
  • std::visit is a way to invoke an operation on the currently active type in the variant. It’s a callable object with overloads for all the possible types in the variant(s).
  • Rarely std::variant might get into invalid state, you can check it via valueless_by_exception

I’d like to thank Patrice Roy (@PatriceRoy1), Mandar Kulkarni (@mjkcool) for finding time to do a review of this article!

See also some other posts about std::variant:

Back to you

  • Have you tried std::variant?
  • What’s the most common use case for you?

Let us know in the comments below.