C++ Boost

boost::variant

Tutorial



The following sample program illustrates a typical usage of variant. Additional sample programs may be found here.

First variant program

Let's suppose you need to implement an object factory function, create. Based on an integer argument to the function, create must construct and return an object of one of the following types: std::string, char*, and int. The relationship between the integer argument, index, and the returned type is specified as follows:

    2000 < index          ->  char*
    1000 < index <= 2000  ->  std::string
      else                ->  int

Typical implementations would probably designate of a virtual base class, returning objects of the derived types through a pointer to the base class. However, since none of the desired types may derive from the base class, the programmer would need to write "adapter" classes derived from the common base to supply the semantics of the various concrete types (i.e, std::string, int, and char*). Clients of the factory function would then check the result using dynamic_cast to determine which concrete type had in fact been created.

Other implementations might leverage the Boost any class, thus avoiding the hassle of defining and implementing the virtual base class and the derived adapter classes. However, this solution still does not avoid the problem of needing to manually check the result using any_cast.

On the other hand, by returning an object of type variant< std::string, char*, int > from the factory function, the resultant code is clear and straight-forward:

#include <iostream>
#include <string>
#include "boost/variant.hpp"

using std::string;
using boost::variant;

typedef variant<string,char*,int> create_result;

//
// create, our factory function
//
create_result create(int index)
{
   // Return char* if (index > 2000):
   if (index > 2000) return "JKLM";

   // Return std::string if (1000 < index <= 2000):
   if (index > 1000) return string("QRST");

   // Otherwise, return int:
   return 15;
}

//
// printer, a visitor that prints to std::cout
//
struct printer : boost::static_visitor<>
{
   template <typename T>
   void operator()(const T& t)
   {
      std::cout << "operand: " << t << std::endl;
   }
};


int main(int , char* [] )
{
   printer a_printer;
   create_result result;

   // First, create a char*...
   result = create(2500);

   // ...and print result by applying print visitor to it:
   boost::apply_visitor(a_printer, result); // Output: "operand: JKLM"

   // Now, create a std::string and print it:
   result = create(1500);
   boost::apply_visitor(a_printer, result); // Output: "operand: QRST"

   // Finally, create an int and print it:
   result = create(5);
   boost::apply_visitor(a_printer, result);  // Output: "15"

   return 0;
}

Instantiation

A concrete variant instantiation can use either of these two equivalent notations:

typedef boost::variant<T1,T2, ... Tn> variant_type;
typedef boost::variant<boost::mpl::vector<T1,T2, ... Tn> > variant_type;

Where the types T1, T2, ... Tn must model the BoundedType concept. An instance of variant_type is capable of holding a value of any of these types.

Comments:

Examples:

   boost::variant<int, char, double> v1; // OK

   variant<int, char, double, char> v2;  // Error: char appears twice
   variant<float> v3;                    // Error: At least two types must be specified
   variant<char, char* const, void*> v4; // Error: top-level const types are illegal

   variant<char, const char*, void*> v5; // OK - const char* is not a top-level const type

If one of these types is incomplete at the instantiation point, it must be enclosed inside an incomplete<> wrapper, as shown below:

   struct jas1; // Forward declaration of jas1 (Just Another Struct)

   variant<jas1, double, std::string> v6; // Error: jas1 is incomplete
   struct jas1 { ... };

   struct jas2; // Forward declaration of jas2
   variant<incomplete<jas2>, double, std::string> v7; // OK -
                                                      // incomplete<> is used for incomplete types
   struct jas2 { ... };

Value semantics

Once a variant object has been created its value may be changed by variant's assingment operator. The right-hand side value of the assignment is converted to the closet possible type of the assigned variant's set of types, by using overload resolution rules. Naturally, If there is an exact match, no convesion is applied, and the right-hand side value is copied as-is. On the other hand, if no conversion exists, or, if the conversion is ambiguous, a compiler error is triggered.

