Яблоки, апельсины и указатели на самый производный класс 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 и непрозрачные указатели. Кроме того, если вы не вменяемый человек, как вы должны понимать мой ответ?
Серьезно, производные функции и полиморфизм существуют для решения этой проблемы. Если вы отказываетесь использовать предоставляемые языком инструменты, зачем вообще их использовать? Любое решение, которое вы можете придумать, в любом случае может быть преобразовано в вызов виртуальной функции, просто вы бы закодировали его вручную, а не компилятор.