c++ delegates performance

5 años después, ¿hay algo mejor que los "Delegados Posibles de C++ más rápidos"?



delegates performance (2)

Quería seguir la respuesta de @ Insilico con un poco de mis propias cosas.

Antes de encontrarme con esta respuesta, intentaba encontrar devoluciones de llamada rápidas que no incurrieran en gastos indirectos y que fueran únicamente comparables / identificables solo por la firma de la función. Lo que terminé creando -con alguna ayuda seria de Klingons Who Happened To Be at BBQ- funciona para todo tipo de funciones (excepto Lambdas, a menos que almacene el Lambda, pero no lo intente porque es realmente difícil y difícil de hacer y puede dar como resultado que un robot te pruebe lo difícil que es y te haga comer la mierda por ello ). Gracias a @sehe, @nixeagle, @StackedCrooked, @CatPlusPlus, @Xeo, @DeadMG y por supuesto a @Insilico por la ayuda en la creación del sistema de eventos. Siéntase libre de usarlo como lo desee.

De todos modos, hay un ejemplo en Ideone, pero el código fuente también está aquí para su uso (porque, dado que Liveworkspace se cayó, no confío en los servicios de compilación turbios. ¿Quién sabe cuándo bajará?). Espero que esto sea útil para alguien que no está ocupado Lambda / Function-objecting el mundo en pedazos:

NOTA IMPORTANTE: A partir de este momento (28/11/2012, 9:35 PM) Esta variada versión no funcionará con el Microsoft VC ++ 2012 noviembre CTP (Milán). Si quieres usarlo con eso, tendrás que deshacerte de todas las cosas variadas y enumerar explícitamente la cantidad de argumentos (y posiblemente plantilla-especializar el tipo de 1 argumento para Event for void ) para que funcione. Es un dolor, y solo pude escribirlo para 4 argumentos antes de que me cansara (y decidí que pasar más de 4 argumentos era un poco exagerado).

Ejemplo de fuente

Fuente:

