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.
The Basics
In my experience, I haven’t used unions much. But when I did, it was mostly some low-level stuff.
For example for floating point optimization:
unionSuperFloat
{
float f;
int i;
}
intRawMantissa(SuperFloat f)
{
return f.i &((1<<23)-1);
}
intRawExponent(SuperFloat f)
{
return(f.i >>23)&0xFF;
}
Or a convenient access to Vector3
/Vector4
types:
class VECTOR3D
{
public:
// operations, etc...
union
{
float m[3];
struct
{
float x, y, z;
};
};
};
VECTOR3D v;
// same effect
v.m[0]=1.0f;
v.x =1.0f;
As you can see those are useful, but quite a low-level usage, even C-style.
But what if you wanted to use unions more “high level”?
The problem with unions is that 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
.
Here’s a basic demo of what you can do with this new type:
#include<string>
#include<iostream>
#include<variant>
structSampleVisitor
{
voidoperator()(int i)const{
std::cout <<"int: "<< i <<"\n";
}
voidoperator()(float f)const{
std::cout <<"float: "<< f <<"\n";
}
voidoperator()(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(constauto intPtr (std::get_if<int>(&intFloatString)); intPtr)
std::cout <<"int!"<<*intPtr <<"\n";
elseif(constauto 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";
elseif(std::holds_alternative<float>(intFloatString))
std::cout <<"the variant holds a float\n";
elseif(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
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. - 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:
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:
classNotSimple
{
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 @Coliru.
- 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::monospace
. 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 the 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
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:
classMyType
{
public:
MyType(){ std::cout <<"MyType::MyType\n";}
~MyType(){ std::cout <<"MyType::~MyType\n";}
};
classOtherType
{
public:
OtherType(){ std::cout <<"OtherType::OtherType\n";}
~OtherType(){ std::cout <<"OtherType::~OtherType\n";}
};
int main()
{
std::variant<MyType,OtherType> v;
v =OtherType();
return0;
}
This will produce the output:
MyType::MyType
OtherType::OtherType
MyType::~MyType
OtherType::~OtherType
OtherType::~OtherType
http://coliru.stacked-crooked.com/a/5951ae413e6f731b
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(constauto 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<classVisitor,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:
autoPrintVisitor=[](constauto& 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:
autoPrintVisitor=[](constauto& t){ std::cout << t <<"\n";};
autoTwiceMoreVisitor=[](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 ()
:
structMultiplyVisitor
{
float mFactor;
MultiplyVisitor(float factor): mFactor(factor){}
voidoperator()(int& i)const{
i *=static_cast<int>(mFactor);
}
voidoperator()(float& f)const{
f *= mFactor;
}
voidoperator()(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
(
[](constint& i){ PRINT("int: "+ i);},
[](const std::string& s){ PRINT("it's a string: "+ s);},
[](constfloat& 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...{usingTs::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
Recently 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
classThrowingClass
{
public:
explicitThrowingClass(int i){if(i ==0)throwint(10);}
operatorint(){throwint(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";
}
return0;
}
Play with the code @Coliru
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
.
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:
sizeofstring: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 any extra 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:
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 the future (see more about std::expected
here).
enumclassErrorCode
{
Ok,
SystemError,
IoError,
NetworkError
};
std::variant<std::string,ErrorCode>FetchNameFromNetwork(int i)
{
if(i ==0)
returnErrorCode::SystemError;
if(i ==1)
returnErrorCode::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";
return0;
}
Play with the example @Coliru
In the example, I’m returning ErrorCode
or a valid type - in this case, a string.
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.
usingDoublePair= std::pair<double,double>
usingEquationRoots= std::variant<DoublePair,double, std::monostate>;
EquationRootsFindRoots(double a,double b,double c)
{
auto d = b*b-4*a*c;
if(d >0.0)
{
auto p = sqrt(d)/(2*a);
return std::make_pair(-b + p,-b - p);
}
elseif(d ==0.0)
return(-1*b)/(2*a);
return std::monostate();
}
structRootPrinterVisitor
{
voidoperator()(constDoublePair>& arg)
{
std::cout <<"2 roots: "<< arg.first <<""<< arg.second <<'\n';
}
voidoperator()(double arg)
{
std::cout <<"1 root: "<< arg <<'\n';
}
voidoperator()(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));
}
Play with the code @Coliru
The code is based on Pattern matching in C++17 with std::variant, std::monostate and std::visit
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)
- …
So we can build a variant that will hold all the possible options.
Here’s a simple version with int
and string
:
classCmdLine
{
public:
usingArg= std::variant<int, std::string>;
private:
std::map<std::string,Arg> mParsedArgs;
public:
explicitCmdLine(int argc,char** argv){ParseArgs(argc, argv);}
// ...
};
And the parsing code:
CmdLine::ArgTryParseString(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;
}
voidCmdLine::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:
structDoorState
{
structDoorOpened{};
structDoorClosed{};
structDoorLocked{};
usingState= 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:
structOpenEvent
{
Stateoperator()(constDoorOpened&){returnDoorOpened();}
Stateoperator()(constDoorClosed&){returnDoorOpened();}
// cannot open locked doors
Stateoperator()(constDoorLocked&){returnDoorLocked();}
};
structCloseEvent
{
Stateoperator()(constDoorOpened&){returnDoorClosed();}
Stateoperator()(constDoorClosed&){returnDoorClosed();}
Stateoperator()(constDoorLocked&){returnDoorLocked();}
};
structLockEvent
{
// cannot lock opened doors
Stateoperator()(constDoorOpened&){returnDoorOpened();}
Stateoperator()(constDoorClosed&){returnDoorLocked();}
Stateoperator()(constDoorLocked&){returnDoorLocked();}
};
structUnlockEvent
{
// cannot unlock opened doors
Stateoperator()(constDoorOpened&){returnDoorOpened();}
Stateoperator()(constDoorClosed&){returnDoorClosed();}
// unlock
Stateoperator()(constDoorLocked&){returnDoorClosed();}
};
Play with the code using the following example: @Coliru
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:
classTriangle
{
public:
voidRender(){ std::cout <<"Drawing a triangle!\n";}
};
classPolygon
{
public:
voidRender(){ std::cout <<"Drawing a polygon!\n";}
};
classSphere
{
public:
voidRender(){ std::cout <<"Drawing a sphere!\n";}
};
int main()
{
std::vector<std::variant<Triangle,Polygon,Sphere>> objects {
Polygon(),
Triangle(),
Sphere(),
Triangle()
};
autoCallRender=[](auto& obj){ obj.Render();};
for(auto& obj : objects)
std::visit(CallRender, obj);
}
Play with the code: @Coliru
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.
You can explore more about this pattern and its advantages in:
Another polymorphism | Andrzej’s C++ blog and in Inheritance vs std::variant, C++ Truths
Sorry for a little interruption in the flow :)
I've prepared a little bonus if you're interested in C++17, check it out here:
Other Uses
There are many many more example, see this tweet:
You can open this tweet and follow the discussion.
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
: