使用不同的子类从父指针调用子方法

19

我有一个父类,有两个或更多的子类继承它。未来可能会增加不同的子类数量,以满足更多的需求,但它们都将遵循基类方案,并包含一些自己独特的方法。让我举个例子 -

#include <iostream>
#include <string>
#include <vector>
#include <memory>

class B{
    private: int a; int b;
    public: B(const int _a, const int _b) : a(_a), b(_b){}
    virtual void tell(){ std::cout << "BASE" << std::endl; }
};

class C : public B{
    std::string s;
    public: C(int _a, int _b, std::string _s) : B(_a, _b), s(_s){}
    void tell() override { std::cout << "CHILD C" << std::endl; }
    void CFunc() {std::cout << "Can be called only from C" << std::endl;}
};

class D : public B{
    double d;
    public: D(int _a, int _b, double _d) : B(_a, _b), d(_d){}
    void tell() override { std::cout << "CHILD D" << std::endl; }
    void DFunc() {std::cout << "Can be called only from D" << std::endl;}
};

int main() {
    std::vector<std::unique_ptr<B>> v;

    v.push_back(std::make_unique<C>(1,2, "boom"));
    v.push_back(std::make_unique<D>(1,2, 44.3));

    for(auto &el: v){
        el->tell();
    }
    return 0;
}
在上面的示例中,tell()方法将正常工作,因为它是虚拟的并且在子类中正确覆盖。但是,目前我无法调用其各自类的CFunc()方法和DFunc()方法。所以我脑海中有两个选择 -
要么在子类中将CFunc()和friends包装在某个已定义的虚拟方法中,以便它们一起执行。但是随着独特方法数量的增加,我将失去对特定执行的控制。
或者在基类中提供一些纯虚方法,就像void process() = 0,并让它们按照他们的意愿在子类中定义。可能会被一些人留空void process(){}并被一些人使用。但是,与前一个选项类似,我失去了返回值和参数。并且像之前的选项一样,如果某个子类中有更多的方法,则这不是解决问题的正确方式。
还有另一个选择 - dynamic_cast<>?在这里这是一个好的选择吗 - 将父指针转换回子指针(顺便说一下,我在这里使用智能指针,所以只允许unique/shared)然后调用所需的函数。但是我如何区分不同的子类?另一个公共成员,可以返回一些独特的类枚举值吗?
我对这种情况非常不熟悉,希望得到反馈。我应该如何解决这个问题?

https://akrzemi1.wordpress.com/2016/02/27/another-polymorphism/ - hg_git
2
你想用CFuncDFunc解决什么问题?在C++中,“我想要做这个,但它的效果不好”通常表明这个并不是你实际问题的正确解决方案。 - kfsone
@AbhinavGauniyal 看起来在你的情况下,除了调用CFunc之外,你可能还有其他考虑因素。你是否可以使用指向"ThingsWithCFunc"的额外容器指针?你一定要使用基类指针来访问它吗?你的第二个解决方案非常接近"模板方法模式",但你说你在其中丢失了返回值和参数——这很难计算进去,因为你的示例代码既没有返回值也没有参数,一个更现实的示例可能会有所帮助。 - kfsone
1
@Nikopol基类对应于其对象和实际硬件之间的映射,通过gpio进行通信,子类是这些硬件。它们共享许多公共代码,因为读写接口在它们之间是共同的,但是一些方法可能因不同类型的硬件而异。考虑一个LED、温度传感器、蜂鸣器。所有这些都有on()off()read()write()和大量相似的方法,但是温度传感器有一个不同的方法-int getTemp(),蜂鸣器也是如此-void make_sound()。我可以将它们的原型始终放在基类中作为纯虚拟的,但这样做不会... - Abhinav Gauniyal
1
@AbhinavGauniyal 好的,听起来访问者模式在这里不适用。为什么不为每种功能定义接口(纯方法)呢?您可以首先定义一个“HardwareInterface”,其中包含所有设备的“on()”、“off()”、“read()”、“write()”。定义“SensorInterface”,其中只有“get_value()”方法(用于温度、光传感器等),定义“AlertInterface”,其中只有“alert()”方法(用于蜂鸣器、振动器等)。然后,“Buzzer”类将从“HardwareInterface”和“AlertInterface”继承。这种方法对您是否有效? - Nikopol
显示剩余9条评论
8个回答