#include <iostream> #include <vector> #include <utility> #include <algorithm> template<typename TFuncSignature> class Callback; template<typename R, typename... Args> class Callback<R(Args...)> { public: typedef R(*TFunc)(void*, Args...); Callback() : obj(0), func(0) {} Callback(void* o, TFunc f) : obj(o), func(f) {} R operator()(Args... a) const { return (*func)(obj, std::forward<Args>(a)...); } typedef void* Callback::*SafeBoolType; operator SafeBoolType() const { return func? &Callback::obj : 0; } bool operator!() const { return func == 0; } bool operator== (const Callback<R (Args...)>& right) const { return obj == right.obj && func == right.func; } bool operator!= (const Callback<R (Args...)>& right) const { return obj != right.obj || func != right.func; } private: void* obj; TFunc func; }; namespace detail { template<typename R, class T, typename... Args> struct DeduceConstMemCallback { template<R(T::*Func)(Args...) const> inline static Callback<R(Args...)> Bind(T* o) { struct _ { static R wrapper(void* o, Args... a) { return (static_cast<T*>(o)->*Func)(std::forward<Args>(a)...); } }; return Callback<R(Args...)>(o, (R(*)(void*, Args...)) _::wrapper); } }; template<typename R, class T, typename... Args> struct DeduceMemCallback { template<R(T::*Func)(Args...)> inline static Callback<R(Args...)> Bind(T* o) { struct _ { static R wrapper(void* o, Args... a) { return (static_cast<T*>(o)->*Func)(std::forward<Args>(a)...); } }; return Callback<R(Args...)>(o, (R(*)(void*, Args...)) _::wrapper); } }; template<typename R, typename... Args> struct DeduceStaticCallback { template<R(*Func)(Args...)> inline static Callback<R(Args...)> Bind() { struct _ { static R wrapper(void*, Args... a) { return (*Func)(std::forward<Args>(a)...); } }; return Callback<R(Args...)>(0, (R(*)(void*, Args...)) _::wrapper); } }; } template<typename R, class T, typename... Args> detail::DeduceConstMemCallback<R, T, Args...> DeduceCallback(R(T::*)(Args...) const) { return detail::DeduceConstMemCallback<R, T, Args...>(); } template<typename R, class T, typename... Args> detail::DeduceMemCallback<R, T, Args...> DeduceCallback(R(T::*)(Args...)) { return detail::DeduceMemCallback<R, T, Args...>(); } template<typename R, typename... Args> detail::DeduceStaticCallback<R, Args...> DeduceCallback(R(*)(Args...)) { return detail::DeduceStaticCallback<R, Args...>(); } template <typename... T1> class Event { public: typedef void(*TSignature)(T1...); typedef Callback<void(T1...)> TCallback; typedef std::vector<TCallback> InvocationTable; protected: InvocationTable invocations; public: const static int ExpectedFunctorCount = 2; Event() : invocations() { invocations.reserve(ExpectedFunctorCount); } template <void (* TFunc)(T1...)> void Add() { TCallback c = DeduceCallback(TFunc).template Bind<TFunc>(); invocations.push_back(c); } template <typename T, void (T::* TFunc)(T1...)> void Add(T& object) { Add<T, TFunc>(&object); } template <typename T, void (T::* TFunc)(T1...)> void Add(T* object) { TCallback c = DeduceCallback(TFunc).template Bind<TFunc>(object); invocations.push_back(c); } template <typename T, void (T::* TFunc)(T1...) const> void Add(T& object) { Add<T, TFunc>(&object); } template <typename T, void (T::* TFunc)(T1...) const> void Add(T* object) { TCallback c = DeduceCallback(TFunc).template Bind<TFunc>(object); invocations.push_back(c); } void Invoke(T1... t1) { for(size_t i = 0; i < invocations.size() ; ++i) invocations[i](std::forward<T1>(t1)...); } void operator()(T1... t1) { Invoke(std::forward<T1>(t1)...); } size_t InvocationCount() { return invocations.size(); } template <void (* TFunc)(T1...)> bool Remove () { return Remove (DeduceCallback(TFunc).template Bind<TFunc>()); } template <typename T, void (T::* TFunc)(T1...)> bool Remove (T& object) { return Remove <T, TFunc>(&object); } template <typename T, void (T::* TFunc)(T1...)> bool Remove (T* object) { return Remove (DeduceCallback(TFunc).template Bind<TFunc>(object)); } template <typename T, void (T::* TFunc)(T1...) const> bool Remove (T& object) { return Remove <T, TFunc>(&object); } template <typename T, void (T::* TFunc)(T1...) const> bool Remove (T* object) { return Remove (DeduceCallback(TFunc).template Bind<TFunc>(object)); } protected: bool Remove( TCallback const& target ) { auto it = std::find(invocations.begin(), invocations.end(), target); if (it == invocations.end()) return false; invocations.erase(it); return true; } };

Sé que el tema de los "delegados de C ++" se ha llevado a la muerte, y tanto http://www.codeproject.com como http://stackoverflow.com cubren profundamente la cuestión.

En general, parece que el delegado más rápido posible de Don Clugston es la primera opción para muchas personas. Hay algunos otros populares.

Sin embargo, noté que la mayoría de esos artículos son antiguos (alrededor de 2005) y muchas elecciones de diseño parecen haberse realizado teniendo en cuenta los viejos compiladores como VC7.

Necesito una implementación delegada muy rápida para una aplicación de audio.

Todavía necesito que sea portátil (Windows, Mac, Linux) pero solo uso compiladores modernos (VC9, el de VS2008 SP1 y GCC 4.5.x).

Mis principales criterios son:

  • debe ser rápido!
  • debe ser compatible con las versiones más recientes de los compiladores. Tengo algunas dudas al respecto con la implementación de Don porque explícitamente dice que no cumple con los estándares.
  • opcionalmente, es agradable tener una sintaxis KISS y facilidad de uso
  • la multidifusión sería agradable, aunque estoy convencido de que es realmente fácil compilarla en cualquier biblioteca de delegados

Además, realmente no necesito características exóticas. Solo necesito la buena idea del viejo puntero al método. No es necesario que admita métodos estáticos, funciones gratuitas o cosas por el estilo.

A partir de hoy, ¿cuál es el enfoque recomendado? ¿Aún usas la versión de Don ? ¿O hay un "consenso de la comunidad" sobre otra opción?

Realmente no quiero usar Boost.signal / signal2 porque no es aceptable en términos de rendimiento. Una dependencia en QT tampoco es aceptable.

Además, he visto algunas bibliotecas más nuevas mientras busco en Google, como por ejemplo cpp-events pero no pude encontrar ningún comentario de los usuarios, incluido SO.


