【问题标题】:given abstract base class X, how to create another template class D<T> where T is the type of the class deriving from X?给定抽象基类 X,如何创建另一个模板类 D<T>,其中 T 是从 X 派生的类的类型?
【发布时间】:2011-05-26 22:49:07
【问题描述】:

我希望能够接受引用 Message1Message2 类的 Message&amp; 对象。我希望能够基于Message&amp; 对象的基础类型创建MessageWithData&lt;Message1&gt;MessageWithData&lt;Message2&gt;。例如,见下文:

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

【问题讨论】:

  • Message1 和 Message2 都是从 Message 派生的,因此使用模板而不是虚拟调度函数有什么优势?
  • 抱歉,在我发布的缩写代码中并不明显——Message1 实际上继承自 Message&lt;Message1&gt;,而 Message&lt;Message1&gt; 继承自 MessageBaseMessageBase 有一个纯虚 dispatch() 方法,Message&lt;Message1&gt; 实现了该方法。在Message&lt;Message1&gt;::dispatch() 中,完成了static_cast&lt;const Message1*&gt;(this) 以便分派回处理程序中的正确函数。因此MessageWithData 也必须使用Message1Message2 进行模板化,这样dispatch() 才能正常工作。

标签: c++ templates inheritance double-dispatch


【解决方案1】:

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

模板在编译时对类型进行操作,也称为静态类型message 的静态类型是Message,而它的动态类型可能是Message1Message2。模板对动态类型一无所知,也无法对其进行操作。使用运行时多态编译时多态,有时也称为静态多态。

运行时多态方法是访问者模式,具有双重调度。下面是一个编译时多态的例子,使用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;
  }
};

【讨论】:

    【解决方案2】:

    你有

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

    在 HERE,您想要创建 MessageWithData&lt;Message1&gt;MessageWithData&lt;Message2&gt;,具体取决于 messageMessage1 还是 Message1 的实例。

    但你不能这样做,因为类模板MessageWithData&lt;T&gt; 需要知道在编译时应该是什么T,但该类型在代码中此时不可用直到运行时通过分派到message

    【讨论】:

      【解决方案3】:

      如前所述,无法按原样构建模板。

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

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

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

      话虽这么说......


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

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

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

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

      • Message是非模板意味着你可以应用传统的OO策略
      • MessageShim&lt;T&gt; 是模板意味着您可以应用传统的泛型编程策略

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

      【讨论】:

      • 看起来 MessageWithData 需要从 MessageShim 级别的成员函数中构造,我对此是否正确?我如何告诉 MessageShim 我需要创建 MessageWithData 而不是 MessageWithDifferentData?我无法完全想象代码将如何流动..
      • @Kyle:有多种可能性,例如,您可以在MessageShim&lt;T&gt; 中重载virtual MessageWithData* createMessageWithData 以创建MessageWithDataShim&lt;T&gt;(源自前者)。任何其他类型的数据的另一个重载。但就像我说的那样,我不喜欢像这样实用的推送数据的想法。如果您无法忍受传递它们,为什么不将它们放入访问者而不是消息中?
      【解决方案4】:

      正如 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 功能时更加实用和可维护。想要使用它仍然是通常其他地方糟糕设计的标志,但并非总是:偶尔会有算法(Ops)受益匪浅:

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

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

      【讨论】:

      • 写得很好,我只是部分地提到了访问者模式——动态类型和dynamic_cast。但是,与访问者模式相比,使用dynamic_cast 进行黑客攻击不是非常缓慢吗?
      • @Xeo:一个电话,是的。但是,如果您需要进行许多虚拟调用,则进行一次类型确定然后具有特定于派生类型的编译时优化代码会更快。然后如前所述,存在一些功能差异(我应该更全面地列出它们 - 将编辑)。干杯。
      • @Tony:好点,没想到批处理操作。是否可以使用访问者模式来实现?我的大脑现在不能正常工作。
      • @Xeo:是的,绝对是......访问者在我列出的方法中删除了丑陋/不可扩展的 if/else 链,但需要每个派生类的明确支持(接受访问者并重新分配)。我会说访客通常是更好的选择 - 我只是列出了上面的内容,因为它似乎更适合所问的问题....
      • 抱歉 - 重读您的第一条评论 - 您已经要求与访问者进行比较,而当我看到“非常缓慢”时,我的思绪就飘到了虚拟调度和重复使用它的想法中考虑到您在询问访客。我的“批量操作”回复不适用于您的实际问题。
      猜你喜欢
      • 2021-11-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-05-22
      • 1970-01-01
      • 2010-10-22
      • 2012-09-25
      • 1970-01-01
      相关资源
      最近更新 更多