Яблоки, апельсины и указатели на самый производный класс C++

Предположим, у меня есть куча фруктов:

class Fruit { ... };
class Apple : public Fruit { ... };
class Orange: public Fruit { ... };

И некоторые полиморфные функции, которые действуют на указанный фрукт:

void Eat(Fruit* f, Pesticide* p)   { ... }
void Eat(Apple* f, Pesticide* p)   { ingest(f,p); }
void Eat(Orange* f, Pesticide* p)   { peel(f,p); ingest(f,p); }

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

К сожалению, то, что я хочу сделать дальше, это именно то, что позволяют функции-члены и динамическое связывание:

typedef list<Fruit*> Fruits;
Fruits fs;
...
for(Fruits::iterator i=fs.begin(), e=fs.end(); i!=e; ++i)
    Eat(*i);

И, очевидно, проблема здесь в том, что указатель, который мы передаем Eat(), будет Fruit*, а не Apple* или Orange*, поэтому ничего не съест, и мы все будем очень голодны.

Итак, что я действительно хочу сделать вместо этого:

Eat(*i);

это:

Eat(MAGIC_CAST_TO_MOST_DERIVED_CLASS(*i));

Но, насколько мне известно, такой магии не существует, за исключением, возможно, в форме большого противного оператора if, полного вызовов dynamic_cast.

Так есть ли какая-то магия во время выполнения, о которой я не знаю? Или я должен реализовать и поддерживать большое противное выражение if, полное dynamic_casts? Или я должен смириться с этим, перестать думать о том, как бы это реализовать в Ruby, и позволить небольшому количеству пестицидов проникнуть в мою колбу с фруктами?

Обновление: вместо надуманного бита с функциями Eat и Pesticide, предположим, вместо этого я просто не хочу употреблять Eat в фрукты, потому что это не имеет смысла. Фрукт, который умеет себя есть? Тьфу. Вместо этого мне нужен класс Eater с функцией Eat, с различным кодом для еды каждого вида фруктов и некоторым кодом по умолчанию на тот случай, если этот фрукт не распознается едоком:

class Eater
{
public:
  void Eat(Apple* f) { wash(); nom(); }
  void Eat(Orange* f) { peel(); nom(); }
  void Eat(Fruit* f) { nibble(); }
};
...
Eater me;
for(Fruits::iterator i=fs.begin(), e=fs.end(); i!=e; ++i)
  me.Eat(*i);  //me tarzan! me eat!

Но, опять же, это не работает, и простое решение в C++ похоже на кучу вызовов dynamic_cast.

Однако, как предполагает один из ответов, может быть другое умное решение. Что если Fruits продемонстрирует качества, которые имеют значение для едоков, с такими функциями, как MustPeel() и MustWash()? Тогда вы могли бы обойтись с одной функцией Eat()...

Обновление: Дэниел Ньюби отмечает, что использование Visitor также решает проблему в том виде, в котором она представлена ​​... но для этого требуется немного семантической стойки на голове (Fruit::use или Fruit::beEaten?).

Хотя я хотел бы принять несколько ответов, я думаю, что ответ psmears на самом деле лучший для будущих читателей. Спасибо всем.

5 ответов

Решение

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

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

Например, вы можете добавить новые виртуальные методы "IsEdible" и "PrepareForEating". Затем вы можете реализовать их для каждого фрукта и реализовать один универсальный метод Eat, который работает для всех фруктов - и также принимает противный пестицид - и все без того, чтобы классы Fruit знали об этом.

Конечно, в зависимости от ваших точных целей, это может быть совершенно неуместно - вот почему вам придется прояснить пример в своей голове:-)

Вам нужно изменить дизайн. А именно, делать все, что вы, кажется, избегаете (по какой причине, кто знает.)

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

// fruit.h
class Pesticide; // you don't need a complete type

struct Fruit
{
    virtual void Eat(Pesticide*) = 0;
};

// apple.h
class Apple : public Fruit
{
    void Eat(Pesticide* p) { ... }
};

// orange.h
class Orange : public Fruit
{
    void Eat(Pesticide* p) { ... }
};

Если вы все еще хотите бесплатную функцию *:

void Eat(Fruit* f, Pesticide* p)   { f->Eat(p); }

* Обратите внимание, что ваш пост уже свидетельствует о плохом дизайне; а именно первый Eat функция:

void Eat(Fruit* f, Pesticide* p)   { }

Когда ничего не делать с фруктом равносильно его употреблению? Чистая виртуальная функция - намного лучший выбор интерфейса.

Просто используйте Я стою прямо здесь! Шаблон. Это как шаблон посетителя, но без контейнера.

// fruit.h
class Fruit;
class Apple;
class Orange;

class Fruit_user {
    public:
        Fruit_user();
        virtual ~Fruit_user();
        virtual use(Apple *f) = 0;
        virtual use(Orange *f) = 0;
};

class Fruit {
    public:
        // Somebody with strong template fu could probably do
        // it all here.
        virtual void use(Fruit_user *fu) = 0;
};

class Apple : public Fruit {
    public:
        virtual void use(Fruit_user *fu) {
            fu->use(this);
        }
};

class Orange: public Fruit {
    public:
        virtual void use(Fruit_user *fu) {
            fu->use(this); 
        }
};


// dow-chemical.h
class Pesticide_fruit_user : public Fruit_user {
    public:
        Pesticide_fruit_user(Pesticide *p) {
            p_ = p;
        }

        virtual void use(Apple *f) { ingest(f, p_); }
        virtual void use(Orange *f) { peel(f, p_); ingest(f, p_); }

    private:
        Pesticide *p_;
};

То, что вы просите, невозможно. Разрешение перегрузки функции должно знать во время компиляции, к какому классу относится этот параметр, чтобы он мог вызвать правильный Eat функция. Единственным исключением являются виртуальные функции-члены, которые вы уже исключили.

Нет ничего плохого в наличии произвольных указателей классов в заголовках. Они составляют основу многих идиом, таких как PIMPL и непрозрачные указатели. Кроме того, если вы не вменяемый человек, как вы должны понимать мой ответ?

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

Другие вопросы по тегам