2009-12-23 6 views
29

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

подход 1

class Callback 
{ 
public: 
    Callback(); 
    ~Callback(); 
    void go(); 
protected: 
    virtual void doGo() = 0; 
}; 

//Constructor and Destructor 

void Callback::go() 
{ 
    doGo(); 
} 

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

подход 2

typedef void (CallbackFunction*)(void*) 

class Callback 
{ 
public: 
    Callback(CallbackFunction* func, void* param); 
    ~Callback(); 
    void go(); 
private: 
    CallbackFunction* iFunc; 
    void* iParam; 
}; 

Callback::Callback(CallbackFunction* func, void* param) : 
    iFunc(func), 
    iParam(param) 
{} 

//Destructor 

void go() 
{ 
    (*iFunc)(iParam); 
} 

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

подход 3

[Это было добавлено к вопросу мною (Andreas); она не была написана создателями]

template <typename T> 
class Callback 
{ 
public: 
    Callback() {} 
    ~Callback() {} 
    void go() { 
     T t; t(); 
    } 
}; 

class CallbackTest 
{ 
public: 
    void operator()() { cout << "Test"; } 
}; 

int main() 
{ 
    Callback<CallbackTest> test; 

    test.go(); 
} 

Каковы преимущества и недостатки каждой реализации?

+1

Или 'go()' может быть функцией шаблона, которая может принимать * либо * указатель на функцию или класс с переопределенным 'operator() (void *)'. –

+2

Подход 2 - это то, что компилятор делает за сенами для реализации подхода 1. Вы не должны делать это сами, потому что это опасно и подвержено ошибкам. –

+0

Прохладный, спасибо Андреасу за подход 3 – doron

ответ

12

подход 1 (Virtual Function)

  • "+" «Правильный способ сделать это в C++
  • "-" Новый класс должен быть создан на обратный вызов
  • "-" Performance -wise дополнительное разыменования через VF-таблицу по сравнению с указателем на функцию. Два косвенных ссылок по сравнению с раствором Functor.

подхода 2 (класс с указателем на функции)

  • «+» Можно обернуть функцию C-стиль для C++ обратный вызов класса
  • «+» функции обратного вызова можно изменить после создания объекта обратного вызова
  • «-» Требуется косвенный вызов. Может быть медленнее, чем метод functor для обратных вызовов, которые можно статически вычислять во время компиляции.

подход 3 (класс вызова T функтор)

  • "+" Возможно, самый быстрый способ сделать это. Отсутствие косвенных вызовов и может быть полностью привязано.
  • «-» Требуется определить дополнительный класс Functor.
  • «-» Требуется, чтобы обратный вызов статически объявлялся во время компиляции.

FWIW, Указатели на функции не являются такими же, как функторов. Функторы (на C++) - это классы, которые используются для вызова функции, которая обычно является operator().

Ниже приведен пример функтор, а также шаблон функции, которая использует функтор аргумент:

class TFunctor 
{ 
public: 
    void operator()(const char *charstring) 
    { 
     printf(charstring); 
    } 
}; 

template<class T> void CallFunctor(T& functor_arg,const char *charstring) 
{ 
    functor_arg(charstring); 
}; 

int main() 
{ 
    TFunctor foo; 
    CallFunctor(foo,"hello world\n"); 
} 

С точки зрения производительности, виртуальные функции и функции Указатели и приводить к вызову косвенной функции (т.е. через регистр), хотя перед загрузкой указателя функции требуется дополнительная нагрузка указателя VFTABLE. Использование функторов (с не виртуальным вызовом) в качестве обратного вызова - это самый эффективный метод использования параметра для функций шаблона, потому что они могут быть встроены и даже если не встроены, не генерируют косвенный вызов.

+0

Но в моем подходе 2 они могут использоваться взаимозаменяемо? – doron

+0

Технически все три класса могут свободно считаться функторами, если вы берете go() как функцию, предоставленную классом вместо оператора(). Сам указатель функции в # 2 не имеет ничего общего с функторами. Однако только подход 3 следует за «духом» реального функтора C++ и может извлечь выгоду из оптимизации шаблонов, доступной для того, что обычно считается функтором для использования на C++. – Adisak

6