11
我有一个父类,有两个或更多的子类继承它...但随着独特方法数量的增加,我将失去对特定执行的控制。
另一个选项是在方法数量预计增加且派生类预计相对稳定时使用访问者模式。以下示例使用boost::variant
假设你从以下三个类开始:
#include <memory>
#include <iostream>

using namespace std;
using namespace boost;

class b{};
class c : public b{};
class d : public b{};

不使用指向基类 b 的(智能)指针,而是使用变体类型:

using variant_t = variant<c, d>;

和变量变体:

variant_t v{c{}};

现在,如果你想要分别处理cd方法,你可以使用:

struct unique_visitor : public boost::static_visitor<void> {
    void operator()(c c_) const { cout << "c" << endl; };
    void operator()(d d_) const { cout << "d" << endl; };
};

你会用以下方式调用

apply_visitor(unique_visitor{}, v);

请注意,您还可以使用相同的机制来统一处理所有类型,方法是使用接受基类的访问者。
struct common_visitor : public boost::static_visitor<void> {
    void operator()(b b_) const { cout << "b" << endl; };
};

apply_visitor(common_visitor{}, v);

请注意,如果类的数量增长速度快于方法的数量,这种方法将导致维护问题。

完整代码:

#include "boost/variant.hpp"
#include <iostream>

using namespace std;
using namespace boost;

class b{};
class c : public b{};
class d : public b{};

using variant_t = variant<c, d>;

struct unique_visitor : public boost::static_visitor<void> {
    void operator()(c c_) const { cout << "c" << endl; };
    void operator()(d d_) const { cout << "d" << endl; };
};

struct common_visitor : public boost::static_visitor<void> {
    void operator()(b b_) const { cout << "b" << endl; };
};

int main() {
    variant_t v{c{}};
    apply_visitor(unique_visitor{}, v);
    apply_visitor(common_visitor{}, v);
}

哎呀,我的容器跑到哪里去了 - std :: vector <std :: unique_ptr <B>> v;?到目前为止,我理解 variant_t 捕获不同的变体类,unique_visitor 积累不同的方法并应用正确的方法,而common_visitor 应用公共方法。但是我需要我的容器用于其他目的,所以我在将此示例与我的用例相关联方面遇到了一些困难。 - Abhinav Gauniyal
还有一个了解这个方法的额外开销会很好。 - Abhinav Gauniyal
1
@AbhinavGauniyal 很好的观点。关于你的第一个观点,就像单个unique_ptr被单个variant_t替换一样,那么前者的vector可以替换后者的vector。关于你的第二个观点,可能会因人而异。访问者的成本可能是双重分派的成本。然而,类bcd本身不包含任何virtual函数,因此当您知道类型时,直接调用可能更便宜。同样,访问者在某些情况下是适用的 - 它并不适用于每种情况。 - Ami Tavory
这不是双重派遣,而是单一派遣。您可能还希望访问者通过引用来获取参数。否则,在这里使用 variant 绝对是正确的方法 +1。 - Barry
还有一种选择是将变体性提升一个级别,这样你就不会有一个变体向量,而是一个向量的变体。 - Neil Gatenby
显示剩余2条评论

10
您可以针对每个设备类使用纯方法声明接口。当您定义特定设备的实现时,只继承与其相关的接口。
使用您定义的接口,您可以迭代并调用特定于每个设备类的方法。
在下面的示例中,我声明了一个HardwareInterface,将被所有设备继承,以及一个AlertInterface,只会被能够物理警报用户的硬件设备继承。也可以定义其他类似的接口,例如SensorInterface、LEDInterface等。
#include <iostream>
#include <memory>
#include <vector>

class HardwareInteface {
    public:
        virtual void on() = 0;
        virtual void off() = 0;
        virtual char read() = 0;
        virtual void write(char byte) = 0;
};

class AlertInterface {
    public:
        virtual void alert() = 0;
};

class Buzzer : public HardwareInteface, public AlertInterface {
    public:
        virtual void on();
        virtual void off();
        virtual char read();
        virtual void write(char byte);
        virtual void alert();
};

void Buzzer::on() {
    std::cout << "Buzzer on!" << std::endl;
}

void Buzzer::off() {
    /* TODO */
}

char Buzzer::read() {
    return 0;
}

void Buzzer::write(char byte) {
    /* TODO */
}

void Buzzer::alert() {
    std::cout << "Buzz!" << std::endl;
}

class Vibrator : public HardwareInteface, public AlertInterface {
    public:
        virtual void on();
        virtual void off();
        virtual char read();
        virtual void write(char byte);
        virtual void alert();
};

