繁体   English   中英

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

[英]calling child methods from parent pointer with different child classes

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

#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()和朋友,以便它一起执行。 但随着数量的增加,我将无法控制特定方法的特定执行。

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

和另一个 -

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

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

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

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

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

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

在下面的示例中,我声明了一个将由所有设备继承的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等。

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

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

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

你可以使用unique_ptr.get()来获取Unique Pointer中的指针,并使用指针作为normall。 像这样:

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
  • 如果可以隐藏尽可能多的特定于类型的实现细节,则应使用第一种方法。

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

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

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

我认为这是可接受的答案(考虑OP问题),即使访问者模式有缺点,它也有优势(参见本主题: 访问者模式的实际优势是什么?有哪些替代方案? )。 基本上,如果您以后需要添加一个新的子类,模式实现将强制您考虑必须采取针对此新类的特定操作的所有情况(编译器将强制您为所有实现新的特定访问方法)您现有的访客子类)。

一个简单的实现(没有提升):

#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

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

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

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

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

这是一种“不太糟糕”的做法,同时保持简单。

关键点:

我们避免在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;
}

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM