Виртуальные методы или указатели функций - PullRequest
30 голосов
/ 23 декабря 2009

При реализации полиморфного поведения в 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);
}

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

Подход 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 ]

13 голосов
/ 23 декабря 2009

Подход 1 (Виртуальная функция)

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

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

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

Подход 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 перед загрузкой указателя функции. Использование функторов (с не виртуальным вызовом) в качестве обратного вызова является наиболее эффективным методом использования параметра для шаблонных функций, поскольку они могут быть встроенными и даже если они не встроены, не генерировать косвенный вызов.

6 голосов
/ 23 декабря 2009

Подход 1

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

Подход 2

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

Подход 3

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

5 голосов
/ 22 июня 2012

Основная проблема с подходом 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 единственным пригодным для использования подходом, когда требуется динамический вызов метода.

3 голосов
/ 23 декабря 2009

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

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

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

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

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

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

1 голос
/ 23 декабря 2009

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

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

0 голосов
/ 24 декабря 2009
  1. Сначала смените на чисто виртуальный. Тогда включите это. Это вообще должно сводить на нет любые вызовы служебных данных, пока встраивание не завершится неудачей (и не будет, если вы его принудительно).
  2. Можно также использовать C, потому что это единственная реальная полезная особенность C ++ по сравнению с C. Вы всегда будете вызывать метод, и он не может быть встроенным, поэтому он будет менее эффективным.
0 голосов
/ 23 декабря 2009

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

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 *> и назвать его записью. Запись может выполнять общие операции над полями и не заботится о том, какой вид поля обрабатывается или не знает.

0 голосов
/ 23 декабря 2009

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

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

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...