Виртуальные методы или указатели функций
При реализации полиморфного поведения в C++ можно использовать либо чисто виртуальный метод, либо использовать указатели на функции (или функторы). Например, асинхронный обратный вызов может быть реализован с помощью:
Подход 1
class Callback
{
public:
Callback();
~Callback();
void go();
protected:
virtual void doGo() = 0;
};
//Constructor and Destructor
void Callback::go()
{
doGo();
}
Таким образом, чтобы использовать обратный вызов здесь, вам нужно переопределить метод 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);
}
Чтобы использовать здесь метод обратного вызова, вам нужно создать указатель на функцию, которая будет вызываться объектом Callback.
Подход 3
[Это было добавлено к вопросу мной (Андреас); это не было написано оригинальным постером]
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();
}
Каковы преимущества и недостатки каждой реализации?
8 ответов
Подход 1 (Виртуальная функция)
- "+" The "правильный способ сделать это в C++
- "-" Для каждого обратного вызова должен быть создан новый класс
- "-" С точки зрения производительности дополнительная разыменование через таблицу VF по сравнению с указателем функций. Две косвенные ссылки по сравнению с решением Functor.
Подход 2 (класс с указателем на функцию)
- "+" Может обернуть функцию в стиле C для C++ Callback Class
- Функция обратного вызова "+" может быть изменена после создания объекта обратного вызова
- "-" Требуется косвенный вызов. Может быть медленнее, чем метод functor для обратных вызовов, которые могут быть статически вычислены во время компиляции.
Подход 3 (Класс, вызывающий T функтор)
- "+" Возможно, самый быстрый способ сделать это. Нет косвенных накладных расходов на вызовы и может быть встроен полностью.
- "-" Требуется определить дополнительный класс Functor.
- "-" Требует, чтобы обратный вызов статически объявлялся во время компиляции.
FWIW, указатели функций не совпадают с функторами. Функторы (в C++) - это классы, которые используются для вызова функции, обычно это оператор ().
Вот пример функтора, а также функция шаблона, которая использует аргумент функтора:
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 перед загрузкой указателя на функцию. Использование функторов (с не виртуальным вызовом) в качестве обратного вызова является наиболее эффективным методом использования параметра для шаблонных функций, поскольку они могут быть встроенными и даже если они не встроены, не генерируют косвенный вызов.
Подход 1
- Легче читать и понимать
- Меньшая вероятность ошибок (
iFunc
не может быть NULL, вы не используетеvoid *iParam
, так далее - Программисты C++ скажут вам, что это "правильный" способ сделать это в C++
Подход 2
- Чуть меньше печатать, чтобы сделать
- ОЧЕНЬ немного быстрее (вызов виртуального метода имеет некоторые издержки, обычно такие же, как у двух простых арифметических операций. Так что, скорее всего, это не будет иметь значения)
- Вот как бы вы сделали это в C
Подход 3
Вероятно, лучший способ сделать это, когда это возможно. Он будет иметь лучшую производительность, он будет безопасен от типов и его легко понять (это метод, используемый STL).
Основная проблема с подходом 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 единственным пригодным для использования подходом, когда требуется динамический вызов метода.
Из вашего примера не ясно, создаете ли вы служебный класс или нет. Ваш класс Callback предназначен для реализации замыкания или более существенного объекта, который вы просто не реализовали?
Первая форма:
- Легче читать и понимать,
- Гораздо проще расширить: попробуйте добавить методы паузы, возобновления и остановки.
- Лучше при обработке инкапсуляции (при условии, что doGo определен в классе).
- Вероятно, это лучшая абстракция, поэтому ее легче поддерживать.
Вторая форма:
- Может использоваться с различными методами для doGo, так что это больше, чем просто полиморфизм.
- Может позволить (с дополнительными методами) изменять метод doGo во время выполнения, позволяя экземплярам объекта изменять свою функциональность после создания.
В конечном счете, IMO, первая форма лучше для всех нормальных случаев. Второй имеет некоторые интересные возможности, но не те, которые вам понадобятся часто.
Одним из основных преимуществ первого метода является большая безопасность типов. Второй метод использует void * для iParam, поэтому компилятор не сможет диагностировать проблемы типов.
Незначительное преимущество второго метода заключается в том, что было бы меньше работы по интеграции с C. Но если ваша кодовая база - только C++, это преимущество является спорным.
Я бы сказал, что указатели на функции более C-style. Главным образом потому, что для их использования вы обычно должны определять плоскую функцию с той же точной сигнатурой, что и у вашего определения указателя.
Когда я пишу на C++, единственная плоская функция, которую я пишу, это int main(). Все остальное является объектом класса. Из двух вариантов я бы определил класс и переопределил ваш виртуальный, но если все, что вам нужно, это уведомить некоторый код о том, что в вашем классе произошло какое-то действие, ни один из этих вариантов не будет лучшим решением.
Я не знаю о вашей конкретной ситуации, но вы можете просмотреть шаблоны проектирования
Я бы предложил модель наблюдателя. Это то, что я использую, когда мне нужно следить за классом или ждать какого-то уведомления.
Например, давайте посмотрим на интерфейс для добавления функциональности чтения в класс:
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 отдельных класса. Комбинации начинают появляться очень плохо с несколькими объектами и несколькими источниками.
Если я использую функтор, который напоминает шаблон дизайна Visitor:
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
, В может положить указатели на их экземпляры в vector<Field *>
и назовите это рекордом. Запись может выполнять общие операции над полями и не заботится или не знает, какой тип поля обрабатывается.
- Измените на чисто виртуальный, в первую очередь. Тогда включите это. Это вообще должно сводить на нет любые вызовы служебных данных, пока встраивание не завершится неудачей (и не будет, если вы его принудительно).
- Можно также использовать C, потому что это единственная реальная полезная особенность C++ по сравнению с C. Вы всегда будете вызывать метод, и он не может быть встроен, поэтому он будет менее эффективным.