Преимущества использования указателей функций
Я программировал уже несколько лет и в определенных случаях использовал указатели на функции. То, что я хотел бы знать, - когда целесообразно или не использовать их по соображениям производительности, и я имею в виду в контексте игр, а не для делового программного обеспечения.
Указатели функций быстры, Джон Кармак использовал их в степени злоупотребления в исходном коде Quake and Doom и потому, что он гений:)
Я хотел бы использовать указатели функций больше, но я хочу использовать их там, где они наиболее подходят.
В наши дни каковы наилучшие и наиболее практичные применения указателей на функции в современных языках c-style, таких как C, С++, С# и Java и т.д.
Ответы
Ответ 1
В указателях функций нет ничего особо "быстрого". Они позволяют вам вызывать функцию, указанную во время выполнения. Но у вас есть те же накладные расходы, что и у любого другого вызова функции (плюс дополнительная ссылка указателя). Кроме того, поскольку функция для вызова определяется во время выполнения, компилятор обычно не может встраивать вызов функции, как мог в любом другом месте. Таким образом, указатели на функции могут в некоторых случаях значительно замедляться, чем обычный вызов функции.
Указатели функций не имеют ничего общего с производительностью и никогда не должны использоваться для повышения производительности.
Вместо этого они очень слабо кивают в парадигму функционального программирования, поскольку они позволяют вам передавать функцию в качестве параметра или возвращаемого значения в другой функции.
Простым примером является общая функция сортировки. Он должен иметь некоторый способ сравнить два элемента, чтобы определить, как их следует сортировать. Это может быть указатель функции, переданный функции сортировки, и на самом деле С++ std:: sort() можно использовать точно так же. Если вы попросите его отсортировать последовательности типа, который не определяет меньше, чем оператор, вы должны передать указатель функции, который он может вызывать для выполнения сравнения.
И это приводит нас к превосходной альтернативе. В С++ вы не ограничены указателями на функции. Вместо этого вы часто используете функторы - то есть классы, которые перегружают operator(), чтобы их можно было "вызвать", как если бы они были функциями. Функторы имеют несколько больших преимуществ над указателями функций:
- Они предлагают большую гибкость: они являются полноценными классами, с конструкторами, деструкторами и переменными-членами. Они могут поддерживать состояние, и они могут выставлять другие функции-члены, которые может вызвать окружающий код.
- Они быстрее: в отличие от указателей функций, тип которых кодирует только подпись функции (переменная типа
void (*)(int)
может быть любой функцией, которая принимает int и возвращает void. Мы не можем знать, какой из них), Тип функтора кодирует точную функцию, которая должна быть вызвана (так как функтор является классом, назовите его C, мы знаем, что функция вызова всегда и всегда будет C:: operator()). И это означает, что компилятор может встроить вызов функции. Это волшебство, которое делает общий std:: sort так же быстро, как ваша функция сортировки вручную, специально разработанная для вашего типа данных. Компилятор может устранить все накладные расходы при вызове пользовательской функции.
- Они безопаснее: в указателе функций очень мало безопасности. У вас нет гарантии, что он указывает на действительную функцию. Это может быть NULL. И большинство проблем с указателями применимы и к указателям на функции. Они опасны и подвержены ошибкам.
Указатели функций (в C) или функторы (на С++) или делегаты (на С#) решают одну и ту же проблему с различными уровнями элегантности и гибкости: они позволяют рассматривать функции как первоклассные значения, передавая их как и любая другая переменная. Вы можете передать функцию другой функции, и она будет вызывать вашу функцию в указанное время (когда истечет таймер, когда требуется перерисовка окна или когда ему нужно сравнить два элемента в вашем массиве)
Насколько я знаю (и я мог ошибаться, потому что я не работал с Java целую вечность), Java не имеет прямого эквивалента. Вместо этого вам нужно создать класс, который реализует интерфейс, и определяет функцию (например, вызывается Execute()). Затем вместо вызова функции, предоставляемой пользователем (в виде указателя функции, функтора или делегата), вы вызываете foo.Execute(). Подобно реализации С++ в принципе, но без общности шаблонов С++ и без синтаксиса функций, который позволяет обрабатывать указатели функций и функторы таким же образом.
Итак, вы используете указатели на функции: когда более сложные альтернативы недоступны (т.е. вы застряли на C), вам нужно передать одну функцию другой. Наиболее распространенным сценарием является обратный вызов. Вы определяете функцию F, которую вы хотите, чтобы система вызывала, когда происходит X. Таким образом, вы создаете указатель на функцию, указывающий на F, и передаете это в рассматриваемую систему.
Итак, забудьте о Джоне Кармаке и не думайте, что все, что вы видите в его коде, волшебным образом улучшит ваш код, если вы его скопируете. Он использовал указатели на функции, потому что упомянутые вами игры были написаны на C, где превосходные альтернативы недоступны, а не потому, что они представляют собой магический ингредиент, чье существование делает код быстрее.
Ответ 2
Они могут быть полезны, если вы не знаете функциональность, поддерживаемую вашей целевой платформой, до времени выполнения (например, функциональность ЦП, доступная память). Очевидным решением является запись таких функций:
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);
);
Ответ 3
В любое время, когда вы используете обработчик событий или делегат на С#, вы эффективно используете указатель на функцию.
И нет, они не о скорости. Указатели функций касаются удобства.
Джонатан
Ответ 4
Указатели функций используются во многих случаях как обратные вызовы. Одно использование - это функция сравнения в алгоритмах сортировки. Поэтому, если вы пытаетесь сравнить настроенные объекты, вы можете предоставить указатель на функцию сравнения, которая знает, как обрабатывать эти данные.
Тем не менее, я приведу цитату из моего бывшего профессора:
Относитесь к новой функции С++, как если бы вы рассматривали загруженное автоматическое оружие в переполненном помещении: никогда не используйте его только потому, что он выглядит превосходно. Подождите, пока вы не поймете последствия, не смейтесь, напишите, что вы знаете, и знаете, что пишете.
Ответ 5
В наши дни самое лучшее и практичное использование целых чисел в современных языках c-style?
Ответ 6
В тусклом, темном возрасте до С++ в моем коде был распространен общий шаблон, который должен был определить структуру с набором указателей на функции, которые (как правило) работали на этой структуре каким-то образом и предоставляли особые способы для Это. В терминах С++ я просто строил таблицу vtable. Разница заключалась в том, что я мог бы связать структуру во время выполнения, чтобы изменить поведение отдельных объектов "на ходу" по мере необходимости. Это обеспечивает гораздо более богатую модель наследования за счет стабильности и простоты отладки. Наибольшая стоимость, однако, заключалась в том, что был ровно один человек, который мог бы написать этот код эффективно: me.
Я использовал это в интерфейсе пользовательского интерфейса, что позволило мне изменить способ рисования объектов, кто был целью команд, и т.д. "на лету" - то, что предложило очень мало пользовательских интерфейсов.
Наличие этого процесса, формализованного на языках OO, лучше всего осмысленно.
Ответ 7
Просто говоря о С#, но указатели на функции используются по всему С#. Делегаты и события (и Lambdas и т.д.) - это все указатели на функции под капотом, поэтому почти любой проект С# будет пронизан указателями на функции. В основном каждый обработчик событий, рядом с каждым запросом LINQ и т.д. Будет использовать указатели функций.
Ответ 8
Указатели функций - плохой человек, пытающийся быть функциональным. Вы даже можете сделать аргумент, что наличие указателей функций делает язык функциональным, поскольку вы можете писать с ними функции более высокого порядка.
Без замыканий и простого синтаксиса они кажутся грубыми. Поэтому вы склонны использовать их гораздо меньше, чем желательно. Главным образом для функций "обратного вызова".
Иногда OO-дизайн работает с использованием функций, вместо этого создает весь тип интерфейса для передачи необходимой функции.
У С# есть замыкания, поэтому указатели на функции (которые фактически хранят объект, поэтому он не только сырая функция, но и типизированное состояние) гораздо более полезны там.
Edit
Один из комментариев сказал, что должна быть демонстрация функций более высокого порядка с указателями функций. Любая функция, выполняющая функцию обратного вызова, является функцией более высокого порядка. Например, EnumWindows:
BOOL EnumWindows(
WNDENUMPROC lpEnumFunc,
LPARAM lParam
);
Первый параметр - это функция, которую нужно пройти, достаточно легко. Но поскольку в C нет замыканий, мы получаем этот прекрасный второй параметр: "Определяет значение, определенное приложением, которое должно быть передано функции обратного вызова". Это определенное пользователем значение позволяет вам вручную обходить нетипизированное состояние, чтобы компенсировать отсутствие закрытий.
Структура .NET также заполнена аналогичными проектами. Например, IAsyncResult.AsyncState: "Получает пользовательский объект, который квалифицирует или содержит информацию об асинхронной операции". Поскольку IAR - это все, что вы получаете от своего обратного вызова, без закрытия, вам нужен способ перебросить некоторые данные в async op, чтобы вы могли его позже выпустить.
Ответ 9
Бывают случаи, когда использование указателей функций может ускорить обработку. Вместо длинных операторов switch или if-then-else могут использоваться простые таблицы отправки.
Ответ 10
В соответствии с моим личным опытом они могут помочь вам сохранить важные строки кода.
Рассмотрим условие:
{
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];
Ответ 11
Указатели функций быстрые
В каком контексте? По сравнению с?
Похоже, вы просто хотите использовать указатели на функции ради их использования. Это было бы плохо.
Указатель на функцию обычно используется как обработчик обратного вызова или события.