Преимущества использования функциональных указателей - PullRequest
14 голосов
/ 20 марта 2009

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

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

Я бы хотел больше использовать указатели функций, но я хочу использовать их там, где они наиболее уместны.

В наши дни, как лучше и практичнее использовать указатели функций в современных языках стиля C, таких как C, C ++, C # и Java и т. Д.?

Ответы [ 11 ]

23 голосов
/ 20 марта 2009

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

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

Вместо этого они являются очень легким намеком на парадигму функционального программирования, поскольку они позволяют передавать функцию в качестве параметра или возвращаемого значения в другой функции.

Простой пример - универсальная функция сортировки. У него должен быть какой-то способ сравнить два элемента, чтобы определить, как они должны быть отсортированы. Это может быть указатель на функцию, передаваемую в функцию сортировки, и фактически std :: sort () в c ++ может использоваться именно так. Если вы попросите его отсортировать последовательности типа, который не определяет оператор меньше чем, вы должны передать указатель на функцию, который он может вызвать, чтобы выполнить сравнение.

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

  • Они предлагают больше гибкости: это полноценные классы с переменными конструктора, деструктора и члена. Они могут поддерживать состояние и могут предоставлять другие функции-члены, которые может вызывать окружающий код.
  • Они быстрее: в отличие от указателей на функции, тип которых только кодирует сигнатуру функции (переменная типа void (*)(int) может быть любой функцией, которая принимает int и возвращает void. Мы не можем знать, какой именно), тип функтора кодирует точную функцию, которая должна быть вызвана (поскольку функтор является классом, назовите его C, мы знаем, что вызываемая функция есть и всегда будет C :: operator ()). А это значит, что компилятор может встроить вызов функции. Это волшебство, которое делает универсальный std :: sort так же быстро, как и ваша сортируемая вручную функция сортировки, разработанная специально для вашего типа данных. Компилятор может устранить все накладные расходы при вызове пользовательской функции.
  • Они безопаснее: в указателе функции очень мало безопасности типов. У вас нет гарантии, что он указывает на действительную функцию. Это может быть NULL. И большинство проблем с указателями относится и к указателям на функции. Они опасны и подвержены ошибкам.

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

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

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

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

9 голосов
/ 20 марта 2009

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

int MyFunc()
{
  if(SomeFunctionalityCheck())
  {
    ...
  }
  else
  {
    ...
  }
}

Если эта функция вызывается глубоко внутри важных циклов, то, вероятно, лучше использовать указатель функции для MyFunc:

int (*MyFunc)() = MyFunc_Default;

int MyFunc_SomeFunctionality()
{
  // if(SomeFunctionalityCheck())
  ..
}

int MyFunc_Default()
{
  // else
  ...
}

int MyFuncInit()
{
  if(SomeFunctionalityCheck()) MyFunc = MyFunc_SomeFunctionality;
}

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

Для выполнения Intel-совместимого байтового кода в Windows, что может быть полезно для интерпретатора. Например, вот функция stdcall, которая возвращает 42 (0x2A), хранящиеся в массиве, который может быть выполнен:

code = static_cast<unsigned char*>(VirtualAlloc(0, 6, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE));
// mov eax, 42
code[0] = 0x8b;
code[1] = 0x2a;
code[2] = 0x00;
code[3] = 0x00;
code[4] = 0x00;
// ret
code[5] = 0xc3;
// this line executes the code in the byte array
reinterpret_cast<unsigned int (_stdcall *)()>(code)();

...

VirtualFree(code, 6, MEM_RELEASE);

);

6 голосов
/ 20 марта 2009

Каждый раз, когда вы используете обработчик событий или делегат в C #, вы эффективно используете указатель на функцию.

И нет, они не о скорости. Функциональные указатели об удобстве.

Jonathan

5 голосов
/ 20 марта 2009

В наши дни, как лучше и эффективнее использовать целые числа в современных языках c-style?

5 голосов
/ 20 марта 2009

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

Тем не менее, я приведу цитату, которую я получил от моего бывшего профессора:

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

4 голосов
/ 20 марта 2009

В тусклом, темном возрасте до C ++, в моем коде был общий шаблон, который определял структуру с набором указателей на функции, которые (как правило) каким-то образом оперировали с этой структурой и обеспечивали определенное поведение Это. В терминах C ++ я просто создавал vtable. Разница заключалась в том, что я мог побочно влиять на структуру во время выполнения, чтобы по мере необходимости изменять поведение отдельных объектов на лету. Это предлагает гораздо более богатую модель наследования за счет стабильности и простоты отладки. Однако самой большой ценой было то, что один человек мог эффективно написать этот код: я.

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

Формализовать этот процесс на ОО-языках лучше во всех смыслах.

3 голосов
/ 23 марта 2009

В некоторых случаях использование указателей на функции может ускорить обработку. Простые таблицы диспетчеризации могут использоваться вместо длинных операторов switch или последовательностей if-then-else.

3 голосов
/ 21 марта 2009

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

Без замыканий и простого синтаксиса, они как бы брутто. Таким образом, вы склонны использовать их гораздо меньше, чем хотелось бы. В основном для функций «обратного вызова».

Иногда ОО-дизайн работает вокруг использования функций, вместо этого создавая целый тип интерфейса для передачи необходимой функции.

В C # есть замыкания, поэтому указатели на функции (которые на самом деле хранят объект, так что это не просто необработанная функция, но и типизированное состояние) гораздо удобнее там.

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

BOOL EnumWindows(          
    WNDENUMPROC lpEnumFunc,
    LPARAM lParam
);

Первый параметр - это функция, которую легко передать. Но поскольку в C нет замыканий, мы получаем этот прекрасный второй параметр: «Определяет определенное приложением значение, которое будет передано в функцию обратного вызова». Это определенное приложением значение позволяет вам вручную обойти нетипизированное состояние, чтобы компенсировать отсутствие замыканий.

.NET Framework также наполнен аналогичным дизайном. Например, IAsyncResult .AsyncState: «Получает пользовательский объект, который квалифицируется или содержит информацию об асинхронной операции». Поскольку IAR - это все, что вы получаете при обратном вызове, без замыканий, вам нужен способ поместить некоторые данные в асинхронную операцию, чтобы вы могли затем преобразовать их.

3 голосов
/ 20 марта 2009

Просто говоря о C #, но указатели на функции используются во всем C #. Делегаты и события (и Lambdas, и т. Д.) - это все указатели на функции, поэтому почти любой проект на C # будет пронизан указателями на функции. В основном, каждый обработчик события, около каждого запроса LINQ и т. Д., Будет использовать указатели функций.

2 голосов
/ 26 марта 2012

Согласно моему личному опыту, они могут помочь вам сохранить важные строки кода.

Рассмотрим условие: {

switch(sample_var)
{

case 0:
          func1(<parameters>);
          break;

case 1:
          func2(<parameters>);
          break;











up to case n:
                funcn(<parameters>);
                break;

}

где func1 () ... funcn () - функции с одинаковым прототипом. Что мы могли бы сделать, это: Объявите массив указателей на функции arrFuncPoint , содержащий адреса функций func1 () до funcn ()

Тогда весь корпус переключателя будет заменен на

* arrFuncPoint [sample_var];

...