3

我希望能够接受一个Message&引用 aMessage1Message2class 的对象。我希望能够创建一个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

4 回答 4

3

您正在尝试混合运行时和编译时概念,即(运行时)多态性和模板。对不起,但那是不可能的。

模板在编译时对类型进行操作,也称为静态类型。的静态类型messageMessage,而其动态类型可能是Message1or 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;
  }
};
于 2011-05-26T23:17:28.277 回答
2

你有

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

在 HERE,您想要创建 aMessageWithData<Message1>或 a MessageWithData<Message2>,具体取决于是否messageMessage1or的实例Message1

但是你不能这样做,因为类模板MessageWithData<T>需要在编译时知道T应该是什么,但是直到运行时通过调度到message.

于 2011-05-26T23:23:52.880 回答
1

如前所述,不可能按原样构建您的模板。

我认为传递附加参数没有任何问题,尽管我可能会将它们打包成一个结构,以便于操作。

当然,我发现使用补充参数更惯用Data,而不是扩展类层次结构将所有这些硬塞进一个模式中。

试图使设计适合模式是一种反模式。正确的方法是调整模式,使其适合设计。

话虽如此...


您的解决方案有多种选择。继承似乎很奇怪,但如果没有手头的整个设计,它可能是你最好的选择。

已经提到过,您不能随意混合编译时和运行时多态性。我通常使用 Shims 来规避这个问题:

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

该方案很简单,可让您从两全其美中受益:

  • Message非模板意味着您可以应用传统的OO策略
  • MessageShim<T>作为模板意味着您可以应用传统的通用编程策略

一旦完成,你应该能够得到你想要的,无论好坏。

于 2011-05-27T06:47:33.923 回答
1

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

在上面的代码中,您可以替换您自己的 Op 并进行相同的运行时到编译时切换。将其视为反向工厂方法可能有帮助,也可能无济于事:-}。

正如所讨论的,for_rt_type必须为每个派生类型更新:如果一个团队“拥有”基类而其他团队编写派生类,则尤其痛苦。与许多稍微有点 hacky 的东西一样,它在支持私有实现而不是作为低级企业库的 API 功能时更加实用和可维护。想要使用它通常仍然是其他地方糟糕设计的标志,但并非总是如此:偶尔会有一些算法Op受益匪浅:

  • 编译时优化,死代码删除等。
  • 派生类型只需要相同的语义,但细节可能会有所不同
    • 例如Derived1::value_typeis int, Derived2::value_typeis double- 允许每个算法高效并使用适当的舍入等。对于仅使用共享 API 的不同容器类型也是如此。
  • 您可以使用模板元编程、SFINAE 等以特定于派生类型的方式自定义行为

就个人而言,我认为应用这种技术的知识和能力(但很少)是掌握多态性的重要部分。

于 2011-05-27T01:21:57.913 回答