Update: Ein Artikel mit dem vollständigen Quellcode und einer ausführlicheren Diskussion wurde auf The Code Project veröffentlicht.
Das Problem mit Zeigern auf Methoden ist, dass sie nicht alle gleich groß sind. Anstatt Zeiger auf Methoden direkt zu speichern, müssen wir sie so "standardisieren", dass sie eine konstante Größe haben. Dies versucht Don Clugston in seinem Artikel über das Code-Projekt zu erreichen. Er nutzt dazu die vertrauten Kenntnisse der beliebtesten Compiler. Ich behaupte, dass es möglich ist, dies in "normalem" C ++ zu tun, ohne solche Kenntnisse zu erfordern.
Betrachten Sie den folgenden Code:
void DoSomething(int)
{
}
void InvokeCallback(void (*callback)(int))
{
callback(42);
}
int main()
{
InvokeCallback(&DoSomething);
return 0;
}
Dies ist eine Möglichkeit, einen Rückruf mit einem einfachen alten Funktionszeiger zu implementieren. Dies funktioniert jedoch nicht für Methoden in Objekten. Lassen Sie uns das beheben:
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;
}
Jetzt haben wir ein System von Rückrufen, die sowohl für freie als auch für Mitgliedsfunktionen funktionieren können. Dies ist jedoch ungeschickt und fehleranfällig. Es gibt jedoch ein Muster - die Verwendung einer Wrapper-Funktion, um den statischen Funktionsaufruf an einen Methodenaufruf auf der richtigen Instanz weiterzuleiten. Wir können Vorlagen verwenden, um dies zu unterstützen. Versuchen wir, die Wrapper-Funktion zu verallgemeinern:
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;
}
Dies ist immer noch äußerst umständlich, aber zumindest müssen wir jetzt nicht jedes Mal eine Wrapper-Funktion ausschreiben (zumindest für den Fall mit 1 Argument). Eine andere Sache, die wir verallgemeinern können, ist die Tatsache, dass wir immer einen Zeiger auf übergeben void*
. Warum nicht zusammenfügen, anstatt es als zwei verschiedene Werte zu übergeben? Und während wir das tun, warum nicht auch verallgemeinern? Hey, lass uns eine einwerfen, operator()()
damit wir sie wie eine Funktion aufrufen können!
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;
}
Wir machen Fortschritte! Aber jetzt ist unser Problem die Tatsache, dass die Syntax absolut schrecklich ist. Die Syntax erscheint redundant. Kann der Compiler die Typen nicht vom Zeiger auf die Methode selbst herausfinden? Leider nein, aber wir können es weiterhelfen. Denken Sie daran, dass ein Compiler in einem Funktionsaufruf Typen über die Ableitung von Vorlagenargumenten ableiten kann. Warum fangen wir nicht damit an?
template<typename R, class T, typename A1>
void DeduceMemCallback(R (T::*)(A1)) {}
Innerhalb der Funktion, wissen wir , was R
, T
und A1
ist. Was ist, wenn wir eine Struktur erstellen können, die diese Typen "hält" und von der Funktion zurückgibt?
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>();
}
Und da DeduceMemCallbackTag
wir über die Typen Bescheid wissen, warum nicht unsere Wrapper-Funktion als statische Funktion einfügen? Und da die Wrapper-Funktion darin enthalten ist, warum nicht den Code einfügen, um unser Callback
Objekt darin zu konstruieren ?
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>();
}
Mit dem C ++ - Standard können wir statische Funktionen für Instanzen (!) Aufrufen:
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;
}
Trotzdem ist es ein langer Ausdruck, aber wir können das in ein einfaches Makro (!) Setzen:
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;
}
Wir können das Callback
Objekt verbessern, indem wir einen sicheren Bool hinzufügen. Es ist auch eine gute Idee, die Gleichheitsoperatoren zu deaktivieren, da zwei Callback
Objekte nicht verglichen werden können. Noch besser ist es, eine teilweise Spezialisierung zu verwenden, um eine "bevorzugte Syntax" zu ermöglichen. Dies gibt uns:
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>
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))
Anwendungsbeispiel:
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));
return 0;
}
Ich habe dies auf dem Visual C ++ - Compiler (Version 15.00.30729.01, die mit VS 2008 geliefert wird) getestet, und Sie benötigen einen relativ neuen Compiler, um den Code zu verwenden. Durch die Überprüfung der Demontage konnte der Compiler die Wrapper-Funktion und den DeduceMemCallback
Aufruf optimieren und sich auf einfache Zeigerzuweisungen reduzieren.
Es ist einfach für beide Seiten des Rückrufs zu verwenden und verwendet nur (was ich glaube) Standard-C ++. Der oben gezeigte Code funktioniert für Elementfunktionen mit 1 Argument, kann jedoch auf weitere Argumente verallgemeinert werden. Es kann auch weiter verallgemeinert werden, indem statische Funktionen unterstützt werden.
Beachten Sie, dass für das Callback
Objekt keine Heap-Zuordnung erforderlich ist. Dank dieses "Standardisierungs" -Verfahrens haben sie eine konstante Größe. Aus diesem Grund ist es möglich, dass ein Callback
Objekt Mitglied einer größeren Klasse ist, da es einen Standardkonstruktor hat. Es ist auch zuweisbar (die vom Compiler generierten Kopierzuweisungsfunktionen sind ausreichend). Dank der Vorlagen ist es auch typsicher.