Подтвердить что ты не робот

Вызов дочерних методов из родительского указателя с разными дочерними классами

У меня есть родительский класс с 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<>?. Будет ли это приятным вариантом здесь - отбрасывание родительского указателя на дочерний указатель (btw Я использую интеллектуальные указатели здесь, поэтому разрешено только unique/shared), а затем вызывается требуемая функция. Но как бы я дифференцировал b/w разные дочерние классы? Другой публичный член, который может вернуть некоторое уникальное значение перечисления класса?

Я совершенно не ознакомлен с этим сценарием и хотел бы получить некоторые отзывы. Как мне подойти к этой проблеме?

4b9b3361

Ответ 1

Вы можете объявлять интерфейсы с чистыми методами для каждого класса устройств. Когда вы определяете конкретную реализацию устройства, вы наследуете только от интерфейсов, которые имеют смысл для него.

Используя определяемые интерфейсы, вы можете затем перебирать и вызывать методы, специфичные для каждого класса устройств.

В следующем примере я объявил 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

У меня есть родительский класс с 2 или более дочерними классами, полученными из него... Но я потеряю контроль над конкретным выполнением уникальных методов по мере увеличения их числа.

Другой вариант, полезный, когда ожидается увеличение количества методов, и ожидается, что производные классы останутся относительно стабильными, заключается в использовании шаблона visitor. Следующие действия используются 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{}};

Теперь, если вы хотите по-разному обрабатывать методы 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; };
};

который вы вызываете с помощью

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

Ответ 3

В то время как то, что вы просите, возможно, это либо приведет к разбросу кода с помощью бросков, либо функций, доступных для классов, которые не имеют смысла. Оба они нежелательны. Если вам нужно знать, является ли это классом C или D, то, скорее всего, либо он сохранит его как B, либо неправильно, либо ваш интерфейс B не прав.

Вся суть полиморфизма заключается в том, что вещи, использующие B, - это то, что им не нужно точно знать, что это такое. Для меня это звучит так, будто вы расширяете классы, а не как их члены, т.е. "C - это B" не имеет смысла, но "C имеет B" ).

Я хотел бы вернуться и пересмотреть то, что делают B, C, D и все будущие элементы, и почему у них есть эти уникальные функции, которые вам нужно вызвать; и посмотрите, действительно ли перегрузка функций - это то, что вы действительно хотите сделать. (Подобно предложению Ami Tavory шаблона посетителя)

Ответ 4

вы можете использовать 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

Ответ 5

  • Вы должны использовать свой 1-й подход, если можете скрыть как можно больше подробностей реализации типа.

  • Затем, если вам нужны публичные интерфейсы, вы должны использовать виртуальные функции (ваш второй подход) и избегать dynamic_cast (ваш третий подход). Многие ады могут рассказать вам, почему (например, Полиморфизм против DownCasting). и вы уже упоминали одну вескую причину, то есть вы не должны действительно проверять тип объекта...

  • Если у вас есть проблемы с виртуальными функциями, потому что ваши drived классы имеют слишком много уникальных открытых интерфейсов, то это не отношения IS-A, и вам нужно рассмотреть ваш дизайн. Например, для совместной работы рассмотрите состав, а не наследование...

Ответ 6

Было много комментариев (в ответах 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;
}

Live demo: https://ideone.com/JshiO6

Ответ 7

Динамическое кастинг - инструмент абсолютного последнего средства. Он обычно используется, когда вы пытаетесь преодолеть плохо разработанную библиотеку, которую нельзя безопасно модифицировать.

Единственная причина, по которой нужна такая поддержка, - это когда вы хотите, чтобы родительские и дочерние экземпляры сосуществовали в коллекции. Правильно? Логика полиморфизма говорит, что все методы специализации, которые не могут логически существовать в родительском, должны ссылаться из методов, которые логически существуют в родительском.

Другими словами, совершенно нормально иметь дочерние методы класса, которых нет в родительском объекте для поддержки реализации виртуального метода.

Реализация очереди задач - это пример квинтэссенции (см. ниже) Специальные методы поддерживают метод 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();
            }
    };

Ответ 8

Здесь "менее плохой" способ сделать это, сохраняя его простым.

Ключевые моменты:

Мы избегаем потери информации о типе во время 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;
}