подход 1

  • Легче читать и понимать
  • Меньше вероятность ошибок (iFunc не может быть NULL, вы не используя void *iParam и т.д.
  • C++ программисты скажут вам, что это "правильный" способ сделать это в C++

подход 2

  • Чуть меньше типирование сделать
  • ОЧЕНЬ немного быстрее (вызов виртуальный метод имеет некоторые накладные расходы, как правило, такой же, из двух простых арифметических операций .. Так что, скорее всего, не будет иметь значения)
  • Вот как ты бы сделать это в C

Approach 3

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

+4

Виртуальные функции могут быть более чем на 40 раз медленнее, если они предотвращают вложение небольшой функции, удаление дубликатов или невозможных ветвей и/или использование хранилища переменных только для регистров. –

+0

@ Zan: # 1 также предотвращает встроенные оптимизации, и это то, что я тоже сравнивал. Теперь я добавил вариант № 3, так или иначе, –

+0

Есть две причины, почему подход 2 является «правильным» способом сделать это в C++ 1. C++ представляет функторы, которые в некотором смысле расширяют идею указателей функций. 2. Библиотека внедрения Boost принимает подход 2 при создании нового потока. – doron

0

Функциональные указатели больше C-стиля, я бы сказал. В основном потому, что для их использования вы обычно должны определить плоскую функцию с той же точной подписью, что и определение указателя.

Когда я пишу C++, единственная плоская функция, которую я пишу, это int main(). Все остальное - объект класса. Из двух вариантов я хотел бы выбрать класс и переопределить ваш виртуальный, но если все, что вы хотите, - это уведомить какой-то код о том, что некоторые действия произошли в вашем классе, ни один из этих вариантов не был бы лучшим решением.

Я не знаю вашей конкретной ситуации, но вы можете просмотреть design patterns

Я хотел бы предложить шаблон наблюдателя. Это то, что я использую, когда мне нужно контролировать класс или ждать какого-то уведомления.

+0

Также, что Адишак сказал о функторах, это хорошая идея, хотя я не использовал их много. – Charles

1

Одним из основных преимуществ первого метода является наличие большей безопасности типа. Второй метод использует void * для iParam, поэтому компилятор не сможет диагностировать проблемы типа.

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

2

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

Первая форма:

  • легче читать и понимать,
  • гораздо проще расширить: попробуйте добавлять методы паузу, возобновить и остановка.
  • Лучше при обработке инкапсуляция (предполагается, что doGo определен в классе).
  • возможно лучшая абстракция, поэтому ее легче поддерживать.

Вторая форма:

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

В конечном счете, ИМО, первая форма лучше для всех нормальных случаев. Во-вторых, есть некоторые интересные возможности, но - но не те, которые вам понадобятся часто.

0

Например, давайте посмотрим на интерфейс для добавления чтения функции к классу:

struct Read_Via_Inheritance 
{ 
    virtual void read_members(void) = 0; 
}; 

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

struct Read_Inherited_From_Cin 
    : public Read_Via_Inheritance 
{ 
    void read_members(void) 
    { 
    cin >> member; 
    } 
}; 

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

Если я использую функтор, который происходит, чтобы походить на посетитель шаблон дизайна:

struct Reader_Visitor_Interface 
{ 
    virtual void read(unsigned int& member) = 0; 
    virtual void read(std::string& member) = 0; 
}; 

struct Read_Client 
{ 
    void read_members(Reader_Interface & reader) 
    { 
    reader.read(x); 
    reader.read(text); 
    return; 
    } 
    unsigned int x; 
    std::string& text; 
}; 

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

struct Read_From_Cin 
    : Reader_Visitor_Interface 
{ 
    void read(unsigned int& value) 
    { 
    cin>>value; 
    } 
    void read(std::string& value) 
    { 
    getline(cin, value); 
    } 
}; 

Мне не нужно менять какой-либо код объекта (это хорошо, потому что оно уже работает). Я также могу применить читателя к другим объектам.

Как правило, я использую наследование при выполнении общих программ. Например, если у меня есть класс Field, то я могу создать Field_Boolean, Field_Text и Field_Integer.In может помещать указатели на свои экземпляры в vector<Field *> и называть это записью. Запись может выполнять общие операции над полями и не заботится о том, что обрабатывается поле .

0
  1. Переход на чистый виртуальный, первый раз. Затем вставьте его. Это должно отрицательно отклонить любой служебный вызов метода, при условии, что inlining не сработает (и это не произойдет, если вы его заставите).
  2. Может также использовать C, потому что это единственная реальная полезная важная функция C++ по сравнению с C. Вы всегда будете вызывать метод, и он не может быть встроен, поэтому он будет менее эффективным.
5

Основная проблема с подходом 2 состоит в том, что он просто не масштабируется. Рассмотрим эквивалент для 100 функций:

class MahClass { 
    // 100 pointers of various types 
public: 
    MahClass() { // set all 100 pointers } 
    MahClass(const MahClass& other) { 
     // copy all 100 function pointers 
    } 
}; 

Размер MahClass раздулся, и время, чтобы построить его также значительно увеличилось. Однако виртуальными функциями являются O (1) увеличение размера класса и времени для его создания. Не говоря уже о том, что вы, пользователь, должны записывать все обратные вызовы для всех производных классов вручную, которые корректируют указатель чтобы стать указателем на производный, и должен указывать типы указателей функций и что беспорядок. Не говоря уже о том, что вы можете его забыть или установить в NULL или что-то столь же глупое, но полностью произойдет, потому что вы пишете 30 классов таким образом и нарушаете DRY, как паразитная оса, нарушает гусеницу.

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

Это оставляет подход 1 единственным применимым подходом при вызове динамического метода.

+0

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

Смежные вопросы