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

时间:2016-09-18 21:57:54

标签: c++ class oop c++11 inheritance

我有一个包含2个或更多子类的父类。随着更多要求的出现,未来不同子类的数量可能会增加,但它们都将遵循基类方案,并且将包含很少的独特方法。让我举一个例子 -

#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()方法。所以我脑子里有两个选择 -

  • packup CFunc()和朋友在子类中已经定义的一些虚拟方法中,以便它们一起执行。但随着数量的增加,我将无法控制特定方法的执行。

  • 或在基类中提供一些纯虚方法,就像void process() = 0一样,让它们在子类中定义。可能会被某些人留空void process(){}并被某些人使用。但是再一次感觉不对,因为我在途中失去了回报价值和参数。也像之前的选项一样,如果某些子类中有更多方法,这种方法感觉不正确。

和另一个 -

  • dynamic_cast<>?这会是一个不错的选择 - 回滚父指针指向子指针(顺便说一句,我在这里使用智能指针,所以只允许unique/shared),然后调用所需的函数。但是,我如何区分不同的子类?另一个可能返回一些唯一类枚举值的公共成员?

我对这种情况非常缺乏经验,并希望得到一些反馈。我该如何处理这个问题?

8 个答案:

答案 0 :(得分:11)

  

我有一个父类,有两个或更多的子类派生出来......但是随着数量的增加,我将失去对特殊方法的特定执行的控制。

另一个选项,当预计方法数量增加时有用,并且派生类预计保持相对稳定,是使用visitor pattern。以下使用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);
}

答案 1 :(得分:9)

您可以为每个设备类声明具有纯方法的接口。定义特定设备实现时,只从有意义的接口继承。

使用您定义的接口,您可以迭代并调用特定于每个设备类的方法。

在下面的示例中,我声明了HardwareInterface将由所有设备继承,而AlertInterface将仅由可以物理警告用户的硬件设备继承。可以定义其他类似的接口,例如SensorInterfaceLEDInterface

#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;
}

根据各个传感器类型,接口可以更加具体:AccelerometerInterfaceGyroscopeInterface等。

答案 2 :(得分:8)

虽然你的要求是可能的,但它会导致你的代码散布在演员阵容中,或者在没有意义的课程上可用的函数。两者都不可取。 如果您需要知道它是C类还是D类,那么很可能将它存储为B是错误的,或者您的接口B是错误的。

多态性的全部意义在于使用B的事情是他们不需要确切地知道它是什么类型的B.对我来说,这听起来像是在扩展课程而不是将它们作为成员,即&#34; C是B&#34;没有意义,但&#34; C有B做&#34;。

我会回过头来重新考虑B,C,D和所有未来项目的作用,以及为什么他们拥有您需要调用的这些独特功能;并研究函数重载是否是您真正想要做的。 (类似于Ami Tavory对访客模式的建议)

答案 3 :(得分:6)

您可以使用$scope.showme = true获取唯一指针中的指针,并将指针用作normall。像这样:

unique_ptr.get()

结果如下:

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();
        }
    }

答案 4 :(得分:5)

  • 如果您可以隐藏尽可能多的特定于类型的实现细节,则应该使用第一种方法。

  • 然后,如果你需要公共接口,你应该使用虚拟功能(你的第二种方法),并避免使用dynamic_cast(你的第三种方法)。许多theads可以告诉你原因(例如Polymorphism vs DownCasting)。你已经提到了一个很好的理由,那就是你不应该真正检查对象类型......

  • 如果您的虚拟功能存在问题,因为您的驱动类有太多独特的公共接口,那么它不是IS-A关系,是时候审查您的设计了。例如,对于共享功能,请考虑组合,而不是继承......

答案 5 :(得分:4)

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

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

易于实施(无需提升):

#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

答案 6 :(得分:4)

动态施法是绝对不得已的工具。当您尝试克服设计不良的无法安全修改的库时,通常会使用它。

需要此类支持的唯一原因是您需要父集合和子集实例共存于集合中。对?多态的逻辑说明父级中逻辑上不存在的所有特化方法都应该从父级逻辑上存在的方法中引用。

换句话说,拥有父类中不存在的子类方法来支持虚方法的实现是完全没问题的。

任务队列实现是典型的例子(见下文) 特殊方法支持primary 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();
            }
    };

答案 7 :(得分: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;
}