Подтвердить что ты не робот

Виртуальные методы или указатели функций

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

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

[Это было добавлено мне на вопрос (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();
}

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

4b9b3361

Ответ 1

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

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

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

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

Подход 3 (класс, вызывающий Т-функтор)

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

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

Ответ 2

Подход 1

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

Подход 2

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

Подход 3

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

Ответ 3

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

Ответ 4

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

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

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

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

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

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

Ответ 5

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

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

Ответ 6

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

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

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

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

Ответ 7

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

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

Ответ 8

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