void Vibrator::on() {
    std::cout << "Vibrator on!" << std::endl;
}

void Vibrator::off() {
    /* TODO */
}

char Vibrator::read() {
    return 0;
}

void Vibrator::write(char byte) {
    /* TODO */
}

void Vibrator::alert() {
    std::cout << "Vibrate!" << std::endl;
}

int main(void) {
    std::shared_ptr<Buzzer> buzzer = std::make_shared<Buzzer>();
    std::shared_ptr<Vibrator> vibrator = std::make_shared<Vibrator>();

    std::vector<std::shared_ptr<HardwareInteface>> hardware;
    hardware.push_back(buzzer);
    hardware.push_back(vibrator);

    std::vector<std::shared_ptr<AlertInterface>> alerters;
    alerters.push_back(buzzer);
    alerters.push_back(vibrator);

    for (auto device : hardware)
        device->on();

    for (auto alerter : alerters)
        alerter->alert();

    return 0;
}

接口可以根据每个传感器类型更具体,例如: AccelerometerInterface(加速度计接口),GyroscopeInterface(陀螺仪接口)等。


2
我认为这种方法存在的问题是现在 OP 必须维护多个容器,而不是像示例中给出的单个容器。但这只适用于插入和删除,因为更新将反映在 shared<ptr> 上,而不是对象的副本上。 - hg_git
@hg_git 实际上,采用这种方法,原始问题要么需要至少维护一个容器每个接口或进行昂贵的“dynamic_cast”。关于插入/删除,我想这取决于系统是否支持热插拔。在我看来,这种设计更清晰,可以轻松扩展。此外,如果需要,您可以轻松添加访问者模式。 - Nikopol
2
拥有多个容器的“问题”并不是问题,而是优势。在此之前,所有项目都必须进行转换、测试,最后才能使用,现在只有实现接口的项目才会被迭代。话虽如此,如果不知道这些项目为什么在容器中以及它们将如何使用,就很难确定。 - UKMonkey

8
虽然你所要求的是可能的,但这将导致你的代码散布着转换,或者有些类中存在不合理的函数。这两种情况都不可取。
多态的整个意义在于使用B的东西不需要知道它到底是什么样的B。对我来说,听起来你正在扩展类而不是将它们作为成员,即“C是B”没有意义,但“C有B”有意义。
我建议重新考虑B、C、D和所有未来项的功能以及它们为什么有这些需要调用的唯一函数,并研究一下函数重载是否真正符合你的需求。(与Ami Tavory的访问者模式类似)

6

你可以使用unique_ptr.get()来获取Unique Pointer中的指针,然后像普通指针一样使用该指针。像这样:

for (auto &el : v) {
        el->tell();
        D* pd = dynamic_cast<D*>(el.get());
        if (pd != nullptr)
        {
            pd->DFunc();
        }
        C* pc = dynamic_cast<C*>(el.get());
        if (pc != nullptr)
        {
            pc->CFunc();
        }
    }

结果是这样的:

CHILD C
Can be called only from C
CHILD D
Can be called only from D

5
  • 如果可能的话,您应该使用第一种方法来隐藏尽可能多的与类型特定实现相关的细节。

  • 然后,如果您需要公共接口,应该使用虚函数(第二种方法),并避免使用dynamic_cast(第三种方法)。许多线程可以告诉您为什么(例如:Polymorphism vs DownCasting)。而且您已经提到了一个很好的原因,即您不应该真正检查对象类型...

  • 如果您对虚函数有问题,因为您的派生类具有太多唯一的公共接口,则它不是IS-A关系,是时候重新审查您的设计了。例如,对于共享功能,请考虑组合,而不是继承...


dynamic_cast 的确切问题是什么?是因为它有轻微的开销还是其他原因?如果我可以使用 static_cast 代替 dynamic_cast,那么还会有任何问题吗? - Abhinav Gauniyal
是的,有很多原因使它变得混乱..在我看来最主要的两个原因是:1- 在执行特定类型操作之前检查对象类型(例如,它是A、B还是C?)的条件不仅速度慢,而且容易出错,因为每次添加新的对象类型时,您都必须检查所有这些条件(对于所有派生类、容器等)以查看它们是否需要更新。2- 创建依赖于所有派生类头文件和唯一接口的依赖关系,而不是通过基类进行统一访问。 - HazemGomaa

4

关于访问者模式,有很多评论(在OP和Ami Tavory的答案中)。

考虑到OP的问题,我认为这是一个可以接受的答案,即使访问者模式有缺点,它也有优点(参见此主题:What are the actual advantages of the visitor pattern? What are the alternatives?)。基本上,如果你以后需要添加一个新的子类,模式实现将迫使你考虑所有需要针对该新类采取特定操作的情况(编译器将强制您为所有现有的访问者子类实现新的特定visit方法)。

一个简单的实现(不使用boost):

#include <iostream>
#include <string>
#include <vector>
#include <memory>

class C;
class D;
class Visitor
{
    public:
    virtual ~Visitor() {}
    virtual void visitC( C& c ) = 0;
    virtual void visitD( D& d ) = 0;
};


class B{
    private: int a; int b;
    public: B(const int _a, const int _b) : a(_a), b(_b){}
    virtual void tell(){ std::cout << "BASE" << std::endl; }
    virtual void Accept( Visitor& v ) = 0; // force child class to handle the visitor
};

class C : public B{
    std::string s;
    public: C(int _a, int _b, std::string _s) : B(_a, _b), s(_s){}
    void tell() override { std::cout << "CHILD C" << std::endl; }
    void CFunc() {std::cout << "Can be called only from C" << std::endl;}
    virtual void Accept( Visitor& v ) { v.visitC( *this ); }
};

class D : public B{
    double d;
    public: D(int _a, int _b, double _d) : B(_a, _b), d(_d){}
    void tell() override { std::cout << "CHILD D" << std::endl; }
    void DFunc() {std::cout << "Can be called only from D" << std::endl;}
    virtual void Accept( Visitor& v ) { v.visitD( *this ); }
};

int main() {
    std::vector<std::unique_ptr<B>> v;

    v.push_back(std::make_unique<C>(1,2, "boom"));
    v.push_back(std::make_unique<D>(1,2, 44.3));

    // declare a new visitor every time you need a child-specific operation to be done
    class callFuncVisitor : public Visitor
    {
        public:
        callFuncVisitor() {}

        virtual void visitC( C& c )
        {
            c.CFunc();
        }
        virtual void visitD( D& d )
        {
            d.DFunc();
        }
    };

    callFuncVisitor visitor;
    for(auto &el: v){
        el->Accept(visitor);
    }
    return 0;
}

实时演示:https://ideone.com/JshiO6


4

动态转换是绝对的最后手段。通常情况下,它被用来克服一个无法安全修改的设计不良的库。

需要这种支持的唯一原因是当您需要在集合中共存父类和子类实例时。是吧?多态性的逻辑表明所有不能在父类中逻辑上存在的特化方法应该从在父类中逻辑上存在的方法中引用。

换句话说,完全可以有子类方法在父类中不存在,以支持虚拟方法的实现。

任务队列实现是典型的例子(见下文)。特殊的方法支持主要的run()方法。这允许将一堆任务推入队列并执行,没有强制类型转换、没有访问者、只有简洁易懂的代码。

// INCOMPLETE CODE
class Task
    {
    public:
        virtual void run()= 0;
    };

class PrintTask : public Task
    {
    private:
        void printstuff()
            {
            // printing magic
            }

    public:
        void run()
        {
        printstuff();
        }
    };

class EmailTask : public Task
    {
    private:
        void SendMail()
            {
            // send mail magic
            }
    public:
        void run()
            {
            SendMail();
            }
    };

class SaveTask : public Task
    private:
        void SaveStuff()
            {
            // save stuff magic
            }
    public:
        void run()
            {
            SaveStuff();
            }
    };

1

以下是一种“相对较好”的简单实现方式。

关键点:

我们避免在 push_back() 过程中丢失类型信息。

可以轻松添加新的派生类。

内存释放符合预期。

易于阅读和维护,这是有争议的。

struct BPtr
{
    B* bPtr;

    std::unique_ptr<C> cPtr;
    BPtr(std::unique_ptr<C>& p) : cPtr(p), bPtr(cPtr.get())
    {  }

    std::unique_ptr<D> dPtr;
    BPtr(std::unique_ptr<D>& p) : dPtr(p), bPtr(dPtr.get())
    {  }
};

int main()
{
    std::vector<BPtr> v;

    v.push_back(BPtr(std::make_unique<C>(1,2, "boom")));
    v.push_back(BPtr(std::make_unique<D>(1,2, 44.3)));

    for(auto &el: v){

        el.bPtr->tell();

        if(el.cPtr) {
            el.cPtr->CFunc();
        }

        if(el.dPtr) {
            el.dPtr->DFunc();
        }
    }

    return 0;
}

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