如何在不向下转换的情况下调用派生类的成员函数

Posted

技术标签:

【中文标题】如何在不向下转换的情况下调用派生类的成员函数【英文标题】:how to call member function of a derived class without downcasting 【发布时间】:2012-10-17 10:52:33 【问题描述】:

我有消息类和处理器类的雇佣关系。每个处理器都可以即时接收一条或多条消息。由于每条消息都可以有一些不同的属性,我必须将该消息向下转换为具体的消息类,以实际处理它。 因为没有。消息类和进程类,我不想使用 dynamic_cast。 我尝试使用以下代码,但这会导致编译时错误。 此外,我可以灵活地将处理器指针附加到消息中(如果需要),但反过来不行。

class Message  
  
    public:  
    virtual const Message* const getMessage() const = 0;
;

class MA : public Message  
  
    public:  
    const MA* const getMessage() const return this;
    void printMA() conststd::cout<<"I am MA"<<std::endl;
;

class MB : public Message  
  
    public:  
    const MB* const getMessage() const return this;
    void printMB() conststd::cout<<"I am MB"<<std::endl;
;

class Processor  
  
public:  
    virtual void process(const Message* m) = 0;

;

class PA : public Processor  
  
    public:  
    void process(const Message* m) processM(m->getMessage());

    void processM(const MA*  m) m->printMA();
    void processM(const MB*  m) m->printMB();
;

int main()  
  
    Message* m1 = new MA();  
    Message* m2 = new MB();  

    Processor* p1 = new PA();  
    p1->process(m1);
    p1->process(m2);
    return 0;

【问题讨论】:

如果非要投,那说明设计有问题 为什么在基于消息的类中不能有一个通用方法说 print 并且从处理器调用? 这里的打印方法只是举例。实际上,我在每个派生消息类中都有不同的属性和方法,将它们放入基类中是不可行的(我也认为不正确)。 @puneetagrawal 所以,我猜你有数据继承?然后,您可以使用模板,Processor 具有专用于 Processor 的功能,它可以访问 MA 的成员而无需强制转换 【参考方案1】:

我最终使用了“双重调度”来解决这个问题。现在,唯一的事情是,每当我添加新的消息类型时,我都需要在 MessageProcessor 类中添加一个函数。但我认为这很好。

class MessageProcessor

    public:
        virtual void process(const MA*) conststd::cout<<"unhandled:MA"<<std::endl;
        virtual void process(const MB*) conststd::cout<<"unhandled:MB"<<std::endl;
        virtual void process(const MC*) conststd::cout<<"unhandled:MC"<<std::endl;
;

class Message

    public:
    virtual void process(const MessageProcessor*) const = 0;
;

class MA : public Message

    public:
    void printMA() conststd::cout<<"I am MA"<<std::endl;
    virtual void process(const MessageProcessor* p) const p->process(this);
;

class MB : public Message

    public:
    void printMB() conststd::cout<<"I am MB"<<std::endl;
    virtual void process(const MessageProcessor* p) const p->process(this);
;

class MC : public Message

    public:
    void printMC() conststd::cout<<"I am MC"<<std::endl;
    virtual void process(const MessageProcessor* p) const p->process(this);
;

class Processor : public MessageProcessor

    public:
    void processM(const Message* m)m->process(this);

;

class PA : public Processor

    public:
    void process(const MA*  m) const m->printMA();
    void process(const MB*  m) const m->printMB();
;

class PB : public Processor

    public:
    void process(const MA*  m) const m->printMA();
    void process(const MC*  m) const m->printMC();
;

int main()

    const Message* m1 = new MA();
    const Message* m2 = new MB();
    const Message* m3 = new MC();

    Processor* p1 = new PA();
    p1->processM(m1);
    p1->processM(m2);
    p1->processM(m3);

    Processor* p2 = new PB();
    p2->processM(m1);
    p2->processM(m2);
    p2->processM(m3);

    return 0;

【讨论】:

实际上,如果只有少数消息类型需要特殊处理,其余的都可以使用相同的代码,那么您可以在 MessageProcessor 中拥有一个 process(const Message*),以及任何没有专门的 process() 将使用它。当然,如果每个消息类型都需要自己的处理程序,那么您当然需要为每个新消息类型添加一个新函数。【参考方案2】:

解决问题的最通用方法可能是Visitor pattern。

【讨论】:

这只能解决他的部分问题,即Processor 类的层次结构。但即使对于访问者,您仍然需要为要访问的Message 类提供一个通用接口,以便访问者可以挂钩。【参考方案3】:

最简单的做法是消除getMessage() 方法,并在Message 中将print() 设为纯虚拟,并在MAMB 中覆盖它。此外,您可以在Process 中使process() 成为纯虚方法,并在PA 中覆盖它。见以下代码:

#include <iostream>

class Message  
  
    public:  
    const std::string _id;

    Message(std::string id):_id(id) 

    virtual void print() const = 0;
    virtual void other_fun() const = 0;
;

