Указатели функций-членов и наиболее быстрые делегаты C++ - Делегаты

ОГЛАВЛЕНИЕ

Делегаты

В отличие от указателей функций-членов, нетрудно найти применение для делегатов. Их можно использовать везде, где вы используете указатель функции в программе C. Самое важное, что с помощью делегатов легко реализовать улучшенный вариант конструктивного шаблона Субъект/Наблюдатель [GoF, p. 293]. Шаблон Наблюдателя чаще всего применяется в коде графического интерфейса пользователя (GUI), но его даже выгоднее использовать в ядре приложения. Делегаты также позволяют элегантно реализовать шаблоны Стратегии [GoF, p. 315] и Состояния [GoF, p. 305].

Делегаты не только намного более полезны, чем указатели функций-членов. Они также намного проще! Так как делегаты предоставляются языками .NET, вы можете подумать, что они являются высокоуровневой концепцией, что их нелегко реализовать в ассемблерном коде. Это не так: вызов делегата по сути очень низкоуровневая концепция, и она может быть такой же низкоуровневой (и быстрой), как и обычный вызов функции. Делегат C++ должен только лишь содержать указатель this и простой указатель функции. Когда вы определяете делегат, вы включаете в него указатель this и вместе с этим вы задаете вызываемую функцию. Компилятор может выявить, как нужно изменить указатель this в момент определения делегата. Нет работы, которую нужно выполнять при вызове делегата. Зачастую компилятор может выполнить всю работу во время компиляции, так что даже определение делегата – это тривиальная операция. Ассемблерный код, порождаемый вызовом делегата в системе x86, должен быть таким простым:

    mov ecx, [this]
    call [pfunc]

Однако, нет способа генерировать такой эффективный код в стандартном C++. Borland решает эту проблему путем добавления нового ключевого слова (__closure) в свой компилятор C++, позволяя использовать удобный синтаксис и генерировать оптимальный код. Компилятор GNU также добавляет расширение языка, но оно не совместимо с Borland. Если вы используете одно из этих расширений языка, вы ограничиваете себя одним производителем компилятора. Но если вы ограничиваетесь стандартным C++, вы можете реализовать делегаты, но они не будут такими эффективными.

Любопытно, что в C# и в других языках .NET делегаты, видимо, в десятки раз медленнее, чем вызов функции (MSDN). Возможно, причина этого – уборка мусора и требования безопасности .NET. Недавно Microsoft добавил "единую модель событий" в Visual C++ с ключевыми словами __event, __raise, __hook, __unhook, event_source и event_receiver. Вероятно, это функция ужасна. Она полностью нестандартная, имеет неприятный синтаксис и даже не выглядит как C++, при этом генерирует неэффективный код.

Потребность в очень быстрых делегатах

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

Есть много реализаций этой схемы, включая несколько в CodeProject. Они различаются сложностью и синтаксисом (особенно тем, насколько похож их синтаксис на C#), а также универсальностью. Полная реализация - это boost::function. Недавно она была принята в следующей версии стандарта C++ [Sutter1]. Ожидается, что ее применение станет широко распространенным.

Хотя традиционные реализации довольно искусны, нам они кажутся неудовлетворительными. Хотя они предоставляют требуемую функциональность, они скрывают лежащую в их основе проблему: в языке не уделяется внимания низкоуровневой конструкции. Неприятно, что на всех платформах код 'вызывателя функции-члена' будет одинаков почти для всех классов. Более того, используется память кучи. Для некоторых приложений это неприемлемо.

Один из наших проектов – это программа моделирования дискретных событий. Ядро такой программы – это диспетчер событий, который вызывает функции-члены различных моделируемых объектов. Многие из данных функций-членов являются очень простыми: они всего лишь обновляют внутреннее состояние объекта и иногда добавляют будущие события в очередь событий. Это идеальная ситуация для использования делегатов. Однако такой делегат вызывается только один раз. Сначала мы применяли boost::function, но затем обнаружили, что выделение памяти для делегатов занимает примерно треть от всего времени выполнения программы! Нам были нужны настоящие делегаты. Они должны быть всего лишь двумя ассемблерными инструкциями!

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