динамическое связывание или переключатель / случай? - PullRequest
1 голос
/ 21 апреля 2010

Сцена, подобная этой:
У меня разные объекты выполняют аналогичную операцию, что и соответствующие функции func ().
Существует 2 вида решения для func_manager () для вызова func () в соответствии с различными объектами

Решение 1 : Использовать символ виртуальной функции, указанный в c ++. func_manager работает по-разному, в соответствии с различными проходами точек объекта.

class Object{
  virtual void func() = 0;
}
class Object_A : public Object{
  void func() {};
}
class Object_B : public Object{
  void func() {};
}
void func_manager(Object* a)
{
   a->func();
}

Решение 2 : Используйте простой переключатель / чехол. func_manager работает по-разному в соответствии с проходом другого типа в

typedef enum _type_t
{
  TYPE_A,
  TYPE_B
}type_t;

void func_by_a()
{
// do as func() in Object_A
}
void func_by_b()
{
// do as func() in Object_A
}
void func_manager(type_t type)
{
    switch(type){
        case TYPE_A:
            func_by_a();
            break;
        case TYPE_B:
            func_by_b();
        default:
            break;
    }
}

Мой вопрос 2:
1. с точки зрения ДИЗАЙН-ШАБЛОН , какой из них лучше?
2. с точки зрения ЭФФЕКТИВНОСТИ НАКОНЕЦ , какой из них лучше? В частности, по мере того, как увеличивается количество видов объектов, может быть до 10-15, какие накладные расходы превышают другие? Я не знаю, как внутри switch / case реализуется, просто куча if / else?

Большое спасибо!

Ответы [ 6 ]

7 голосов
/ 21 апреля 2010

с точки зрения DESIGN PATTERN, какой из них лучше?

Лучше использовать полиморфизм (Раствор 1).
Всего лишь одна точка данных: представьте, что у вас есть огромная система, построенная вокруг одной из двух, и внезапно приходит требование добавить еще один тип . С первым решением вы добавляете один производный класс , убедитесь, что он создан, где требуется, и все готово. С решением 2 у вас есть тысячи switch заявлений по всей системе, и более или менее невозможно гарантировать, что вы нашли все места, где вы должны изменить их для Новый тип.

с точки зрения БЕЗОПАСНОЙ ЭФФЕКТИВНОСТИ, какая из них лучше? Особенно как виды Объекта

Сложно сказать.
Я помню сноску из Стенли Липпмана внутри объектной модели C ++ , где он говорит, что исследования показали, что виртуальные функции могут иметь небольшое преимущество по сравнению с переключениями типов. Однако я бы с трудом процитировал главу и стих, и, по мнению IIRC, преимущество оказалось недостаточно большим, чтобы сделать решение зависимым от него.

3 голосов
/ 21 апреля 2010

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

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

1 голос
/ 21 апреля 2010

Почему никто не предлагает функциональные объекты? Я думаю, что Kingkai заинтересованы в решении проблемы, а не только в том, что два решения.
Я не опытный с ними, но они делают свою работу:

struct Helloer{
    std::string operator() (void){
        return std::string("Hello world!");
    }
};

struct Byer{
    std::string operator() (void){
        return std::string("Good bye world!");
    }
};

template< class T >
void say( T speaker){
    std::cout << speaker() << std::endl;
}

int main()
{
   say( Helloer() );
   say( Byer() );
}

Редактировать: На мой взгляд, это более "правильный" подход, чем классы с одним методом (который не является оператором вызова функции). На самом деле, я думаю, что эта перегрузка была добавлена ​​в C ++, чтобы избежать таких классов.
Кроме того, функциональные объекты удобнее использовать, даже если вам не нужны шаблоны, как и обычные функции.
В конце рассмотрим STL - он везде использует func-объекты и выглядит довольно естественно. И я даже не упоминаю Boost

1 голос
/ 21 апреля 2010

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

С точки зрения эффективности в обеих альтернативах у вас более или менее одинаковый сгенерированный код, где-то должен быть код выбора.Разница в том, что наследование обрабатывает его для вас автоматически в 1-м, а вы делаете это вручную во 2-м.

1 голос
/ 21 апреля 2010
  1. Я бы сказал, что первый лучше. В решении 2 func_manager нужно будет знать обо всех типах и обновляться при каждом добавлении нового типа. Если вы перейдете к решению 1, вы сможете позже добавить новый тип, и func_manager будет просто работать.

  2. В этом простом случае я бы предположил, что решение 1 будет быстрее, поскольку он может непосредственно искать адрес функции в vtable. Если у вас есть 15 различных типов, оператор switch, скорее всего, будет не в виде таблицы переходов, а в основном, как вы говорите, огромный оператор if / else.

1 голос
/ 21 апреля 2010

Использование « динамическая диспетчеризация » или «виртуальная диспетчеризация» (т. Е. Вызов виртуальной функции) лучше как в отношении дизайна (сохранение изменений в одном месте, расширяемость), так и в отношении эффективности времени выполнения (просто разыменование по сравнению с простым разыменованием, где используется таблица переходов или лестница if ... else, которая действительно медленная). С другой стороны, «динамическое связывание» не означает, что вы думаете ... «динамическое связывание» относится к разрешению значения переменной на основе ее самого последнего объявления, в отличие от «статического связывания» или «лексического связывания», которое относится к разрешению переменной по текущей внутренней области, в которой она объявлена.

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

Эффективность
Динамическая диспетчеризация просто ищет функцию в виртуальной таблице объекта и затем переходит в это место. Это невероятно быстро. Если оператор switch использует таблицу переходов, он будет примерно такой же скорости; однако, если существует очень мало реализаций, у некоторого программиста будет соблазн использовать лестницу if ... else вместо оператора switch, которая обычно не может использовать преимущества таблиц переходов и, следовательно, медленнее.

...