Невероятно быстрые делегаты C++
ОГЛАВЛЕНИЕ
Введение
Дон Клагстон предложил подход к делегатам (далее называемым FastDelegate), который требует такого же вызывающего кода, что и создаваемый для вызова с помощью указателя на член-функцию в простейшем случае (он описал, почему некоторые компиляторы порождают более сложный код для полиморфных классов и для классов с виртуальным наследованием). Он описал, почему многие другие популярные подходы оказались неэффективными. К сожалению, его подход основан на 'хитром трюке' (как он сказал). Он работает во многих популярных компиляторах, но несовместим со стандартами C++.
Кажется верным, что FastDelegate – это самый быстрый способ. Но мы полагаем, что это заявление требует доказательств, потому что современные оптимизирующие компиляторы C++ делают поразительные вещи. Мы уверены, что boost::function и другие делегаты, основанные на динамическом выделении памяти, являются медленными, но кто сказал, что нет других хороших подходов?
Мы собираемся предложить другой подход, который:
а) быстрый;
б) не использует динамическое выделение памяти;
с) полностью совместим со стандартами C++.
Еще один подход к делегатам
Рассмотрим делегат, получающий один аргумент и не возвращающий никакого значения. Его можно определить следующим образом, используя предпочтительный синтаксис (как boost::function и FastDelegate, наша библиотека поддерживает предпочтительный и совместимый синтаксисы; более подробную информацию смотрите в документации):
delegate<void (int)>
Этот код упрощен, чтобы вам было легче понять, как он работает. Следующий код был получен путем удаления ненужных строк ниже и выше рассматриваемого кода и путем замены параметров шаблона конкретными типами.
class delegate
{
public:
delegate()
: object_ptr(0)
, stub_ptr(0)
{}
template <class T, void (T::*TMethod)(int)>
static delegate from_method(T* object_ptr)
{
delegate d;
d.object_ptr = object_ptr;
d.stub_ptr = &method_stub<T, TMethod>; // #1
return d;
}
void operator()(int a1) const
{
return (*stub_ptr)(object_ptr, a1);
}
private:
typedef void (*stub_type)(void* object_ptr, int);
void* object_ptr;
stub_type stub_ptr;
template <class T, void (T::*TMethod)(int)>
static void method_stub(void* object_ptr, int a1)
{
T* p = static_cast<T*>(object_ptr);
return (p->*TMethod)(a1); // #2
}
};
Делегат состоит из нетипированного указателя на данные (так как делегат не должен зависеть от типа приемника) и указателя на функцию. Эта функция получает указатель на данные в качестве дополнительного параметра. Он преобразует указатель данных в указатель объекта ('void*', в отличие от указателей-членов может быть благополучно преобразован обратно в указатели объектов: [expr.static.cast], элемент 10) и вызывает требуемую функцию-член.
Когда вы создаете непустой делегат, вы неявно реализуете функцию-заглушку путем получения ее адреса (смотрите строку #1 выше). Это возможно, потому что стандарт C++ разрешает использовать указатель на член или указатель на функцию в качестве параметра шаблона ([temp.params], элемент 4):
SomeObject obj;
delegate d = delegate::from_member<SomeObject,
&SomeObject::someMethod>(&obj);
Теперь 'd' содержит указатель на функцию-заглушку, связываемый с 'someMethod' во время компиляции. Хотя был задан указатель на член, вызов в строке #2 выполняется так же быстро, как и прямой вызов метода (потому что его значение известно во время компиляции).
Как обычно, делегат можно вызвать с помощью оператора вызова встраиваемой функции, которая перенаправляет вызов целевому методу через функцию-заглушку:
d(10); // вызов SomeObject::someMethod
// для obj и передача им 10 в качестве параметра
Конечно, это предполагает дополнительный вызов функции, но непроизводительные затраты существенно зависят от оптимизатора. Иногда их может и вовсе не быть.