微型模板库:variant






4.88/5 (13投票s)
2004 年 2 月 24 日
3分钟阅读

86167

697
一篇关于如何实现和使用 variant<> 的文章。Variant 对于创建异构容器等非常有用。
引言
我的标准警告:不要尝试使用 MSVC v6.0/v7.0 编译此项目。此项目需要一个兼容的编译器。MSVC v7.1 或 GCC v3.2.3 都可以正常工作。在关于 typelist 的文章中,我简要提到了 variant。在这里,我想更详细地讨论它。本文中的代码仅用于演示基本概念。实际实现可以在TTL中找到。在 C++ 中,不允许在 union
中包含非 POD 数据类型。例如,以下代码将无法编译。
struct my_type { int x; my_type() : x(0) {} virtual ~my_type(); }; union my_union { my_type a; double b; };
variant
模板解决了这个问题,并增加了许多其他很酷的功能。variant
的一个应用是异构容器。
typedef variant< my_type, double > mv; main() { my_type a; std::vector< mv > v; v.push_back(2.3); //add double v.push_back(a); //add my_type }
variant 的语义受到 boost::variant
的启发。在 A. Alexandrescu 的《Modern C++ Design》一书中可以找到关于 variant 的精彩讨论。
实现
variant
具有可变数量的模板参数。
variant<int> variant<int, double> ...为了支持可变数量的模板参数,我们使用了在typelist 文章中提出的技术。
主要的 variant 实现思路是:
variant
处于奇异状态。
variant 的伪代码如下:
template < typename T1, typename T2, ... > struct variant { //list of user types typedef meta::typelist< T1, T2,...> types; //initialization variant() : p_(0) {} template< typename T > variant( const T& d ) : p_(0) { //find the index of this type in the variant typelist which_ = find_type<T>::value; //in place construction 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: //data_holder is a wrapper for the user types 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) {} }; //list of data holder types typedef meta::typelist<data_holder<T1>, data_holder<T2>,...> holder_types; //reserve enough space to hold the largest type char buffer_[find_largest_type<holder_types>::value]; //pointer to the controlled object data_holder_base *p_; //object type index int which_; };请注意:上面的代码只是伪代码。实际实现更复杂。可能需要写一整本书来描述所有细节。我更愿意谈谈如何在实践中使用
variant
。
使用 variant
让我们考虑一个简单的例子:typedef variant<int, double> my_variant;这个
typedef
定义了一种数据类型,它可以包含一个 double
或 int
变量。假设我们需要编写一个函数,根据变量的数据类型对 my_variant
执行某些操作。我们可以使用简单的 switch/case 语句。void f( my_variant& v ) { int n; double x; switch( v.which() ) { case 0: //int variable n = get<int>(v); //do something with int; break; case 1: //double variable x = get<double>(v); //do something with the double; break; } };
正如您所见,get<>
函数可用于从 variant<>
中检索类型化数据。显然,这个 switch 语句很丑陋且不太灵活。函数 f()
必须知道 my_variant
中的类型索引。解决这些问题的一种方法是利用 Gof 访问者模式的思想。
operator()
。operator()
。TTL 的 variant
具有 apply_visitor<>
函数,该函数负责调用适当的访问者 operator()
。使用此技术,可以如下实现上述示例。typedef variant<int, double> my_variant; struct visitor { void operator()(int n) { //do something with the int; ... } void operator()(double x) { //do something with the double; ... } //ignore any other types template< typename T > void operator()( T d ) { } }; my_variant var; visitor vis; apply_visitor(var, vis);
我认为这样看起来好多了,我们不必担心类型索引或任何其他类型的标识符。apply_visitor()
函数实现在TTL中。apply_visitor
执行以下操作:
operator()
。
variant 的另一个有趣实现是事件分发。假设我们有一个事件源,可以生成多种事件类型。为了简单起见,事件类型是 int
和 double
。我们可以这样定义事件类型:
typedef variant< int, double > event;现在我们需要一种方法来指定一个回调函数,事件源将调用该函数来通知客户端或观察者。使用通用函数对象(参见TTL:实现函数对象)定义回调函数很方便。
typedef function< void (event&) > callback;现在我们可以将所有内容组合在一起:
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; ... //generate int event ev = 1; cb_( ev ); .... //generate double event ev = 2.3; cb_( ev ); } }; //define our event vistor struct event_visitor { //process int event void operator()(int n) { cout << "got int:" << n; } //process double event void operator()(double n) { cout << "got double:" << n; } //ignore any other events 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(); }您可以在
samples/test
文件夹中找到一个工作示例。扩展此示例来实现完整的 Observer 模式实现而不进行任何多态继承并不难。结果是,在编译时执行“强”类型检查。