Ответ 1
Обновление: Статья с полным исходным кодом и более подробное обсуждение опубликованы в проекте Code.
Ну, проблема с указателями на методы заключается в том, что они не все одинакового размера. Поэтому вместо того, чтобы напрямую хранить указатели на методы, нам нужно "стандартизировать" их, чтобы они имели постоянный размер. Это то, что пытается сделать Дон Клугстон в своей статье "Проект кодекса". Он делает это, используя интимные знания самых популярных компиляторов. Я утверждаю, что это возможно сделать в "нормальном" С++, не требуя таких знаний.
Рассмотрим следующий код:
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>();
}
Стандарт С++ позволяет называть статические функции на экземплярах (!):
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 С++ (версия 15.00.30729.01, тот, который поставляется с VS 2008), и вам нужен довольно недавний компилятор для использования кода. При проверке разборки компилятор смог оптимизировать функцию обертки и вызов DeduceMemCallback
, сократив до простых назначений указателей.
Он прост в использовании для обеих сторон обратного вызова и использует только (что я считаю) стандартным С++. Код, показанный выше, работает для функций-членов с 1 аргументом, но может быть обобщен на большее количество аргументов. Его также можно обобщить, разрешив поддержку статических функций.
Обратите внимание, что объект Callback
не требует выделения кучи - они имеют постоянный размер благодаря этой процедуре "стандартизации". Из-за этого возможно, что объект Callback
будет членом более крупного класса, так как он имеет конструктор по умолчанию. Он также можно присваивать (достаточны функции генерации копии, созданные компилятором). Это также типично, благодаря шаблонам.