Last Update:
Everything You Need to Know About std::variant from C++17
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 aunion
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 viaholds_alternative
. - You can access the value by using
get_if
orget
(but that might throwbad_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 returnObject
otherwise you assign some error code (as of C++23 you can usestd::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:
- Refactoring with
std::optional
- Using
std::optional
- Error handling and
std::optional
- About
std::variant
- About
std::any
- In place construction for
std::optional
,std::variant
andstd::any
std::string_view
Performance- C++17 string searchers & conversion utilities (this post)
- Working with
std::filesystem
- Even more:
Plus other 110+ articles with the C++17 tag @C++Stories.
Resources about C++17 STL:
- C++17 In Detail by Bartek!
- C++17 - The Complete Guide by Nicolai Josuttis
- C++ Fundamentals Including C++ 17 by Kate Gregory
- Practical C++14 and C++17 Features - by Giovanni Dicanio
- C++17 STL Cookbook by Jacek Galowicz
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.
- if there’s an ambiguity, then you can use a version
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:
- Arne Mertz wrote more about this technique in his recent post: SimplifyC++ - Overload: Build a Variant Visitor on the Fly.
- And here’s the paper for the proposal of
std::overload
: P0051 - C++generic overload function - Also, if you’d like to know how std::visit works underneath, then you might want to check out this post: Variant Visitation by Michael Park
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 int
and 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
orstd::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 viavalueless_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
:
- C++17 has a Visitor Modernes C++
- My take on variant - Jonathan Müller
Back to you
- Have you tried
std::variant
? - What’s the most common use case for you?
Let us know in the comments below.
I've prepared a valuable bonus for you!
Learn all major features of recent C++ Standards on my Reference Cards!
Check it out here: