37

Everything You Need to Know About std::variant from C++17

 4 years ago
source link: https://www.tuicool.com/articles/EnANFnA
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

I3mMjqu.png!web

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 type from C++17 works and where it might be useful.

    • A Functional Background
  • std::variant Creation
  • Accessing the Stored Value
  • Visitors for std::variant
  • Other std::variant Operations
  • Exception Safety Guarantees
  • Performance & Memory Considerations
  • Migration From boost::variant
  • Examples of std::variant
    • Computing Roots of an Equation
    • Parsing a Command Line
    • Parsing a Config File

Last update: 10th June 2019

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 hacks:

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; 
}

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.

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: Don’t use a union for type punning

ReasonIt 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.

But what if you want to use unions for "high-er level" types?

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>

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

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:

A77Ffua.png!web

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 @Coliru .

  • By default, a variant object is initialized with the first type,
    std::monostate
    
  • 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 the Values

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

emplace
get

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:

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 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.

Type Conversions

Sometimes you also have to pay attention to type Conversions!

Have a look at the following code:

std::variant<std::string, bool, int> var { 42 };
var = "Hello World";

Do you know what’s the current state of the variant after v = "Hello World"; ? Is that std::string ?

Thanks to one comment from r/programming user (see link here ) we can examine the behaviour.

"Hello World" has a type of const char* and the C++ compiler will convert that to bool and not to std::string ! See c++ - boost::variant - why is “const char*” converted to “bool”? - Stack Overflow .

You can fix the code by using the actual types so:

var = std::string("Hello World");
// or
using namespace std::string_literals;
var = "Hello World"s;

In short: use the actual types you store inside a variant so that no hidden and unwanted conversions are invoked.

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 in a variant, you cannot write:

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;

As you see, above we have a compile time error. You have to use helper functions to access the value.

The first option is 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

If you like to know more about the "magic" behind those 2 lines of code that declare overload, you can check out my other blog post: 2 Lines Of Code and 3 C++17 Features - The overload Pattern .

AThe example showed only how to use std::visit with a single variant, but if you want pass a couple of them, then please read my other blog post: How To Use std::visit With Multiple Variants .

Other good resources:

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

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 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.

As for the compile time you might want to eheck out blog post (already mentioned) by Michael Park: Variant Visitation V2 who discusses various techniques to reduce genenerated code. He also shows some interesting compilation benchmarks.

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 the future ( see more about std::expected here ).

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.

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)
{
    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);
    }
    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) 
    { 
        std::cout << "1 root: " << 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));
}

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 :

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:

FNbqIfb.png!web

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

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 describedin 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

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:

Download a free copy of C++17 Language Ref Card!

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.

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 :


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK