visitor template for boost::variant
Asked Answered
L

3

11

I would like to use a boost.variant<T0,T1,T2> as a parameter to a template 'Visitor' class which would provide visitor operators as required by the boost.variant visitor mechanism, in this case all returning void i.e.,

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

The template would also have for each of the types T0... in the variant a corresponding virtual function which by default does nothing. The user is able inherit from the template class and redefine only those virtual functions which he is interested in. This is something akin to the well-known 'Template Method' pattern. The only solution I have been able to come up with is by wrapping both the boost::variant and the associated visitor in a single template, and accessing them via typedefs. This works okay, however it feels a little clunky. Here's the code:

#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(){}
    };

The class is then used as follows:

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);

As I say, this seems to work okay but I would prefer it if I didn't have to create a special wrapper class to tie the variant and the visitor together. I would prefer to be able just to use a boost.variant directly to instantiate the template visitor class. I've had a look at using type parameters, non-type parameters and template template parameters but nothing seems to suggest itself. Is what I am trying to do not possible? I may be missing something, and would appreciate it if anyone has any input on this.

Latter answered 31/8, 2010 at 19:11 Comment(0)
D
17

The code with Boost Variant and virtual dispatching is a little fishy. Especially taking into account that you know what are you interested in processing during the compile-time and there is absolutely no need in creating a virtual table at run-time in order to achieve your goals.

I would recommend you use partial template specialization. Thus, have a default template method that can accept any type in the variant and will do nothing. For those types you are interested in, just specialize template.

Here is an example. We have three types - Foo, Bar and War. We are interested only in the last two types and have a specialization for them. So Foo is being ignored.

#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;
}

Here is a simple example of the usage:

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);
}

The output of this program will be:

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

Here is another solution. We create a default visitor ignoring everything, except what you have specified in a type list. It is not that convenient because you have to specify a list of types twice - once in a type list and then in each processing method (operator). Plus, the generic template, in fact, will be inheriting your visitor. But nevertheless, here we go:

#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);
}
Diplopia answered 31/8, 2010 at 20:37 Comment(8)
Thanks, Vlad, that is a very good suggestion, achieving what I wanted without the run-time dispatch, which I agree is a little fishy. I'd still be interested to know if what I was trying to do templates-wise i.e., instantiating a class directly with another template-type and using the type parameters of the latter type within the former type, is possible?Latter
You are welcome, Tom. It is a bit hard to get your idea, but if I understand is at least 90% correctly, that is achievable but, in my opinion, is not that convenient in general cases. I've edited my answer and added another solution that is a little bit closer to your initial idea. But instead of using virtual table, the generic visitor inherits your implementation and not vise versa. If that is not acceptable, you can still use virtual functions. I am just trying to avoid them where possible.Diplopia
Your second solution looks interesting, and I will study it (I am still familiarising myself with some of the techniques of modern C++). I guess my slight concern with your first suggested solution, now I've thought about it more, is that it loses the compile-time type-checking of the variant-visitor mechanism, due to the default template function. In particular, you could apply any visitor to any variant and the compiler will never complain. That may be an acceptable price to pay for the convenience, but I was searching for a solution which still provided some type-safety.Latter
@Tom, for a guarantee that types that are not supported will not be silently ignored with default operator, I would add "boost::enable_if" to it that will disable that operator for unsupported types or enable it only for supported ones. Both solutions are good, but the second one has some problems with delegating a constructor to the base class if it is not trivial one. In either case, boost::variant<> is not that widely used pattern and it if it is rarely being extended with new types. Andrei Alexandrescu covers this topic very well in his Modern C++ Design book, highly recommended.Diplopia
Thanks very much, Vlad, I understand your use of enable_if/typelist now. I have one operator()(T) using enable_if and it does what I want, i.e., type-safety and a simple solution. One problem however, is that the code won't compile (with VS10) if I initialise the variant as- sql_field charField(CharStruct()). I get an error 'lhs of .apply_visitor must be class/struct/union.' It compiles ok if instead I initialise variant using sql_field charField = CharStruct(). I guess this could be the problem of delegating the constructor to the base class? Why do you say Variant is not that widely used?Latter
@Tom, "Type variable ( Type ())" - this is a function definition according to C++ rules. This is hard to explain in short, so just don't use it :-) "Type variable = Type ()" is the way to go. It does exactly what you wanted, and does not call an assignment operator, in fact, it just calls a constructor.Diplopia
Of course. Thanks, as well as solving my specific original problem I have learned more than I expected, and will now go and learn more myself. This has been a great help and I really appreciate your time. Best regards!Latter
Weird that this was never upvoted. I love the name choice for the "picky" visitor.Styptic
C
1

As time passes new and interesting libraries develop. This question is old, but since then there is a solution that to me personally is far more superior to the ones that have been given so far.

The excellent Mach7 library which allows unprecedented matching (and therefore visiting) capabilities. It is written by Yuriy Solodkyy, Gabriel Dos Reis and Bjarne Stroustrup himself. For the ones stumbling on this question, here is an example taken from the README:

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
}

I am working with it now and so far it is a real pleasure to use.

Csc answered 18/7, 2017 at 13:29 Comment(0)
A
0

Tom, I believe that your question makes much sense in a particular context. Say that you want to store visitors of multiple types in a vector, but you can't because they are all of different types. You have a few choices: use variant again to store visitors, use boost.any, or use virtual functions. I think that virtual functions are an elegant solution here, but certainly not the only one.

Here is how it goes.

First, let's use some variant; bool, int, and float will do.

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

Then comes the base class, more or less as you had it.


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(){}
};

Next, we have two specific variants.


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; } };

Finally, we can make a single vector of different variants:


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; }

And here is the output:

I am Visitor1 at T2
I am Visitor2 at T2
I am Visitor1 at T0
I am Visitor at T0

If for some reason I needed to have different variants in one container, I would surely consider this solution. I would also think how much worse/better would it be to actually stick the visitors into another variant. The nice thing about using inheritance is that it is extensible post factum: you can always inherit from a class, but once a variant is set, you can't change it without actually touching the existing code.

Andra answered 10/3, 2011 at 20:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.