The assignment rules (mentioned above) also apply when a variant object is initalized. Hence, the rest of this section will refer to assignment and initialization interchangeably.

   variant<int, char, double> v1;
   v1 = 5.9; // double -> double (no conversion is needed)
   v1 = 3;   // int -> int                - " -
   v1 = 'x'; // char -> char              - " -

   v1 = short(5);           // short -> int
   v1 = 5.9f;               // float -> double
   v1 = static_cast<unsigned char>('x'); // unsigned char -> int


   v1 = string("abc");    // Error! no implicit conversion from
                          //   string to int/char/double

   v1 = static_cast<long double>(4.0); // Error! (ambiguity):
                          //      long double -> double conversion
                          //      clashes with long double -> int conversion


   variant<std::string, double, int> v2; // Default construction.
                                         // Use default value of first type,
                                         //    i.e: v2 = std::string()


   struct class1
   {
      class1(const char* s = 0) { .. }
      ..
   };

   struct class2
   {
      class2(const char* s) { .. }
      ..
   };

   variant<class1, double> v3;
   v3 = 5;               // int -> double
   v3 = class1("abc");   // class1 -> class1
   v3 = "abc";           // const char* -> class1

   variant<class1, class2> v4;
   v4 = class1("text1"); // class1 -> class1 
   v4 = class2("text2"); // class2 -> class2      
   v4 = "text3";         // Error! (ambiguity):
                         //     class1 clashes with class2

Copy assignment: When a variant object is assigned to another variant object - which is of the same concrete type - the assignee becomes an exact duplicate of the assigned variant:

   variant<int, char, double> v1, v2;
   v1 = 5.9; // v1 = double(5.9)
   v2 = v1;  // v2 = double(5.9)

   v1 = 3;   // v1 = int(3)
   v2 = v1;  // v2 = int(3)

   v1 = short(5); // short -> int, hence: v1 = int(5) 
   v2 = v1;       // v2 = int(5)

   v1 = 5.7f;     // float -> double, hence: v1 = double(5.7)
   v2 = v1;       // v2 = double(5.7)

Variant-to-variant assignment: Consider this case:

   typedef variant<RR, SS, TT, .. > rhs_type;
   rhs_type src;

   typedef variant<XX, YY, ZZ, .. > lhs_type;
   lhs_type trg;

   trg = src; // Variant to variant assignment:
              // trg and src are two variants of different types

What will trg's value be, following such an assignment?

    typedef variant<int, std::string> variant_type;
    variant_type a;

    variant<int, double, std::string> b;
    variant<variant_type, int, double, std::string> c;

    a = "hello";  // char* converted to std::string

    b = a;        // b <- value of a, so b holds an std::string (Case 1)

    c = a;        // c <- a, so c holds a variant_type object holding
                  // an std::string (Case 2)

Note that a variant-to-variant assignment will fail - at compile time - if one of src's bounded types cannot be assigned to trg (due to ambiguity/lack of conversion).
For instance:

    variant<int, std::string> a;
    variant<int, const char*> b;

    a = b; // OK: int->int or const char* -> std::string

    b = a; // Error!
           // std::string is not implicitly convertible to either int
           // or const char*, so the compiler will break on this line

Functor-based visitation

The visitation facility, implemented via apply_visitor(), is the primary mechanism thru which client code can gain access to a variant-held value.

Let us consider the following piece of code.

   boost::variant<XX, YY, ZZ, ... > a_variant;

   struct visitor_type { ... };
   visitor_type visitor;

   boost::apply_visitor(visitor, a_variant)

In this sample, visitor_type must be a Visitor of a_variant. These requirements may be informally defined as: visitor_type must be a unary function object that is capable of accepting a value of any of a_variant's BoundedTypes. If visitor_type fails to meet these requirements, (i.e: it cannot accept one, or more, of the types in a_variant's set of types) a compiler error is triggered.

The 'visit' begins when apply_visitor() is called: apply_visitor(visitor, a_variant) passes a_variant's currently held value to visitor. From visitor's standpoint, its function-call operator is called with whatever value a_variant is holding. This gives visitor a chance to inspect/modify a_variant's value.

The following snip demonstrates the visitation facility using a concrete visitor class, print_int_float_visitor. The code will print "Char value: 'x'" and "Int value: 77" to std::cout:

   struct print_int_float_visitor
       :    boost::static_visitor<void> // Return type is void,
                                        // so derive from static_visitor<void>
   {
      // Handle int values
      void operator()(int x) const
      {
         std::cout << "Int value: " << x << std::endl;
      }

      // Handle char values
      void operator()(float x) const
      {
         std::cout << "Float value: " << x << std::endl;
      }
   };
   .
   .
   variant<int,float> var = 53.22f;

   //Apply print_int_float_visitor to var.
   apply_visitor(print_int_float_visitor(), var); // Output: "Float value: 53.22"

   var = 77;
   apply_visitor(print_int_float_visitor(), var); // Output: "Int value: 77"

Note how print_int_char_visitor specifies its return type by publicly deriving from boost::static_visitor<void>.

