шаблон посетителя для boost::variant

Я хотел бы использовать 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 напрямую для создания экземпляра класса посетителя шаблона. Я рассмотрел использование параметров типа, параметров, не являющихся типом, и параметров шаблона шаблона, но, похоже, ничего не напрашивается. То, что я пытаюсь сделать, невозможно? Я могу что-то упустить, и был бы признателен, если бы кто-нибудь что-то сказал по этому поводу.


person Tom Jordan    schedule 31.08.2010    source источник


Ответы (3)


Код с Boost Variant и виртуальной диспетчеризацией немного подозрительный. Особенно с учетом того, что вы знаете, что вас интересует в обработке во время компиляции, и нет абсолютно никакой необходимости создавать виртуальную таблицу во время выполнения для достижения ваших целей.

Я бы рекомендовал вам использовать частную специализацию шаблона. Таким образом, есть метод шаблона по умолчанию, который может принимать любой тип в варианте и ничего не делать. Для тех типов, которые вас интересуют, просто настройте шаблон.

Вот пример. У нас есть три типа - 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);
}
person Community    schedule 31.08.2010
comment
Спасибо, Влад, это очень хорошее предложение, позволяющее добиться того, чего я хотел, без диспетчеризации во время выполнения, что, я согласен, немного подозрительно. Мне все еще было бы интересно узнать, возможно ли то, что я пытался сделать с помощью шаблонов, то есть создание экземпляра класса непосредственно с другим типом шаблона и использование параметров типа последнего типа в первом типе? - person Tom Jordan; 01.09.2010
comment
Добро пожаловать, Том. Немного сложно уловить вашу идею, но если я правильно понимаю, то хотя бы на 90% это достижимо, но, на мой взгляд, не очень удобно в общих случаях. Я отредактировал свой ответ и добавил другое решение, которое немного ближе к вашей первоначальной идее. Но вместо использования виртуальной таблицы общий посетитель наследует вашу реализацию, а не наоборот. Если это неприемлемо, вы все равно можете использовать виртуальные функции. Я просто стараюсь избегать их, где это возможно. - person ; 01.09.2010
comment
Ваше второе решение выглядит интересно, и я его изучу (я все еще знакомлюсь с некоторыми приемами современного C++). Я предполагаю, что мое небольшое беспокойство по поводу вашего первого предложенного решения, теперь я подумал об этом больше, заключается в том, что оно теряет проверку типов во время компиляции механизма варианта-посетителя из-за функции шаблона по умолчанию. В частности, вы можете применить любого посетителя к любому варианту, и компилятор никогда не будет жаловаться. Это может быть приемлемой платой за удобство, но я искал решение, которое по-прежнему обеспечивало бы некоторую безопасность типов. - person Tom Jordan; 02.09.2010
comment
@Tom, для гарантии того, что неподдерживаемые типы не будут молча игнорироваться оператором по умолчанию, я бы добавил к нему boost::enable_if, который отключит этот оператор для неподдерживаемых типов или включит его только для поддерживаемых. Оба решения хороши, но у второго есть некоторые проблемы с делегированием конструктора базовому классу, если он не тривиален. В любом случае, boost::variant‹› не является широко используемым шаблоном, и он редко расширяется новыми типами. Андрей Александреску очень хорошо освещает эту тему в своей книге «Современный дизайн C++», которую мы настоятельно рекомендуем. - person ; 02.09.2010
comment
Большое спасибо, Влад, теперь я понимаю, что ты используешь enable_if/typelist. У меня есть один оператор () (T), использующий enable_if, и он делает то, что я хочу, то есть безопасность типов и простое решение. Одна проблема, однако, заключается в том, что код не будет компилироваться (с VS10), если я инициализирую вариант как sql_field charField(CharStruct()). Я получаю сообщение об ошибке: «Левая часть .apply_visitor должна быть классом/структурой/объединением». Он компилируется нормально, если вместо этого я инициализирую вариант, используя sql_field charField = CharStruct(). Я предполагаю, что это может быть проблема делегирования конструктора базовому классу? Почему вы говорите, что Variant не так широко используется? - person Tom Jordan; 03.09.2010
comment
@Tom, переменная типа ( Type ()) - это определение функции в соответствии с правилами C ++. Это трудно объяснить вкратце, так что просто не используйте его :-) Переменная типа = Type () - это правильный путь. Он делает именно то, что вы хотели, и не вызывает оператор присваивания, а просто вызывает конструктор. - person ; 03.09.2010
comment
Конечно. Спасибо, а также решение моей конкретной исходной проблемы, я узнал больше, чем ожидал, и теперь пойду и узнаю больше сам. Это очень помогло, и я очень ценю ваше время. С наилучшими пожеланиями! - person Tom Jordan; 03.09.2010
comment
Странно, что за это никогда не проголосовали. Мне нравится выбор имени для привередливого посетителя. - person Georg Fritzsche; 13.11.2010

Со временем появляются новые и интересные библиотеки. Этот вопрос старый, но с тех пор есть решение, которое лично для меня намного лучше тех, которые были даны до сих пор.

Отличная библиотека Mach7, которая обеспечивает беспрецедентные возможности сопоставления (и, следовательно, посещения). Сценарий написали сами Юрий Солодкий, Габриэль Дос Рейс и Бьерн Страуструп. Для тех, кто наткнулся на этот вопрос, вот пример, взятый из 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
}

Я работаю с ним сейчас, и до сих пор это настоящее удовольствие от использования.

person samvv    schedule 18.07.2017

Том, я считаю, что твой вопрос имеет смысл в конкретном контексте. Допустим, вы хотите хранить посетителей разных типов в векторе, но не можете, потому что все они разных типов. У вас есть несколько вариантов: снова использовать вариант для хранения посетителей, использовать boost.any или использовать виртуальные функции. Я думаю, что виртуальные функции — элегантное решение, но, конечно, не единственное.

Вот как это происходит.

Во-первых, давайте использовать какой-то вариант; bool, int и float подойдут.

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

И вот результат:

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

Если бы мне по какой-то причине нужно было иметь разные варианты в одном контейнере, я бы обязательно рассмотрел это решение. Я также подумал бы, насколько хуже/лучше было бы на самом деле засунуть посетителей в другой вариант. Преимущество использования наследования заключается в том, что оно расширяемо постфактум: вы всегда можете наследовать от класса, но как только вариант установлен, вы не можете изменить его, фактически не коснувшись существующего кода.

person Marcin Zalewski    schedule 10.03.2011