65.9K
CodeProject 正在变化。 阅读更多。
Home

微型模板库:variant

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.88/5 (13投票s)

2004 年 2 月 24 日

3分钟阅读

viewsIcon

86167

downloadIcon

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 模板参数转换为 typelist。
  • 编译时:使用 typelist,找到最大的元素并为此元素大小保留缓冲区。
  • 运行时:使用保留的缓冲区原地构造受控对象。
  • 运行时:保存当前实例类型的索引。
  • 运行时:如果未初始化,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 定义了一种数据类型,它可以包含一个 doubleint 变量。假设我们需要编写一个函数,根据变量的数据类型对 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 访问者模式的思想。

  • 定义一个 variant 访问者函数对象,它为 variant 中的所有类型都具有单独的 operator()
  • 当应用于 variant 时,会调用适当的访问者的 operator()

    TTLvariant 具有 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 执行以下操作:

  • 查找 which_ 成员标识的类型;
  • 将对象的指针转换为该类型;
  • 将转换后的指针传递给用户提供的访问者。
  • 编译器会自动选择适当的 operator()

    variant 的另一个有趣实现是事件分发。假设我们有一个事件源,可以生成多种事件类型。为了简单起见,事件类型是 intdouble。我们可以这样定义事件类型:

    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 模式实现而不进行任何多态继承并不难。结果是,在编译时执行“强”类型检查。
  • © . All rights reserved.