Сделать этот код правильно полиморфным

У меня есть абстрактный класс Parent, который имеет несколько дочерних элементов и пустые функции для взаимодействия с каждым из этих дочерних элементов. Каждый Дочерний элемент переопределяет функции Родителя и по-разному взаимодействует с другими Дочерними элементами; т. е. Child1 имеет разные реализации для взаимодействия с (Child1), взаимодействия с (Child2) и т. д. и т. д.

В Parent у меня есть функция interface_with(Parent foo). Каждый ребенок, желающий взаимодействовать с другим ребенком, должен сначала пройти через эту функцию. До сих пор все было хорошо, но затем мы сталкиваемся с проблемой: после обработки некоторой базовой логики дочернему элементу необходимо знать конкретный тип своего параметра, чтобы он мог продолжить и вызвать свою собственную переопределенную функцию. На данный момент у меня есть это:

Child1* child1 = dynamic_cast<Child1*>(foo);
Child2* child2 = dynamic_cast<Child2*>(foo);
Child3* child3 = dynamic_cast<Child3*>(foo);

if(child1 != nullptr){
    interact_with(child1)
}

else if(child2 != nullptr){
    interact_with(child2)
}

else if(child3 != nullptr){
    interact_with(child3)
}

Это работает, но это не очень хорошее решение. Особенно плохо становится, когда у меня так много занятий. Указывает ли это на несовершенство базовой конструкции, и если да, то как я могу это исправить?

РЕДАКТИРОВАТЬ: Чтобы уточнить: у меня есть что-то вроде этого

//Parent is an abstract class
class Parent
{
    void interact_with(Parent* foo){
        //this is here because there is a lengthy code segment
        //that needs to be run no matter what child interacts
        //with which

        //afterwards, I need to interact with whatever foo really is
    }

    virtual void interact_with(Child1* child){*blank*};
    virtual void interact_with(Child2* child){*blank*};
    virtual void interact_with(Child3) child){*blank*};
};

class Child1 : public Parent
{
    virtual void interact_with(Child1* child){*do something*};
    virtual void interact_with(Child2* child){*do something else*};
    virtual void interact_with(Child3* child){*do something else*};
};

Уже.


person Bluejay    schedule 26.02.2015    source источник
comment
Я полагаю, о виртуальных методах не может быть и речи?   -  person imreal    schedule 26.02.2015
comment
См. этот вопрос: stackoverflow.com/questions/ 9818132/ . Не совсем дубликат, но может помочь вашему мысленному процессу, поскольку это определенно проблема, которую вы пытаетесь решить (шаблон посетителя/двойная отправка).   -  person aruisdante    schedule 26.02.2015


Ответы (4)


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

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

См. следующий рабочий пример:

#include <iostream>

class Child1;
class Child2;

class Parent
{
public:
    virtual void interact_with(Parent* other) = 0;
    virtual void interact_with(Child1* child) {};
    virtual void interact_with(Child2* child) {};
};

class Child1 : public Parent
{
public:
    virtual void interact_with(Parent* other)
    {
        other->interact_with(this);
    }
    virtual void interact_with(Child1* child)
    {
        std::cout << "Child1 - Child1\n";
    }
    virtual void interact_with(Child2* child)
    {
        std::cout << "Child1 - Child2\n";
    }
};

class Child2 : public Parent
{
public:
    virtual void interact_with(Parent* other)
    {
        other->interact_with(this);
    }
    virtual void interact_with(Child1* child)
    {
        std::cout << "Child2 - Child1\n";
    }
    virtual void interact_with(Child2* child)
    {
        std::cout << "Child2 - Child2\n";
    }
};

int main()
{
    Child1 c1;
    Parent* p1 = &c1; // upcast to parent, from p1, we don't know the child type
    Child2 c2;
    Parent* p2 = &c2;

    c1.interact_with(&c2); // single virtual call to Child1 - Child2
    p1->interact_with(&c2); // single virtual call to Child1 - Child2
    p1->interact_with(p2); // double virtual call to Child2 - Child1 (NB: reversed interaction)
}

Он выводит:

Child1 - Child2
Child1 - Child2
Child2 - Child1

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

person André Sassi    schedule 26.02.2015
comment
Это решение прекрасно работает. Конечно, мне приходится создавать идентичные виртуальные функции для каждого класса, но это избавляет от уродливого динамического приведения без существенного влияния на остальную часть проекта. - person Bluejay; 27.02.2015

Предупреждение: это решение нарушит работу ваших интерфейсов. Но не волнуйтесь, они уже сломаны ;)

