【问题标题】:visitor template for boost::variantboost::variant 的访问者模板
【发布时间】:2011-04-06 10:55:27
【问题描述】:

我想使用 boost.variant<T0,T1,T2> 作为模板“访问者”类的参数,该类将根据 boost.variant 访问者机制的要求提供访问者运算符,在这种情况下,所有返回 void 即

void operator()(T0 value);
void operator()(T1 value);
void operator()(T2 value);

该模板还将为每个类型 T0... 在变体中具有相应的虚函数,默认情况下该虚函数不执行任何操作。用户可以从模板类继承并只重新定义他感兴趣的那些虚函数。这类似于众所周知的“模板方法”模式。 我能想出的唯一解决方案是将 boost::variant 和相关访问者包装在一个模板中,并通过 typedefs 访问它们。这工作正常,但感觉有点笨拙。代码如下:

#include "boost/variant.hpp"

//create specializations of VariantWrapper for different numbers of variants - 
//just show a template for a variant with three types here. 
//variadic template parameter list would be even better! 

template<typename T0, typename T1, typename T2>
struct VariantWrapper
{
    //the type for the variant
    typedef boost::variant<T0,T1,T2> VariantType;

    //The visitor class for this variant
    struct Visitor : public boost::static_visitor<>
    {
        void operator()(T0 value)
        {
            Process(value);
        }
        void operator()(T1 value)
        {
            Process(value);
        }
        void operator()(T2 value)
        {
            Process(value);
        }
        virtual void Process(T0 val){/*do nothing */}
        virtual void Process(T1 val){/*do nothing */}
        virtual void Process(T2 val){/*do nothing */}
    protected:
        Visitor(){}
    };

    typedef Visitor VisitorType;
private:
    VariantWrapper(){}
    };

然后按如下方式使用该类:

typedef VariantWapper<bool,int,double> VariantWrapperType;
typedef VariantWrapperType::VariantType VariantType;
typedef VariantWrapperType::VisitorType VisitorType;

struct Visitor : public VisitorType
{
    void Process(bool val){/*do something*/}
    void Process(int val){/*do something*/}
    /* this class is not interested in the double value */
};

VariantType data(true);
apply_visitor(Visitor(),data);

正如我所说,这似乎工作正常,但如果我不必创建一个特殊的包装类来将变体和访问者联系在一起,我会更喜欢它。我希望能够直接使用 boost.variant 来实例化模板访问者类。我看过使用类型参数、非类型参数和模板模板参数,但似乎没有任何建议。我想做的事是不可能的吗?我可能遗漏了一些东西,如果有人对此有任何意见,我将不胜感激。

【问题讨论】:

    标签: c++ templates boost variant boost-variant


    【解决方案1】:

    带有 Boost Variant 和虚拟调度的代码有点可疑。特别是考虑到您知道在编译时对处理什么感兴趣,并且绝对没有必要在运行时创建虚拟表来实现您的目标。

    我建议您使用partial template specialization。因此,有一个默认模板方法可以接受变体中的任何类型并且什么都不做。对于您感兴趣的那些类型,只需专门化模板即可。

    这是一个例子。我们有三种类型 - Foo、Bar 和 War。我们只对最后两种类型感兴趣,并对它们进行了专门研究。所以 Foo 被忽略了。

    #include <iostream>
    #include <boost/variant.hpp>
    
    using namespace std;
    using namespace boost;
    
    struct Foo {};
    struct Bar {};
    struct War {};
    
    typedef variant<Foo, Bar, War> Guess;
    
    struct Guesstimator : public boost::static_visitor<void>
    {
        template <typename T>
        void operator () (T) const
        {
        }
    };
    
    template <>
    inline void
    Guesstimator::operator () <Bar> (Bar) const
    {
        cout << "Let's go to a pub!" << endl;
    }
    
    template <>
    inline void
    Guesstimator::operator () <War> (War) const
    {
        cout << "Make love, not war!" << endl;
    }
    

    下面是一个简单的用法示例:

    int
    main ()
    {
        Guess monday;
        apply_visitor (Guesstimator (), monday);
    
        War war;
        Guess ww2 (war);
        apply_visitor (Guesstimator (), ww2);
    
        Bar irishPub;
        Guess friday (irishPub);
        apply_visitor (Guesstimator (), friday);
    }
    

    这个程序的输出将是:

    Make love, not war!
    Let's go to a pub!
    

    这是另一种解决方案。我们创建一个默认访问者,忽略所有内容,除了您在类型列表中指定的内容。这并不方便,因为您必须两次指定类型列表 - 一次在类型列表中,然后在每个处理方法(运算符)中。此外,事实上,通用模板将继承您的访问者。但是,我们开始吧:

    #include <cstddef>
    #include <iostream>
    #include <boost/variant.hpp>
    #include <boost/mpl/vector.hpp>
    #include <boost/mpl/contains.hpp>
    #include <boost/utility/enable_if.hpp>
    
    // Generic visitor that does magical dispatching of
    // types and delegates passes down to your visitor only
    // those types specified in a type list.
    template <typename Visitor, typename TypeList>
    struct picky_visitor :
        public boost::static_visitor<void>,
        public Visitor
    {
        template <typename T>
        inline void
        operator () (T v, typename boost::enable_if< typename boost::mpl::contains< TypeList, T >::type >::type *dummy = NULL) const
        {
            Visitor::operator () (v);
        }
    
        template <typename T>
        inline void
        operator () (T v, typename boost::disable_if<typename boost::mpl::contains< TypeList, T >::type >::type *dummy = NULL) const
        {
        }
    };
    
    // Usage example:
    
    struct nil {};
    typedef boost::variant<nil, char, int, double> sql_field;
    
    struct example_visitor
    {
        typedef picky_visitor< example_visitor, boost::mpl::vector<char, int> > value_type;
    
        inline void operator () (char v) const
        {
            std::cout << "character detected" << std::endl;
        }
    
        inline void operator () (int v) const
        {
            std::cout << "integer detected" << std::endl;
        }
    };
    
    int
    main ()
    {
        example_visitor::value_type visitor;
    
        sql_field nilField;
        sql_field charField ('X');
        sql_field intField (1986);
        sql_field doubleField (19.86);
    
        boost::apply_visitor (visitor, nilField);
        boost::apply_visitor (visitor, charField);
        boost::apply_visitor (visitor, intField);
        boost::apply_visitor (visitor, doubleField);
    }
    

    【讨论】:

    • @Tom,为了保证不支持的类型不会被默认运算符静默忽略,我将向其中添加“boost::enable_if”,这将为不支持的类型禁用该运算符或启用它仅适用于受支持的。两种解决方案都很好,但是第二种解决方案在将构造函数委托给基类时存在一些问题,如果它不是微不足道的。在任何一种情况下, boost::variant 都不是广泛使用的模式,如果它很少被新类型扩展的话。 Andrei Alexandrescu 在他的《现代 C++ 设计》一书中很好地介绍了这个主题,强烈推荐。
    • 非常感谢,Vlad,我现在了解您对 enable_if/typelist 的使用。我有一个使用 enable_if 的 operator()(T) ,它可以满足我的要求,即类型安全和一个简单的解决方案。然而,一个问题是,如果我将变体初始化为 sql_field charField(CharStruct()),代码将无法编译(使用 VS10)。我收到一个错误“.apply_visitor 的 lhs 必须是类/结构/联合。”如果我使用 sql_field charField = CharStruct() 来初始化变体,它可以编译。我想这可能是将构造函数委托给基类的问题?为什么说 Variant 没有那么广泛使用?
    • @Tom, "Type variable (Type ())" - 这是根据 C++ 规则定义的函数。简而言之,这很难解释,所以不要使用它:-)“类型变量=类型()”是要走的路。它正是你想要的,并且不调用赋值运算符,实际上它只是调用了一个构造函数。
    • 当然。谢谢,除了解决我的特定原始问题,我学到的东西比我预期的要多,现在我自己会去学习更多。这是一个很大的帮助,我非常感谢您的时间。最好的问候!
    • 奇怪的是,这从未被赞成。我喜欢 “picky” 访客的名称选择。
    【解决方案2】:

    随着时间的推移,新的有趣的库不断发展。这个问题很老了,但从那以后有一个解决方案对我个人来说比迄今为止给出的解决方案要优越得多。

    优秀的Mach7 库允许前所未有的匹配(以及访问)功能。它由 Yuriy Solodkyy、Gabriel Dos Reis 和 Bjarne Stroustrup 本人编写。对于那些在这个问题上磕磕绊绊的人,这里是一个来自自述文件的例子:

    void print(const boost::variant<double,float,int>& v)
    {
        var<double> d; var<float> f; var<int> n;
    
        Match(v)
        {
          Case(C<double>(d)) cout << "double " << d << endl; break;
          Case(C<float> (f)) cout << "float  " << f << endl; break;
          Case(C<int>   (n)) cout << "int    " << n << endl; break;
        }
        EndMatch
    }
    

    我现在正在使用它,到目前为止,使用它真的很愉快。

    【讨论】:

      【解决方案3】:

      Tom,我相信您的问题在特定情况下非常有意义。假设你想在一个向量中存储多种类型的访问者,但你不能,因为它们都是不同的类型。您有几个选择:再次使用 variant 来存储访问者、使用 boost.any 或使用虚拟函数。我认为虚函数在这里是一个优雅的解决方案,但肯定不是唯一的。

      事情是这样的。

      首先,让我们使用一些变体; boolintfloat 可以。

      typedef boost::variant<bool, int, float> variant_type;
      

      然后是基类,或多或少就像你拥有的那样。

      
      template
      struct Visitor : public boost::static_visitor<>
      {
        void operator()(T0 value)
        {
          Process(value);
        }
        void operator()(T1 value)
        {
          Process(value);
        }
        void operator()(T2 value)
        {
          Process(value);
        }
        virtual void Process(T0 val){ std::cout << "I am Visitor at T0" << std::endl; }
        virtual void Process(T1 val){ std::cout << "I am Visitor at T1" << std::endl; }
        virtual void Process(T2 val){ std::cout << "I am Visitor at T2" << std::endl; }
      protected:
        Visitor(){}
      };
      

      接下来,我们有两个特定的变体。

      
      template
      struct Visitor1 : public Visitor
      {
          void Process(T0 val){ std::cout << "I am Visitor1 at T0" << std::endl; }
          void Process(T2 val){ std::cout << "I am Visitor1 at T2" << std::endl; }
      };
      
      

      template struct Visitor2 : public Visitor { void Process(T1 val){ std::cout << "I am Visitor2 at T1" << std::endl; } void Process(T2 val){ std::cout << "I am Visitor2 at T2" << std::endl; } };

      最后,我们可以制作不同变体的单个向量:

      
      int main() {
        variant_type data(1.0f);
        std::vector*> v;
        v.push_back(new Visitor1());
        v.push_back(new Visitor2());
      
      

      apply_visitor(*v[0],data); apply_visitor(*v[1],data); data = true; apply_visitor(*v[0],data); apply_visitor(*v[1],data);

      return 0; }

      这是输出:

      我是 T2 的访客 1 我是 T2 的访客 2 我是 T0 的访客 1 我是T0的访客

      如果出于某种原因我需要在一个容器中包含不同的变体,我肯定会考虑这种解决方案。我还认为实际上将访问者吸引到另一个变体中会更糟/更好。使用继承的好处是它是事后可扩展的:你总是可以从一个类继承,但是一旦设置了一个变体,你就不能在不实际接触现有代码的情况下更改它。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2023-04-05
        • 2020-07-18
        • 1970-01-01
        • 2022-07-08
        • 2016-07-20
        相关资源
        最近更新 更多