Обновление: Статья с полным исходным кодом и более подробным обсуждением была опубликована в Проекте кода.
Что ж, проблема с указателями наметоды в том, что они не все одинакового размера.Поэтому вместо того, чтобы хранить указатели на методы напрямую, нам нужно «стандартизировать» их, чтобы они имели постоянный размер.Это то, что Дон Клагстон пытается достичь в своей статье Code Project.Он делает это, используя глубокие знания самых популярных компиляторов.Я утверждаю, что это можно сделать в «обычном» C ++, не требуя таких знаний.
Рассмотрим следующий код:
void DoSomething(int)
{
}
void InvokeCallback(void (*callback)(int))
{
callback(42);
}
int main()
{
InvokeCallback(&DoSomething);
return 0;
}
Это один из способов реализовать обратный вызов с использованием простого старогоуказатель на функциюОднако это не работает для методов в объектах.Давайте исправим это:
class Foo
{
public:
void DoSomething(int) {}
static void DoSomethingWrapper(void* obj, int param)
{
static_cast<Foo*>(obj)->DoSomething(param);
}
};
void InvokeCallback(void* instance, void (*callback)(void*, int))
{
callback(instance, 42);
}
int main()
{
Foo f;
InvokeCallback(static_cast<void*>(&f), &Foo::DoSomethingWrapper);
return 0;
}
Теперь у нас есть система обратных вызовов, которая может работать как для свободных функций, так и для функций-членов.Это, однако, неуклюже и подвержено ошибкам.Тем не менее, существует закономерность - использование функции-оболочки для «перенаправления» статического вызова функции на вызов метода в соответствующем экземпляре.Мы можем использовать шаблоны, чтобы помочь с этим - давайте попробуем обобщить функцию-обертку:
template<typename R, class T, typename A1, R (T::*Func)(A1)>
R Wrapper(void* o, A1 a1)
{
return (static_cast<T*>(o)->*Func)(a1);
}
class Foo
{
public:
void DoSomething(int) {}
};
void InvokeCallback(void* instance, void (*callback)(void*, int))
{
callback(instance, 42);
}
int main()
{
Foo f;
InvokeCallback(static_cast<void*>(&f),
&Wrapper<void, Foo, int, &Foo::DoSomething> );
return 0;
}
Это все еще чрезвычайно неуклюже, но по крайней мере теперь нам не нужно выписывать функцию-обертку каждый раз (по крайней мере для случая с 1 аргументом).Еще одна вещь, которую мы можем обобщить, это то, что мы всегда передаем указатель на void*
.Вместо того, чтобы передавать это как два разных значения, почему бы не собрать их вместе?И пока мы это делаем, почему бы не обобщить это?Эй, давайте добавим operator()()
, чтобы мы могли вызывать его как функцию!
template<typename R, typename A1>
class Callback
{
public:
typedef R (*FuncType)(void*, A1);
Callback(void* o, FuncType f) : obj(o), func(f) {}
R operator()(A1 a1) const
{
return (*func)(obj, a1);
}
private:
void* obj;
FuncType func;
};
template<typename R, class T, typename A1, R (T::*Func)(A1)>
R Wrapper(void* o, A1 a1)
{
return (static_cast<T*>(o)->*Func)(a1);
}
class Foo
{
public:
void DoSomething(int) {}
};
void InvokeCallback(Callback<void, int> callback)
{
callback(42);
}
int main()
{
Foo f;
Callback<void, int> cb(static_cast<void*>(&f),
&Wrapper<void, Foo, int, &Foo::DoSomething>);
InvokeCallback(cb);
return 0;
}
Мы делаем успехи!Но сейчас наша проблема в том, что синтаксис абсолютно ужасен.Синтаксис выглядит избыточным;не может компилятор выяснить типы из указателя на сам метод?К сожалению нет, но мы можем помочь.Помните, что компилятор может выводить типы посредством вывода аргументов шаблона при вызове функции.Так почему бы нам не начать с этого?
template<typename R, class T, typename A1>
void DeduceMemCallback(R (T::*)(A1)) {}
Внутри функции мы знаем, что такое R
, T
и A1
.Так что, если мы сможем создать структуру, которая может «хранить» эти типы и возвращать их из функции?
template<typename R, class T, typename A1>
struct DeduceMemCallbackTag
{
};
template<typename R, class T, typename A1>
DeduceMemCallbackTag2<R, T, A1> DeduceMemCallback(R (T::*)(A1))
{
return DeduceMemCallbackTag<R, T, A1>();
}
И поскольку DeduceMemCallbackTag
знает о типах, почему бы не поместить нашу функцию-обертку в качестве статическойфункция в этом?И поскольку в нем есть функция-обертка, почему бы не поместить в нее код для создания нашего Callback
объекта?
template<typename R, typename A1>
class Callback
{
public:
typedef R (*FuncType)(void*, A1);
Callback(void* o, FuncType f) : obj(o), func(f) {}
R operator()(A1 a1) const
{
return (*func)(obj, a1);
}
private:
void* obj;
FuncType func;
};
template<typename R, class T, typename A1>
struct DeduceMemCallbackTag
{
template<R (T::*Func)(A1)>
static R Wrapper(void* o, A1 a1)
{
return (static_cast<T*>(o)->*Func)(a1);
}
template<R (T::*Func)(A1)>
inline static Callback<R, A1> Bind(T* o)
{
return Callback<R, A1>(o, &DeduceMemCallbackTag::Wrapper<Func>);
}
};
template<typename R, class T, typename A1>
DeduceMemCallbackTag<R, T, A1> DeduceMemCallback(R (T::*)(A1))
{
return DeduceMemCallbackTag<R, T, A1>();
}
Стандарт C ++ позволяет нам вызывать статические функции для экземпляров (!):
class Foo
{
public:
void DoSomething(int) {}
};
void InvokeCallback(Callback<void, int> callback)
{
callback(42);
}
int main()
{
Foo f;
InvokeCallback(
DeduceMemCallback(&Foo::DoSomething)
.Bind<&Foo::DoSomething>(&f)
);
return 0;
}
Тем не менее, это длинное выражение, но мы можем поместить его в простой макрос (!):
template<typename R, typename A1>
class Callback
{
public:
typedef R (*FuncType)(void*, A1);
Callback(void* o, FuncType f) : obj(o), func(f) {}
R operator()(A1 a1) const
{
return (*func)(obj, a1);
}
private:
void* obj;
FuncType func;
};
template<typename R, class T, typename A1>
struct DeduceMemCallbackTag
{
template<R (T::*Func)(A1)>
static R Wrapper(void* o, A1 a1)
{
return (static_cast<T*>(o)->*Func)(a1);
}
template<R (T::*Func)(A1)>
inline static Callback<R, A1> Bind(T* o)
{
return Callback<R, A1>(o, &DeduceMemCallbackTag::Wrapper<Func>);
}
};
template<typename R, class T, typename A1>
DeduceMemCallbackTag<R, T, A1> DeduceMemCallback(R (T::*)(A1))
{
return DeduceMemCallbackTag<R, T, A1>();
}
#define BIND_MEM_CB(memFuncPtr, instancePtr) \
(DeduceMemCallback(memFuncPtr).Bind<(memFuncPtr)>(instancePtr))
class Foo
{
public:
void DoSomething(int) {}
};
void InvokeCallback(Callback<void, int> callback)
{
callback(42);
}
int main()
{
Foo f;
InvokeCallback(BIND_MEM_CB(&Foo::DoSomething, &f));
return 0;
}
Мы можем улучшить объект Callback
, добавив безопасный bool.Также полезно отключить операторы равенства, поскольку невозможно сравнить два Callback
объекта.Еще лучше использовать частичную специализацию, чтобы обеспечить «предпочтительный синтаксис».Это дает нам:
template<typename FuncSignature>
class Callback;
template<typename R, typename A1>
class Callback<R (A1)>
{
public:
typedef R (*FuncType)(void*, A1);
Callback() : obj(0), func(0) {}
Callback(void* o, FuncType f) : obj(o), func(f) {}
R operator()(A1 a1) const
{
return (*func)(obj, a1);
}
typedef void* Callback::*SafeBoolType;
operator SafeBoolType() const
{
return func != 0? &Callback::obj : 0;
}
bool operator!() const
{
return func == 0;
}
private:
void* obj;
FuncType func;
};
template<typename R, typename A1> // Undefined on purpose
void operator==(const Callback<R (A1)>&, const Callback<R (A1)>&);
template<typename R, typename A1>
void operator!=(const Callback<R (A1)>&, const Callback<R (A1)>&);
template<typename R, class T, typename A1>
struct DeduceMemCallbackTag
{
template<R (T::*Func)(A1)>
static R Wrapper(void* o, A1 a1)
{
return (static_cast<T*>(o)->*Func)(a1);
}
template<R (T::*Func)(A1)>
inline static Callback<R (A1)> Bind(T* o)
{
return Callback<R (A1)>(o, &DeduceMemCallbackTag::Wrapper<Func>);
}
};
template<typename R, class T, typename A1>
DeduceMemCallbackTag<R, T, A1> DeduceMemCallback(R (T::*)(A1))
{
return DeduceMemCallbackTag<R, T, A1>();
}
#define BIND_MEM_CB(memFuncPtr, instancePtr) \
(DeduceMemCallback(memFuncPtr).Bind<(memFuncPtr)>(instancePtr))
Пример использования:
class Foo
{
public:
float DoSomething(int n) { return n / 100.0f; }
};
float InvokeCallback(int n, Callback<float (int)> callback)
{
if(callback) { return callback(n); }
return 0.0f;
}
int main()
{
Foo f;
float result = InvokeCallback(97, BIND_MEM_CB(&Foo::DoSomething, &f));
// result == 0.97
return 0;
}
Я проверил это на компиляторе Visual C ++ (версия 15.00.30729.01, которая поставляется с VS 2008), ивам нужен довольно свежий компилятор для использования кода.Путем проверки разборки компилятор смог оптимизировать функцию-обертку и вызов DeduceMemCallback
, сократив до простых назначений указателей.
Он прост в использовании для обеих сторон обратного вызова и использует только(что я считаю) стандарт C ++.Код, который я показал выше, работает для функций-членов с 1 аргументом, но может быть обобщен для большего количества аргументов.Кроме того, его можно обобщить, разрешив поддержку статических функций.
Обратите внимание, что объект Callback
не требует выделения кучи - они имеют постоянный размер благодаря этой процедуре «стандартизации».Из-за этого объект Callback
может быть членом более крупного класса, поскольку он имеет конструктор по умолчанию.Это также присваивается (функции сгенерированные компилятором функции копирования достаточно).Это также безопасно для типов благодаря шаблонам.