На мой взгляд, ваша ошибка проектирования заключается в следующем: хотя все дети «равны», вы выбираете одного из них, который будет отвечать за взаимодействия, и вызываете на нем метод (и если вы хотите 3, 4, ..., N равных детей (в массив) для одновременного взаимодействия, какой из них отвечает?)

Если в вашем приложении все объекты одинаково важны и ни один объект не отвечает за взаимодействия, вам следует перенести взаимодействия в свободные перегруженные бинарные функции:

void interact(Child1* a, Child1* b);

void interact(Child1* a, Child2* b);

...

void interact(Child2* a, Child1* b)
{
    interact(b, a); // if order does not matter, reuse another function 
}

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

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

person Ivan Aksamentov - Drop    schedule 26.02.2015
comment
Вы поднимаете некоторые хорошие моменты. Несмотря на то, что взаимодействие всегда инициируется одной стороной и одновременно может происходить только одно, мне нравится ваш подход. Тем не менее, я не думаю, что на данном этапе будет много пользы от изменения всей структуры моей программы, тем более что, как вы сказали, это не решит проблему шаблонного кода. Внутреннее устройство функций может значительно различаться, и порядок имеет значение, поэтому я думаю, что шаблоны также будут иметь ограниченную полезность. Пока кажется, что придерживаться моего текущего метода литья было бы наименее напряженным и самым простым способом :-) - person Bluejay; 27.02.2015

Как вы сказали, такое использование dynamic_cast‹> — плохой дизайн. Вы должны сделать функцию interface_with виртуальной в объявлении.

virtual void interact_with(Parent foo);

Это заставит вызов метода использовать реализацию интерактивного_с подкласса вместо реализации родительского класса. Затем вы можете заменить все, что вы написали, только этим.

interact_with(foo);

Вот довольно хорошее объяснение виртуальных методов.

person Sam    schedule 26.02.2015
comment
Прошу прощения, если я не совсем правильно объяснил. Я уже использую виртуальные функции. В Parent у меня есть виртуальные функции для взаимодействия с (дочерним) для всех дочерних элементов. Однако независимо от того, какой ребенок взаимодействует с каким другим ребенком, в первую очередь необходимо выполнить некоторую логику. После этого необходимо знать тип дочернего элемента foo, чтобы можно было вызвать правильный метод взаимодействия с(); если foo действительно является дочерним элементом4, это не проблема, поскольку у меня есть виртуальная функция для взаимодействия с дочерним элементом4, но в этот момент мы знаем только, что это своего рода родитель, а не какой именно дочерний элемент. - person Bluejay; 27.02.2015
comment
Я не понимаю структуру вашей программы. К каким классам принадлежит interface_with(Parent foo)? Есть ли еще одна функция с именем interface_with()? Если да, то к какому классу он относится? - person Sam; 27.02.2015
comment
Есть родительский класс, а есть дети. Дети по-разному взаимодействуют друг с другом. Родительский элемент содержит функцию взаимодействия с (родительским) вместе с функцией взаимодействия с (дочерним) для каждого дочернего типа, которые являются виртуальными и изменяются в каждой реализации дочернего элемента. Идея состоит в том, что каждый дочерний элемент взаимодействует с другим дочерним элементом по-своему, но прежде чем это сделать, необходимо выполнить некоторую логику, независимо от того, с каким дочерним типом он взаимодействует — следовательно, взаимодействие_с (родительский foo). - person Bluejay; 03.03.2015

Вам нужна двойная отправка

Существует множество подходов, которые вы можете использовать в зависимости от ваших требований. Очень общим является наличие простой карты формы Ключ (тип, тип) -> Значение (функция), которая связывает пару типов аргументов с вызываемой функцией.

В этом случае вам понадобится набор бесплатных функций типа void function(Parent*, Parent*) (по одной для каждой необходимой вам комбинации) и карта типа std::unordered_map<std::pair<TypeId, TypeId>, FunctionType>, где TypeId — некоторая форма идентификатора типа с семантикой значений.

Затем вы выполняете отправку во время выполнения:

if(map_.find(make_pair(type1, type2)) != map_.end())
    map_[make_pair(type1, type2)](obj1, obj2);

Не перед регистрацией каждой функции:

map_[make_pair(type1, type2)] = func12;
map_[make_pair(type2, type3)] = func23;
....
person imreal    schedule 26.02.2015
comment
Я узнал что-то новое сегодня с двойной скоростью. Однако кажется, что в моем конкретном случае это было бы излишним, особенно с учетом большого количества комбинаций - в качестве альтернативы, сохраняя мой текущий метод, все, что мне нужно сделать, это добавить один оператор cast/else if для каждого класса. В этот момент я могу сделать что-то вроде того, что предложил Дроп. - person Bluejay; 27.02.2015