//here eventID is 10, contextID is 1;
}
int main()
{
int eventID = 10; int contextID = 1;
Initiator initiator; // (1)
initiator.setup(NativeHandler); // (2)
initiator.setup(std::bind(AnotherHandler, contextID, std::placeholders::_1)); // (3)
initiator.run(); // (4)
}
В строке 1 объявлен инициатор. В строке 2 происходит настройка инициатора с передачей ему указателя на функцию с «родной» сигнатурой, т. е. сигнатурой, для которой инициатор осуществляет вызов. Если бы мы после этого запустили инициатор путем вызова метода run, то инициатор вызывал бы функцию NativeCallbackHandler. В строке 3 вместо функции с «родной» сигнатурой мы подставляем объект связывания, который будет перенаправлять вызов в другую функцию. В строке 4 запускаем инициатор, в котором после вызова функции объекта связывания будет осуществлен вызов AnotherCallbackHandler с соответствующими параметрами. Аналогичным образом, подставляя нужные связывания из Табл. 13, осуществляется перенаправление вызовов для других вариантов.
Итак, использование объектов связывания предлагает универсальный способ преобразования вызовов: вместо объектов преобразования (п. 4.2.2, 4.6.3) в универсальный аргумент подставляется объект связывания, сгенерированный соответствующим вызовом std::bind.
4.6.7. Универсальный аргумент и производительность
Может показаться, что организация обратных вызовов с использованием std::function в качестве универсального аргумента является наилучшим решением, предлагающим простоту реализации в сочетании с максимальной гибкостью. В большинстве случаев это действительно так, однако std::function обладает недостатком, который может свести на нет все остальные достоинства: большие временные затраты для осуществления вызова по сравнению с другими способами реализации. Причины этого следующие:
1) при вызове происходит проверка, настроен ли аргумент;
2) вызов происходит через промежуточный объект с виртуальной функцией (см. 4.5.1) – расходуется дополнительное время для вызова этой функции;
3) поскольку промежуточный объект создается динамически, его адрес может изменяться, что требует загрузки адреса перед вызовом;
4) на этапе компиляции тип аргумента неизвестен, поэтому код обработки не может быть встроен в точку вызова.
Первые три причины вносят незначительный вклад в общее время, затрачиваемое на выполнение вызова, а вот четвертая может привести к резкому падению производительности. Мы уже рассматривали подобную проблему при анализе функциональных объектов (п. 2.4.6): при малом объеме кода обработчика время, затраченное на вызов функции, может превысить время выполнения тела функции.
Проведем эксперимент. Напишем программу, в которой циклически будут осуществляться вызовы различных типов для кода небольшого размера25. Поскольку код обработчика один и тот же, общее время, затраченное на выполнение вызова, будет прямо пропорционально времени, затраченному на организацию вызова. Запустим программу и выполним профилирование26. Результаты профилирования представлены в Табл. 14, графически они изображены на Рис. 1927.
Табл. 14. Время, затраченное на выполнение вызовов различных типов для кода небольшого размера, млс.
Рис. 19. Гистограмма результатов профилирования вызовов различных типов для кода небольшого размера
Проанализируем вначале результаты при организации вызовов напрямую, без использования универсального аргумента. Быстродействие для указателя на функцию и указателя на метод различается незначительно, а вот при использовании функциональных объектов и лямбда-выражений оно вырастает на порядки28, потому что код встраивается в точку вызова.
Посмотрим теперь результаты при использовании универсального аргумента. Если сравнить с вызовами напрямую, время выполнения ожидаемо увеличивается. Однако если для указателя на функцию и указателя на метод увеличение незначительно, то для функционального объекта и лямбда-выражения оно увеличивается настолько, что практически исчезает отличие от других способов. Теперь код обработчика не встраивается в точку вызова, и расходы на вызов функции во много раз превышают расходы на выполнение тела функции.
Модифицируем теперь код обработчика таким образом, чтобы оптимизатор не мог встроить его в точку вызова. Числовые значения замеров представлены в Табл. 15, графически они изображены на Рис. 20. Теперь картина получается иная: прямое использование функциональных объектов и лямбда-выражений не дают заметного выигрыша в производительности, а использование универсального аргумента увеличивает время выполнения незначительно.
Табл. 15. Время, затраченное на выполнение вызовов различных типов для кода большого размера, млс.