|
boost::variantTutorial |
The following sample program illustrates a typical usage of variant
.
Additional sample programs may be found here.
variant
programLet'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; }
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:
boost::mpl::vector
can be
replaced by any other mpl sequence
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 { ... };
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?
rhs_type
is not part of lhs_type
's
set of types, then src
's
value is assigned to trg
, using the rules discussed throughout
this section. rhs_type
does appear on lhs_type
's
set of types, then src
itself will be assigned into trg
,
turning trg
into a variant holding a variant 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
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.