std::any vs. std::variant (2)

Last time we looked at how to assign a value to an object of type any or variant. In this article we’re going to look at how to extract this value in order to output it or assign it to another variable (of known type). We’ll also consider how to use any and variant with streams.

Considering any first, it is important to recognize that, true to its name, it can wrap any type. Code that dereferences an any object must therefore be very careful in order to remain legal, type-safe, C++. The (only) way to extract the value from an any object is to use a cast, called std::any_cast. This has the same syntax as the familiar static_cast and dynamic_cast, and has slightly more in common with the second of these as it is run-time checked. If the object cannot be cast exactly to the target type (enclosed in triangular brackets), an exception of type std::bad_any_cast is thrown.

Of course, throwing and catching exceptions is a performance hit, so you will probably want to check the return value of the type() member against a typeid() object (from header <typeinfo>):

any a = 1;

if (a.type() == typeid(int)) {
    cout << "a holds an int\n";
    int i = any_cast<int>(a);    // safe, won't throw
    cout << "a = " << i << '\n';
}
else {
    cout << "a does not hold an int\n";
}

Moving onto variant, the way to extract the value is similar as for std::tuple, using std::get with either an index or type:

variant<monostate,int,double> v1 = 1, v2 = 2.3;

if (v1.index() == 1) {
    cout << "v1 holds an int\n";
    int i = get<1>(v1);          // safe, won't throw
    cout << "v1 = " << i << '\n';
}
if (holds_alternative<double>(v2)) {
    cout << "v2 holds a double\n";
    double d = get<double>(v2);  // safe, won't throw
    cout << "v2 = " << d << '\n';
}

A bad use of get will result in an exception of type std::bad_variant_access being thrown, however checking with an if-statement, or switching on the member function index(), ensures this will not happen. (Note that indexing starts from zero, which in this case is the index of std::monostate which would indicate a null-value; void is not a valid parameter type for variant.)

It is possible to use (checked or unchecked) any_cast and get with output streams directly, thus not using an intermediate named variable as shows here. However an exception thrown by an entity put to a stream may cause problems with untidy or truncated output and even stream termination, so this probably should be avoided. (The type of the entity put to a stream must always be known at compile-time, so little is gained by avoiding the use of an intermediate variable.) Another possible scenario is overloading operator << for any or a specific variant type, we’ll get to this in the next article.

Reading from streams must use an intermediate variable, or some method of extracting the any or variant from the input string. The following code indicates an outline of such a method, real code would probably check the input string against multiple regexes:

        any a;
        variant<int,double,string> v;
        string s;
        getline(cin, s);
        if (isalpha(s.front())) {
            a = s; v = s;
        } else if (s.find('.')) {
            a = stod(s); v = stod(s);
        } else {
            a = stoi(s); v = stoi(s);
        }

The most important thing to note is that the string conversion functions stoi() and stod() can be used to assign to both the any and the variant as the type is implied from their return types.

Finally a quick mention of the function template std::make_any which has similar syntax to std::make_unique and std::make_shared; that is, it forwards on the supplied parameters to the constructor of the type specified in angle-brackets. Next in this mini-series we’ll compare these two types again as we look to create a JSON type that can be output to streams.

2 thoughts on “std::any vs. std::variant (2)”

  1. Sorry, I don’t think that’s a prototypical advice of how to use either any or variant.
    It is obvious, that the most prominent advantage of variant against any is that the first is compile-time, the second is run-time.
    Stop giving such bad advice! C++ is not Java!

    Like

    1. I’m afraid I don’t agree. Both any and variant are run-time checked:
      auto s = any_cast<string>(my_any); // can throw std::bad_any_cast
      auto s = get<string>(my_variant); // can throw std::bad_variant_access
      As to giving bad advice, I have tried to explain that variant is basically a struct of a union and an enum, while any holds something like a polymorphic base class pointer. C++ is not Java, so for effective use a basic understanding of implementation is useful.

      Like

Leave a comment