заданный абстрактный базовый класс X, как создать еще один шаблонный класс D <T>, где T - это тип класса, производного от X? - PullRequest
3 голосов
/ 27 мая 2011

Я хочу иметь возможность принимать объект Message&, который ссылается на класс Message1 или Message2. Я хочу иметь возможность создать MessageWithData<Message1> или MessageWithData<Message2> на основе базового типа объекта Message&. Например, см. Ниже:

class Message {};
class Message1 : public Message {};
class Message2 : public Message {};

template<typename Message1or2>
class MessageWithData : public Message1or2 { public: int x, y; }

class Handler()
{
public:
  void process(const Message& message, int x, int y)
  {
    // create object messageWithData whose type is 
    // either a MessageWithData<Message1> or a MessageWithData<Message2> 
    // based on message's type.. how do I do this?
    //
    messageWithData.dispatch(...)
  }
};

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

(метод, которым я более или менее следую, взят из http://jogear.net/dynamic-double-dispatch-and-templates)

Ответы [ 4 ]

3 голосов
/ 27 мая 2011

Вы пытаетесь смешать понятия времени исполнения и времени компиляции, а именно (время выполнения) полиморфизм и шаблоны.Извините, но это невозможно.

Шаблоны работают с типами во время компиляции, также называемыми статическими типами .Статический тип message равен Message, а динамический тип может быть Message1 или Message2.Шаблоны ничего не знают о динамических типах и не могут с ними работать.Используйте либо полиморфизм времени исполнения , либо полиморфизм времени компиляции, иногда также называемый статическим полиморфизмом.

Подход полиморфизма времени выполнения - это шаблон посетителя с двойной диспетчеризацией.Вот пример полиморфизма времени компиляции с использованием CRTP идиома :

template<class TDerived>
class Message{};

class Message1 : public Message<Message1>{};
class Message2 : public Message<Message2>{};

template<class TMessage>
class MessageWithData : public TMessage { public: int x, y; };

class Handler{
public:
  template<class TMessage>
  void process(Message<TMessage> const& m, int x, int y){
    MessageWithData<TMessage> mwd;
    mwd.x = 42;
    mwd.y = 1337;
  }
};
2 голосов
/ 27 мая 2011

У вас есть

void process(const Message& message, int x, int y)
{
  // HERE
  messageWithData.dispatch(...)
}

В ЗДЕСЬ вы хотите создать MessageWithData<Message1> или MessageWithData<Message2>, в зависимости от того, является ли message экземпляром Message1 или Message1.

Но вы не можете этого сделать, потому что шаблон класса MessageWithData<T> должен знать во время компиляции , что должно быть T, но этот тип недоступен в данный момент в коде до времени выполнения путем отправки в message.

1 голос
/ 27 мая 2011

Как уже упоминалось, невозможно создать ваш шаблон как есть.

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

Конечно, я считаю более идиоматичным использование дополнительного параметра Data, а не расширение иерархии классов для включения всего этого в шаблон .

Itэто анти-шаблон, чтобы попытаться сделать дизайн в соответствии с шаблоном.Правильный способ - адаптировать шаблон так, чтобы он соответствовал дизайну.

При этом ...


Есть несколько альтернатив вашему решению.Наследование кажется странным, но без целого дизайна это может быть вашим лучшим выбором.

Уже упоминалось, что вы не можете свободно смешивать полиморфизмы во время компиляции и во время выполнения.Я обычно использую Shims, чтобы обойти проблему:

class Message {};
template <typename T> class MessageShim<T>: public Message {};
class Message1: public MessageShim<Message1> {};

Схема проста и позволяет вам извлечь выгоду из лучшего из обоих миров:

  • Message, не являющийся шаблономозначает, что вы можете применять традиционные стратегии ОО
  • MessageShim<T> как шаблон означает, что вы можете применять традиционные стратегии общего программирования

Когда вы закончите, вы сможете получить то, что вы хотите,к лучшему или к худшему.

1 голос
/ 27 мая 2011

Как говорит Xeo, вам, вероятно, не следует делать это в данном конкретном случае - существуют лучшие варианты дизайна. Тем не менее, вы можете сделать это с помощью RTTI, но это, как правило, вызывает недовольство, поскольку ваш process() становится централизованной точкой обслуживания, которую необходимо обновлять по мере добавления новых производных классов. Это легко упустить из виду и подвержено ошибкам во время выполнения.

Если вы по какой-то причине должны продолжить это, то, по крайней мере, обобщите средство, чтобы одна функция использовала определение типа среды выполнения на основе RTTI для вызова произвольного поведения, как в:

#include <iostream>
#include <stdexcept>

struct Base
{
    virtual ~Base() { }

    template <class Op>
    void for_rt_type(Op& op);
};

struct Derived1 : Base
{
    void f() { std::cout << "Derived1::f()\n"; }
};

struct Derived2 : Base
{
    void f() { std::cout << "Derived2::f()\n"; }
};

template <class Op>
void Base::for_rt_type(Op& op)
{
    if (Derived1* p = dynamic_cast<Derived1*>(this))
        op(p);
    else if (Derived2* p = dynamic_cast<Derived2*>(this))
        op(p);
    else
        throw std::runtime_error("unmatched dynamic type");
}

struct Op
{
    template <typename T>
    void operator()(T* p)
    {
        p->f();
    }
};

int main()
{
    Derived1 d1;
    Derived2 d2;
    Base* p1 = &d1;
    Base* p2 = &d2;
    Op op;
    p1->for_rt_type(op);
    p2->for_rt_type(op);
}

В приведенном выше коде вы можете заменить свою собственную операционную функцию и выполнить такую ​​же передачу времени от времени исполнения до компиляции. Это может или не может помочь думать об этом как о заводском методе в обратном порядке: -}.

Как уже говорилось, for_rt_type необходимо обновлять для каждого производного типа: особенно больно, если одна команда «владеет» базовым классом, а другие команды пишут производные классы. Как и в случае со многими хакерскими вещами, он более практичен и удобен для поддержки частной реализации, а не как функция API низкоуровневой корпоративной библиотеки. Желание использовать это все еще обычно признак плохого дизайна в другом месте, но не всегда: иногда существуют алгоритмы (Op s), которые приносят огромную пользу:

  • оптимизация во время компиляции, удаление мертвого кода и т. Д.
  • производным типам нужна только одна и та же семантика, но детали могут различаться
    • например. Derived1::value_type is int, Derived2::value_type is double - позволяет алгоритмам для каждого быть эффективными и использовать соответствующее округление и т. Д. Аналогично для различных типов контейнеров, где используется только общий API.
  • вы можете использовать метапрограммирование шаблонов, SFINAE и т. Д. Для настройки поведения специфичным для производного типа способом

Лично я считаю, что знание и умение применять эту технику (хотя и редко) является важной частью овладения полиморфизмом.

...