Introduction
My standard warning: Don't try to compile this project with MSVC v6.0/v7.0. This project requires a compliant compiler. MSVC v7.1 or GCC v3.2.3 will work just fine. In the article about typelist, I briefly mentioned variant. Here, I'd like to discuss it in more detail. The code in this article is to demonstrate the basic ideas only. The actual implementation can be found in TTL. In C++, it is not legal to have non-POD data types in union
. For example the following code won't compile.
struct my_type
{
int x;
my_type() : x(0) {}
virtual ~my_type();
};
union my_union
{
my_type a;
double b;
};
The variant
template solves this problem and adds a lot of other cool features. One application of variant
is heterogeneous containers.
typedef variant< my_type, double > mv;
main()
{
my_type a;
std::vector< mv > v;
v.push_back(2.3);
v.push_back(a);
}
The variant semantic was inspired by boost::variant
. A very good discussion about variant can be found in "Modern C++ Design" by A. Alexandrescu.
Implementation
variant
has a variable number of template parameters.
variant<int>
variant<int, double>
...
To support variable numbers of template parameters, we use the technique that was suggested in the
typelist article.
The main variant implementation ideas are:
Compile-time: convert variant template parameters to typelist.
Compile-time: using the typelist, find the largest element and reserve a buffer of this element size.
Run-time: use the reserved buffer to construct controlled objects in place.
Run-time: keep the index of the current instance type.
Run-time: if not initialized, variant
is in a singular state.
The variant pseudo-code looks like this:
template < typename T1, typename T2, ... >
struct variant
{
typedef meta::typelist< T1, T2,...> types;
variant() : p_(0) {}
template< typename T >
variant( const T& d ) : p_(0)
{
which_ = find_type<T>::value;
p_ = new(buffer_) data_holder<T>(d);
}
virtual ~variant() { destroy(); }
inline int which() const { return which_; }
inline bool is_valid() const { return p_ != 0; }
void destroy()
{
if(!is_valid()) return;
p_->~data_holder_base();
p_ = 0;
}
private:
struct data_holder_base
{
virtual ~data_holder_base() {}
};
temlate< typename T >
struct data_holder : data_holder_base
{
T d_;
data_holder( const T& d ) : d_(d) {}
};
typedef meta::typelist<data_holder<T1>, data_holder<T2>,...> holder_types;
char buffer_[find_largest_type<holder_types>::value];
data_holder_base *p_;
int which_;
};
Please note: the above code is only a pseudo-code. The actual implementation is more complex. It might take a small book to describe all the details. I'd rather talk about how to use variant
in practice.
Using variant
Let's consider a simple example: typedef variant<int, double> my_variant;
This typedef
defines a data type that can contain a double
or int
variable. Suppose that we need to write a function that does something with my_variant
depending on the variable data type. We can use a simple switch/case statement. void f( my_variant& v )
{
int n;
double x;
switch( v.which() )
{
case 0:
n = get<int>(v);
break;
case 1:
x = get<double>(v);
break;
}
};
As you can see, the get<>
function can be used to retrieve the typed data from variant<>
. Obviously this switch statement is ugly and not very flexible. The function f()
has to know the type indexes in my_variant
. One way to solve these problems is to utilize the Gof visitor pattern ideas.
Define a variant visitor functor that has a separate operator()
for all types in variant.
When applied to the variant, an appropriate visitor's operator()
is called.
TTL's variant
has the apply_visitor<>
function that takes care of calling the appropriate visitor operator()
. Using this technique, the above example can be implemented as follows. typedef variant<int, double> my_variant;
struct visitor
{
void operator()(int n)
{
...
}
void operator()(double x)
{
...
}
template< typename T >
void operator()( T d )
{
}
};
my_variant var;
visitor vis;
apply_visitor(var, vis);
I think that it looks much nicer and we don't have to worry about type indexes or any other type identifiers for the same matter. The apply_visitor()
function is implemented in TTL. apply_visitor
does the following:
finds what type is identified by the which_ member;
casts the object's pointer to this type;
passes the casted pointer to the user supplied visitor.
the compiler automatically selects the appropriate operator()
.
Another interesting implementation of variant is event dispatching. Suppose we have an event source that can generate multiple event types. For the simplicity sake, the event types are int
and double
. We can define the event type as following:
typedef variant< int, double > event;
Now we need a way to specify a callback function that will be called by the event source to notify the client or observer. It is convenient to define callbacks using generic functors (see, TTL:implementing functors)
typedef function< void (event&) > callback;
Now we can put everything together: typedef variant< int, double > event;
typedef function< void (event&) > callback;
struct event_source
{
callback cb_
event_source( callback& cb ) : cb_(cb) {}
void do_something()
{
event ev;
...
ev = 1;
cb_( ev );
....
ev = 2.3;
cb_( ev );
}
};
struct event_visitor
{
void operator()(int n)
{
cout << "got int:" << n;
}
void operator()(double n)
{
cout << "got double:" << n;
}
template< typename T >
void operator()( T d )
{
}
}
void my_callback( event& e )
{
event_visitor vistor;
apply_visitor(e, vistor);
}
main()
{
event_source src(my_callback);
src.do_something();
}
You can find a working example in the samples/test
folder. It is not hard to extend this example to a complete Observer pattern implementation w/o any polymorphic inheritances. As a result, a "strong" type checking is performed at compile time.