Mise à jour: Un article avec le code source complet et un examen plus détaillé a été publié sur Le Projet de Code.
Eh bien, le problème avec les pointeurs de méthodes est qu'elles ne sont pas toutes de la même taille. Donc, au lieu de stocker des pointeurs vers des méthodes directement, nous avons besoin de "normaliser" de sorte qu'ils sont d'une taille constante. C'est ce que Ne Clugston tente de réaliser dans son Projet de Code de l'article. Il le fait en utilisant la connaissance intime de la plus populaire des compilateurs. J'affirme qu'il est possible de le faire en "normal" C++ sans exiger de telles connaissances.
Considérons le code suivant:
void DoSomething(int)
{
}
void InvokeCallback(void (*callback)(int))
{
callback(42);
}
int main()
{
InvokeCallback(&DoSomething);
return 0;
}
C'est une façon de mettre en œuvre un rappel à l'aide d'un bon vieux pointeur de fonction. Toutefois, cela ne fonctionne pas pour les méthodes des objets. Nous allons remédier à cela:
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;
}
Maintenant, nous avons un système de rappels qui peuvent travailler à la fois libre et les fonctions de membres. Ceci, cependant, est maladroit et sujette à erreur. Cependant, il existe un modèle - l'utilisation d'une fonction wrapper "en avant" de la fonction statique appel à un appel de méthode sur l'instance appropriée. Nous pouvons utiliser des modèles pour vous aider avec cela - nous allons essayer de généraliser la fonction wrapper:
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;
}
C'est toujours extrêmement maladroit, mais au moins, maintenant, nous n'avons pas d'écrire une fonction wrapper à chaque fois (au moins pour le 1er argument de cas). Une autre chose que nous pouvons généraliser, c'est le fait que nous sommes toujours en passant un pointeur void*
. Au lieu de passer en tant que deux valeurs différentes, pourquoi ne pas les mettre ensemble? Et tandis que nous faisons, pourquoi ne pas le généraliser? Hey, ajoutons une operator()()
si on peut appeler ça comme une fonction!
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;
}
Nous sommes à faire des progrès! Mais maintenant, notre problème est le fait que la syntaxe est absolument horrible. La syntaxe apparaît redondante; ne peut pas le compilateur de déterminer le type du pointeur à la méthode elle-même? Malheureusement non, mais nous pouvons vous aider. Rappelez-vous qu'un compilateur peut en déduire types via argument de modèle de déduction dans un appel de fonction. Alors pourquoi ne pas commencer par ça?
template<typename R, class T, typename A1>
void DeduceMemCallback(R (T::*)(A1)) {}
L'intérieur de la fonction, nous savons ce qu' R
, T
et A1
. Alors que faire si nous pouvons construire une structure qui peut "tenir" ces types et de retour de la fonction?
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>();
}
Et depuis DeduceMemCallbackTag
sait sur les types, pourquoi ne pas mettre notre fonction wrapper comme une fonction statique? Et puisque la fonction wrapper y est, pourquoi ne pas mettre le code pour construire notre Callback
objet?
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>();
}
La norme C++ nous permet d'appeler des fonctions statiques sur les instances (!):
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;
}
Pourtant, c'est une longue expression, mais on peut mettre dans une macro simple (!):
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;
}
Nous pouvons améliorer l' Callback
objet par l'ajout d'un coffre-fort bool. C'est aussi une bonne idée de désactiver les opérateurs d'égalité puisqu'il n'est pas possible de comparer les deux Callback
objets. Encore mieux, est d'utiliser partielle de spécialisation afin de permettre une "syntaxe préférée". Ce qui nous donne:
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))
Exemple d'utilisation:
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;
}
J'ai testé cela sur le compilateur Visual C++ (version 15.00.30729.01, celui qui vient avec VS 2008), et vous avez besoin d'un assez récente, le compilateur à utiliser le code. Par l'inspection du démontage, le compilateur est capable d'optimiser loin la fonction wrapper et l' DeduceMemCallback
appel, réduisant à de simples pointeur de missions.
C'est simple à utiliser pour les deux côtés de la fonction de rappel, et utilise uniquement (ce que je crois être) standard C++. Le code que j'ai montré ci-dessus fonctionne pour les fonctions de membre avec 1 argument, mais peut être généralisée à d'autres arguments. Il peut également être généralisée en permettant de soutien pour les fonctions statiques.
Notez que l' Callback
objet nécessite pas d'allocation de tas - ils sont d'une taille constante grâce à cette "normalisation" de la procédure. De ce fait, il est possible d'avoir un Callback
objet que ce soit un membre de la plus grande classe, car il a un constructeur par défaut. Il est également assignable (le compilateur a généré copie d'assignation de fonctions sont suffisantes). Il est également typesafe, grâce à des modèles.