Вопрос

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

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

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

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

Это было полезно?

Решение

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

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

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

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

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

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

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

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

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

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

Другие советы

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

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);

);

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

И нет, они не о скорости.Указатели на функции — это удобство.

Джонатан

Каковы сегодня наилучшие и наиболее практичные способы использования целых чисел в современных языках стиля C?

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

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

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

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

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

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

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

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

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

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

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

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

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

BOOL EnumWindows(          
    WNDENUMPROC lpEnumFunc,
    LPARAM lParam
);

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

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

По моему личному опыту, они могут помочь вам сэкономить значительные строки кода.

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

switch(sample_var)
{

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

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











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

}

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

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

*arrFuncPoint[sample_var];

Указатели функций работают быстро

В каком контексте?По сравнению с?

Похоже, вы просто хотите использовать указатели на функции ради их использования.Это было бы плохо.

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top