C++多态性:我错过了什么?

3
我正在学习C++,并希望构建类似于C#事件的东西来处理嵌入式C++项目中的中断。
到目前为止,我已经想出了一个几乎符合我的要求的解决方案。然而,我需要一些关于多态性的帮助。以下代码片段是重新现我的情况的最小示例:
#include <iostream>       

struct Event
  { };

struct EventHandler
  {
    virtual void Esr (const Event& I) { }
  };

struct EventSender
  {
    EventSender (EventHandler& Handler) : _Handler (Handler) { }

    template <typename T>
    void SendEvent (const T&) const
      {
        _Handler.Esr (T ());
      }

    EventHandler& _Handler;
  };

struct SpecialEvent : public Event
  { };

struct MyHandler : public EventHandler
  {
    void Esr (const Event& I) override { std::cout << "Event" << std::endl; }
    void Esr (const SpecialEvent& I) { std::cout << "SpecialEvent" << std::endl; }
  };            

int main()
  {
    MyHandler handler;
    EventSender sender (handler);

    /* Invoke directly  */
    handler.Esr (Event ());
    handler.Esr (SpecialEvent ());

    /* Invoke indirectly  */
    sender.SendEvent (Event ());
    sender.SendEvent (SpecialEvent ());  // Expected cout msg: "SpecialEvent"

    return 0;
  }

预期控制台输出:
Event
SpecialEvent
Event
SpecialEvent

实际控制台输出:

Event
SpecialEvent
Event
Event

编译器/链接器在这里做了我不知道的什么处理?


一个有趣的问题:向答案迈出了一步,但并不完整,因此需要评论:通过在方法声明中替换{} is =0;,使Esr方法成为纯虚拟方法。 - Dale Wilson
5
你希望他在调试器中看到什么,而他之前没有发现的?他已经很好地确定了实际调用的方法。 现在他在问:“为什么?” - Dale Wilson
我认为你混淆了多态和重载。 - Banex
@George,多态没有通用的定义,我更喜欢将它们区分开来。请看这个链接:https://dev59.com/_nE95IYBdhLWcg3wPbV7 - Banex
人们通常将编译时多态和运行时多态区分开来。 - user1781434
显示剩余2条评论
3个回答

3
在这里,您正在尝试使用重载而不是经典(基于虚函数的)多态性。您想要的(至少我理解的是)是在直接使用处理程序和通过发送者间接调用它之间基本相同的行为。发生的变化在于“事件”和“特殊事件”之间。在这种情况下,经典的多态性将涉及到Event中的一个虚函数,该虚函数在SpecialEvent中被覆盖:
struct Event { 
    virtual void operator()() const { std::cout << "Event\n"; }
};

struct SpecialEvent : public Event { 
    virtual void operator()() const override { std::cout << "Special Event\n"; }
};

有了这个功能,对一个 Event 的引用(或指针)将调用实际类型的成员。在这里进行多态意味着我们只需要一个处理程序类,因此代码最终会变成这样:

#include <iostream>

struct Event { 
    virtual void operator()() const { std::cout << "Event\n"; }
};

struct EventHandler {
    void Esr(const Event& I) const { I(); }
};

struct EventSender {
    template <typename T>
    void SendEvent (const T& t) const {
        handler.Esr(t);
    }

    EventHandler handler;
};

struct SpecialEvent : public Event { 
    virtual void operator()() const override { std::cout << "Special Event\n"; }
};

int main() {
    EventHandler handler;
    EventSender sender;

    /* Invoke directly  */
    handler.Esr (Event ());
    handler.Esr (SpecialEvent ());

    /* Invoke indirectly  */
    sender.SendEvent (Event ());
    sender.SendEvent (SpecialEvent ());  // Expected cout msg: "SpecialEvent"
}

2

在MyHandler中有两种方法。其中一种覆盖了基类方法,另一种则没有。

一种解决方法是在基类中声明这两种方法:

struct EventHandler
{
    virtual void Esr (const Event& I) = 0;
    virtual void Esr (const SpecialEvent& I) = 0;
};

这样编译器就可以使用参数的类型来解析EventHandler级别的方法。

如果您想避免所有派生类都必须重载两种方法的要求,您可以像这样做:

 struct EventHandler
 {
    virtual void Esr (const Event& I) = 0;
    virtual void Esr (const SpecialEvent& I)
    {
        // if not overridden, use the non-specialized event handler.
        Esr(reinterpret_cast<const Event &>(I));
    }
 };

为了回答你的问题:
在C++中,方法调用在编译/链接时被解析为1)对特定代码块(即方法体)的调用或2)通过称为vtable的隐藏数据结构的间接调用。实际的vtable是在运行时确定的,但编译器必须决定哪个表条目用于调用。(搜索vtable以获取更多有关它们是什么以及如何实现的信息。)
它必须基于它所允许知道的内容来进行此解析。在这种情况下,是基于通过其调用方法的指针或引用的类型。请注意,这不一定是实际对象的类型。
在你的例子中,当你通过handler调用时,编译器可以了解到在MyHandler中声明的两个方法,因此它可以选择你期望的那个方法,但是当调用通过sender时,它必须找到在EventSender中声明的方法。只有一个方法在EventSender中声明。幸运的是,参数可以强制转换为const Event &,因此编译器能够使用该方法。因此,它使用了MyHandler的vtable条目[在运行时]并使用该方法的vtable条目。
 Esr (const Event& I)

这就是你最终进入错误方法的原因。

顺便说一下:我的回答旨在解释你看到的内容,并为你提供解决即时问题的方法。Jerry Coffin的回答提供了一种长期更好的替代方案。


1
首先,您不能将引用转换为基类的后代。 您需要使用该类型的指针,并使用 dynamic_cast
所以,您有
EventSender sender (handler);

main()中,sender的构造函数绑定到了MyHandler的基类EventHandler上,因为这是MyHandler构造函数的参数类型(= EventHandler::EventHandler)。因此,调用了EventHandler.Esr(const Event &),它恰好是虚拟的,所以有一个指针指向了MyHandler.Esr(const Event &)
请注意,技术上来说Esr(const Event &)Esr(const SpecialEvent &)是两个不同的方法;它们只是碰巧使用了相同的名称。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接