开发者

given abstract base class X, how to create another template class D<T> where T is the type of the class deriving from X?

I want to be able to accept a Message& object which references either a Message1 or Message2 class. I want to be able to create a MessageWithData<Message1> or MessageWithData<Message2> based on the underlying type of the Message& object. For example, see below:

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?
    //
    messag开发者_如何学运维eWithData.dispatch(...)
  }
};

The messageWithData class essentially contains methods inherited from Message which allow it to be dynamically double dispatched back to the handler based on its type. My best solution so far has been to keep the data separate from the message type, and pass it all the way through the dynamic dispatch chain, but I was hoping to come closer to the true idiom of dynamic double dispatch wherein the message type contains the variable data.

(The method I'm more or less following is from http://jogear.net/dynamic-double-dispatch-and-templates)


You're trying to mix runtime and compile-time concepts, namely (runtime-)polymorphism and templates. Sorry, but that is not possible.

Templates operate on types at compile time, also called static types. The static type of message is Message, while its dynamic type might be either Message1 or Message2. Templates don't know anything about dynamic types and they can't operate on them. Go with either runtime polymorphism or compile-time polymorphism, sometimes also called static polymorphism.

The runtime polymorphism approach is the visitor pattern, with double dispatch. Here is an example of compile-time polymorphism, using the CRTP idiom:

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


You have

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

At HERE, you want to create either a MessageWithData<Message1> or a MessageWithData<Message2>, depending on whether message is an instance of Message1 or Message1.

But you cannot do that, because the class template MessageWithData<T> needs to know at compile time what T should be, but that type is not available at that point in the code until runtime by dispatching into message.


As has been mentioned, it is not possible to build your template as is.

I do not see any issue with passing additional parameters, though I would perhaps pack them into a single structure, for ease of manipulation.

Certainly I find it more idiomatic to use a supplementary Data parameter, rather than extending a class hierarchy to shoehorn all this into a pattern.

It is an anti-pattern to try to make a design fit a pattern. The proper way is to adapt the pattern so that it fits the design.

That being said...


There are several alternatives to your solution. Inheritance seems weird, but without the whole design at hand it may be your best bet.

It has been mentioned already that you cannot freely mix compile-time and run-time polymorphisms. I usually use Shims to circumvent the issue:

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

The scheme is simple and allow you to benefit from the best of both worlds:

  • Message being non-template mean that you can apply traditional OO strategies
  • MessageShim<T> being template mean that you can apply traditional Generic Programming strategies

Once done, you should be able to get what you want, for better or worse.


As Xeo says, you probably shouldn't do this in this particular case - better design alternatives exist. That said, you can do it with RTTI, but it's generally frowned upon because your process() becomes a centralised maintenance point that needs to be updated as new derived classes are added. That's easily overlooked and prone to run-time errors.

If you must persue this for some reason, then at least generalise the facility so a single function uses RTTI-based runtime type determination to invoke arbitrary behaviour, as in:

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

In the code above, you can substitute your own Op and have the same runtime-to-compiletime handover take place. It may or may not help to think of this as a factory method in reverse :-}.

As discussed, for_rt_type has to be updated for each derived type: particularly painful if one team "owns" the base class and other teams write derived classes. As with a lot of slightly hacky things, it's more practical and maintainable in support of private implementation rather than as an API feature of a low-level enterprise library. Wanting to use this is still typically a sign of bad design elsewhere, but not always: occasionally there are algorithms (Ops) that benefit enormously:

  • compile-time optimisations, dead code removal etc.
  • derived types only need same semantics, but details can vary
    • e.g. Derived1::value_type is int, Derived2::value_type is double - allows algorithms for each to be efficient and use appropriate rounding etc.. Similarly for different container types where only a shared API is exercised.
  • you can use template metaprogramming, SFINAE etc. to customise the behaviours in a derived-type specific way

Personally, I think knowledge of and ability to apply this technique (however rarely) is an important part of mastering polymorphism.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