The visitor class in the following snip, multiply_by_two, returns an int value, which is twice its operand.
This value is retuned to the caller of boost::apply_visitor():

   struct multiply_by_two
       :    boost::static_visitor<int> // Return type is int,
                                       // so derive from static_visitor<int>
   {
      template<typename T>
      int operator()(T t) const
      {
         return int(2 * t);
      }
   };
   .
   .
   variant<int, short, char> a = 9;
   int result = apply_visitor(multiply_by_two(), a);
   std::cout << "Result = " << result << std::endl; //Output: "Result = 18"

If a visitor offers several overloads of operator(), overload resolution rules are applied to choose the correct form. If a visitor cannot accept one of the types a variant may hold (due to ambiguity/lack of conversion), a comiler error is generated.

   struct print_int_char : boost::static_visitor<void>
   {
      // Handler *A*
      void operator()(int f) const
      {
         std::cout << "Int value: " << f << std::endl;
      }

      // Handler *B*
      void operator()(char c) const
      {
         std::cout << "Char value: '" << c << '\''
            << std::endl;
      }
   };
   .
   .
   variant<short, char> a = static_cast<short>(88);
   apply_visitor(print_int_char(), a);   // Output: "Int value: 88.0"
                                         //   (int value intercepted by handler *A*)

   a = 'x';                              // a = 'x'
   apply_visitor(print_int_char(), a);   // Output: "Char value: 'x'"

   variant<int, char, void*> b = 88;     // b = int(88)
   apply_visitor(print_int_char(), b);   // Error! -
                                         //    void* cannot be handled by neither
                                         //    handler (*A* or *B*)

"Catch-all" behavior can be achieved by supplying a templated form of operator():

   struct ignore_non_ints_visitor : boost::static_visitor<void>
   {
      void operator()(int t) const
      {
         std::cout << "Current value: " << t << std::endl;
      }

      template<typename T>
      void operator()(const T& t) const
      {
         // Catch all other types:
         std::cout << "Ignore me" << std::endl;
      }
   };
   .
   .
   variant<int, double, std::string> a = "abcde";
   apply_visitor(ignore_non_ints_visitor(), a); //Output: "Ignore me"

   a = 22.24;
   apply_visitor(ignore_non_ints_visitor(), a); //Output: "Ignore me"

   a = 8;
   apply_visitor(ignore_non_ints_visitor(), a); //Output: "Current value: 8"

A visitor may accept its operand using a "pass-by-reference" parameter passing scheme. This allows a visitor to mutate the variant-held value:

   struct first_capital : boost::static_visitor<void>
   {
       void operator()(std::string& str) const
       {
           str[0] = toupper(str[0]);
       }

       void operator()(char* str) const
       {
           *str = toupper(*str);
       }
   };

   struct printer : boost::static_visitor<void>
   {
       template<typename T>
       void operator()(const T& t) const
       {
           std::cout << t << std::endl;
       }
   };
   .
   .
   variant<std::string, char*> a = std::string("abcde");
   apply_visitor(printer(), a); //Output: "abcde"

   apply_visitor(first_capital(), a); //Invoke the mutating visitor
                                      //(capitalizes the first letter)

   apply_visitor(printer(), a); //Output: "Abcde"

The last sample shows persistency of visitors. Every visitor is actually a function object, so its data members can be used to keep persistent data. The int_accumulator visitor uses total_ to keep track of the total sum of its operands:

   struct int_accumulator : boost::static_visitor<void>
   {
       int_accumulator() : total_(0) { }

       void operator()(int x)
       {
           total_ += x;
       }

       int total_;
   } ;
   .
   .
   int_accumulator adder;
   variant<int, short, char> a = 15;
   apply_visitor(adder, a); //adder.total_ += 15

   a = short(9);
   apply_visitor(adder, a); //adder.total_ += 9

   std::cout << "Total = " << adder.total_ << std::endl; //Output: "Total = 24"

Note: When boost::apply_visitor() is used, the compiler verifies that the specified visitor type is a valid visitor with respect to the relevant variant type. If the visitor fails to meet the Visitor requirements, a compiler error is fired. This allows programming errors to be detected at compile-time rather than run-time, thus enhancing code safety and stability.


Revised 14 February 2003

© Copyright Eric Friedman and Itay Maman 2002-2003. All rights reserved.

Permission to use, copy, modify, distribute and sell this software and its documentation for any purpose is hereby granted without fee, provided that the above copyright notice appear in all copies and that both that copyright notice and this permission notice appear in supporting documentation. Eric Friedman and Itay Maman make no representations about the suitability of this software for any purpose. It is provided "as is" without express or implied warranty.