class MA : public Message  
 
    private: double d_; 
    public:  
    MA():Message("MA"), d_(0.0) 

    virtual void print() const
    
        std::cout<<"I am MA"<<std::endl;
        std::cout << "I also have a double" << std::endl; 
    

    virtual void other_fun() const  std::cout << "I am MA specific" << std::endl; 

    void do_hoops () const  std::cout << "Hoop!"<<std::endl;
;

class MB : public Message  
  
    private: int i_;
    public:  
    MB():Message("MB"), i_(0) 

    virtual void print() const
    
        std::cout<<"I am MB"<<std::endl;
        std::cout << "I also have an int"<<std::endl;
    

    virtual void other_fun() const  std::cout << "I am MB specific" << std::endl; 

    void do_twist() const  std::cout << "Twist!"<<std::endl; 
;

class Processor  
  
public:  
    const std::string _id;
    Processor(std::string id) : _id(id)

    virtual void process(const Message* m) = 0;

;

class PA : public Processor  
  
    public:  
    PA():Processor("PA") 

    virtual void process(const Message* m) 
    
        m->print();
        m->other_fun();
    
;

int main()  
  
    Message* m1 = new MA();  
    Message* m2 = new MB();  

    // generic handling of message
    Processor* p1 = new PA();  
    p1->process(m1);
    p1->process(m2);

    // message specific stuff
    dynamic_cast<MA*>(m1)->do_hoops();
    dynamic_cast<MB*>(m2)->do_twist();
    return 0;

Ideone 上的输出。

不需要强制转换,虚拟函数将在运行时通过动态调度(虚拟表查找等)选择。 MessageProcess 是抽象基类(“接口”),MAMBPA 是实现这些接口的具体类。理想情况下,您还可以将std::string 状态排除在Message 接口之外,但这留作练习。

如果您要调用特定于派生类的函数,并且如果您在运行时知道您实际上正在调用这样的类,则需要进行强制转换。这是通过dynamic_cast 指向您的基类指针当前指向的特定派生类来完成的。

【讨论】:

这里的打印方法只是举例。实际上,我在每个派生消息类中都有不同的属性和方法,将它们放入基类中是不可行的(我也认为不正确)。 @puneetagrawal 您可以根据需要将其他状态/功能添加到MAMB,并在print() 或其他成员的覆盖版本中简单地使用它们。但是,如果您想多态地调用成员(即在运行时选择与消息类型对应的成员),那么这样的方法需要在基类中。 @puneetagrawal 更新了我的答案以反映您的问题。我建议将所有通用代码移至抽象基类,并在适当的地方覆盖它。只有那些没有公共接口的函数,我称之为MA::do_hoops()MB::do_twist(),才应该使用动态转换。【参考方案4】:

你有一个设计缺陷。 Processor::process 的签名表明它需要一个 Message,那么它不应该通过尝试访问不是 Message 的公共接口的东西来破坏这个承诺。

您可以将Process 设为继承自用户提供的策略的模板类(主机)。这里的策略是具体的Message 类。像这样的:

#include <iostream>

struct MA

    void print ()
      
        std::cout << "MA: I'm the interface" << std::endl;
    

    void printMA ()
      
        std::cout << "MA: I'm special" << std::endl;
    
;

struct MB

    void print ()
      
        std::cout << "MB: I'm the interface" << std::endl;
    

    void printMB ()
      
        std::cout << "MB: I'm special" << std::endl;
    
;

template <typename M>
struct Process :
    public M

    void process()
      
        M::print();
    
;

int main ()

    Process<MA> p1;
    Process<MB> p2;

    p1.print();     // MA: I'm the interface
    p1.printMA();   // MA: I'm special

    p2.print();     // MB: I'm the interface
    p2.printMB();   // MB: I'm special

策略具有定义其接口的print 方法。他们也有一些特殊的方法,比如printMAprintMB。主机类(此处为 Process)充当策略的用户界面。它可以使用策略类的接口方法。用户可以通过宿主类调用特殊的策略方法。

【讨论】:

如果他想拥有一个可以处理多种类型消息的Processor,那么将Process设置为模板类并没有帮助。【参考方案5】:

您遇到了 C++ 的限制。您真正想要的是让多态性处理方法的参数,而不仅仅是调用参数的方法。它通常被称为double dispatch。不幸的是,虽然有一些解决方法,但我还没有看到任何完美的解决方法。该 Wikipedia 文章显示了普遍接受的解决方法(使用访问者模式)。

【讨论】:

我同意。我最终使用了“双重调度”来解决这个问题。现在,唯一的事情是我需要在 MessageProcessor 类中添加一个函数,每当我添加新的消息类型时。但我认为这很好。

以上是关于如何在不向下转换的情况下调用派生类的成员函数的主要内容,如果未能解决你的问题,请参考以下文章

继承和派生

向下转换指向成员函数的指针

如何在调用基类的静态函数之前设置派生静态成员

详解C++中基类与派生类的转换以及虚基类

为啥指向基类的派生类指针可以调用派生类成员函数? [复制]

类型兼容规则