Actualización: Se publicó un artículo con el código fuente completo y una discusión más detallada en The Code Project.

Bueno, el problema con los punteros a los métodos es que no son todos del mismo tamaño. Entonces, en lugar de almacenar punteros a los métodos directamente, necesitamos "estandarizarlos" para que tengan un tamaño constante. Esto es lo que Don Clugston intenta lograr en su artículo de Code Project. Lo hace utilizando un conocimiento íntimo de los compiladores más populares. Afirmo que es posible hacerlo en C ++ "normal" sin requerir tal conocimiento.

Considera el siguiente código:

void DoSomething(int) { } void InvokeCallback(void (*callback)(int)) { callback(42); } int main() { InvokeCallback(&DoSomething); return 0; }

Esta es una forma de implementar una devolución de llamada utilizando un puntero de función antiguo simple. Sin embargo, esto no funciona para los métodos en objetos. Arreglemos esto:

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; }

Ahora, tenemos un sistema de devoluciones de llamadas que puede funcionar tanto para funciones libres como para miembros. Esto, sin embargo, es torpe y propenso a errores. Sin embargo, hay un patrón: el uso de una función de envoltura para "reenviar" la llamada de función estática a una llamada de método en la instancia adecuada. Podemos usar plantillas para ayudar con esto - intentemos generalizar la función de envoltura:

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; }

Esto sigue siendo extremadamente torpe, pero al menos ahora no tenemos que escribir una función de envoltura cada vez (al menos para el caso de 1 argumento). Otra cosa que podemos generalizar es el hecho de que siempre estamos pasando un puntero a void* . En lugar de pasarlo como dos valores diferentes, ¿por qué no juntarlos? Y mientras lo hacemos, ¿por qué no generalizarlo también? Oye, entremos un operator()() para que podamos llamarlo como una función!

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; }

¡Estamos progresando! Pero ahora nuestro problema es el hecho de que la sintaxis es absolutamente horrible. La sintaxis parece redundante; ¿No puede el compilador descubrir los tipos del puntero al método en sí mismo? Desafortunadamente no, pero podemos ayudarlo. Recuerde que un compilador puede deducir tipos a través de la deducción de argumento de plantilla en una llamada a función. Entonces, ¿por qué no empezamos con eso?

template<typename R, class T, typename A1> void DeduceMemCallback(R (T::*)(A1)) {}

Dentro de la función, sabemos qué es R , T y A1 . Entonces, ¿qué pasa si podemos construir una estructura que pueda "retener" estos tipos y devolverlos desde la función?

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>(); }

Y dado que DeduceMemCallbackTag conoce los tipos, ¿por qué no pone nuestra función de contenedor como una función estática? Y como la función contenedora está en ella, ¿por qué no poner el código para construir nuestro objeto 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>(); }

El estándar C ++ nos permite llamar funciones estáticas en instancias (!):

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; }

Aún así, es una expresión larga, pero podemos poner eso en una 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; }

Podemos mejorar el objeto Callback agregando un bool seguro. También es una buena idea desactivar los operadores de igualdad ya que no es posible comparar dos objetos de Callback . Aún mejor, es usar especialización parcial para permitir una "sintaxis preferida". Esto nos da:

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))

Ejemplo de uso:

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; }

He probado esto en el compilador de Visual C ++ (versión 15.00.30729.01, el que viene con VS 2008), y necesitas un compilador bastante reciente para usar el código. Al inspeccionar el desmontaje, el compilador pudo optimizar la función de envoltura y la llamada DeduceMemCallback , reduciendo a simples asignaciones de puntero.

Es simple de usar para ambos lados de la devolución de llamada, y usa solo (lo que creo que es) C ++ estándar. El código que he mostrado arriba funciona para funciones miembro con 1 argumento, pero se puede generalizar a más argumentos. También se puede generalizar más permitiendo soporte para funciones estáticas.

Tenga en cuenta que el objeto de Callback no requiere asignación de pila: tienen un tamaño constante gracias a este procedimiento de "estandarización". Debido a esto, es posible hacer que un objeto Callback sea ​​miembro de una clase más grande, ya que tiene un constructor predeterminado. También es asignable (las funciones de asignación de copia generadas por el compilador son suficientes). También es seguro, gracias a las plantillas.