72 votes

5 ans plus tard, y a-t-il quelque chose de mieux que les "Fastest Possible C++ Delegates"?

Je sais que le sujet des "délégués C++" a été traité à l'excès, et à la fois http://www.codeproject.com et http://stackoverflow.com couvrent en profondeur la question.

En général, il semble que l'implémentation de délégué la plus rapide de Don Clugston soit le premier choix pour beaucoup de gens. Il y en a quelques autres populaires.

Cependant, j'ai noté que la plupart de ces articles sont anciens (vers 2005) et de nombreux choix de conception semblent avoir été faits en tenant compte des anciens compilateurs comme VC7.

J'ai besoin d'une implémentation de délégué très rapide pour une application audio.

J'ai toujours besoin que ce soit portable (Windows, Mac, Linux) mais je n'utilise que des compilateurs modernes (VC9, celui de VS2008 SP1 et GCC 4.5.x).

Mes principaux critères sont:

  • il doit être rapide!
  • il doit être compatible avec les futures versions des compilateurs. J'ai des doutes à ce sujet avec l'implémentation de Don car il précise explicitement qu'elle ne respecte pas la norme.
  • facultativement, une syntaxe KISS et une facilité d'utilisation sont agréables à avoir
  • la multidiffusion serait bien, bien que je sois convaincu que c'est vraiment facile à construire autour de n'importe quelle bibliothèque de délégués

De plus, je n'ai pas vraiment besoin de fonctionnalités exotiques. J'ai juste besoin du bon vieux truc de pointeur vers une méthode. Pas besoin de prendre en charge les méthodes statiques, les fonctions libres ou des choses comme ça.

Quelle est l'approche recommandée aujourd'hui? Toujours utiliser la version de Don? Ou y a-t-il un "consensus communautaire" sur une autre option?

Je ne veux vraiment pas utiliser Boost.signal/signal2 car ce n'est pas acceptable en termes de performance. Une dépendance à QT n'est pas acceptable non plus.

De plus, j'ai vu quelques bibliothèques plus récentes en faisant des recherches sur Google, comme par exemple cpp-events mais je n'ai pu trouver aucun retour d'utilisateurs, y compris sur SO.

120voto

In silico Points 30778

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.

10voto

Je voulais rebondir sur la réponse de @Insilico avec mes propres informations.

Avant d'avoir découvert cette réponse, j'essayais de trouver des rappels rapides qui ne génèrent aucun surcoût et qui étaient uniquement comparables / identifiés par la signature de la fonction. Ce que j'ai fini par créer - avec l'aide sérieuse des Klingons qui se sont retrouvés à un barbecue - fonctionne pour tous les types de fonctions (à l'exception des Lambdas, sauf si vous stockez le Lambda, mais ne le faites pas car c'est vraiment difficile et compliqué et cela pourrait entraîner un robot qui vous prouve à quel point c'est difficile et vous oblige à en subir les conséquences). Merci à @sehe, @nixeagle, @StackedCrooked, @CatPlusPlus, @Xeo, @DeadMG et bien sûr @Insilico pour l'aide apportée à la création du système d'événements. N'hésitez pas à utiliser comme bon vous semble.

Quoi qu'il en soit, un exemple est disponible sur ideone, mais le code source est également disponible ici pour votre usage (parce que, depuis la fermeture de Liveworkspace, je ne leur fais pas confiance aux services de compilation douteux. Qui sait quand ideone tombera en panne ?!). J'espère que cela sera utile pour quelqu'un qui n'est pas occupé à critiquer le monde en morceaux Lambda/Fonction :

NOTE IMPORTANTE : À l'heure actuelle (28/11/2012, 21h35), cette version variadique ne fonctionnera pas avec le Microsoft VC++ 2012 November CTP (Milan). Si vous souhaitez l'utiliser avec cela, vous devrez vous débarrasser de tout ce qui est variadique et énumérer explicitement le nombre d'arguments (et éventuellement spécialiser le type à 1 argument pour Event pour void) pour le faire fonctionner. C'est pénible, et je n'ai réussi à l'écrire que pour 4 arguments avant de me fatiguer (et de décider que passer plus de 4 arguments était un peu exagéré).

Exemple de code source

Code source :

#include 
#include 
#include 
#include 

template
class Callback;

template
class Callback {
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(a)...);
        }
        typedef void* Callback::*SafeBoolType;
        operator SafeBoolType() const {
                return func? &Callback::obj : 0;
        }
        bool operator!() const {
                return func == 0;
        }
        bool operator== (const Callback& right) const {
                return obj == right.obj && func == right.func;
        }
        bool operator!= (const Callback& right) const {
                return obj != right.obj || func != right.func;
        }
private:
        void* obj;
        TFunc func;
};

namespace detail {
        template
        struct DeduceConstMemCallback { 
                template inline static Callback Bind(T* o) {
                        struct _ { static R wrapper(void* o, Args... a) { return (static_cast(o)->*Func)(std::forward(a)...); } };
                        return Callback(o, (R(*)(void*, Args...)) _::wrapper);
                }
        };

        template
    struct DeduceMemCallback { 
                template inline static Callback Bind(T* o) {
                        struct _ { static R wrapper(void* o, Args... a) { return (static_cast(o)->*Func)(std::forward(a)...); } };
                        return Callback(o, (R(*)(void*, Args...)) _::wrapper);
                }
        };

        template
        struct DeduceStaticCallback { 
                template inline static Callback Bind() { 
                        struct _ { static R wrapper(void*, Args... a) { return (*Func)(std::forward(a)...); } };
                        return Callback(0, (R(*)(void*, Args...)) _::wrapper); 
                }
        };
}

template
detail::DeduceConstMemCallback DeduceCallback(R(T::*)(Args...) const) {
    return detail::DeduceConstMemCallback();
}

template
detail::DeduceMemCallback DeduceCallback(R(T::*)(Args...)) {
        return detail::DeduceMemCallback();
}

template
detail::DeduceStaticCallback DeduceCallback(R(*)(Args...)) {
        return detail::DeduceStaticCallback();
}

template  class Event {
public:
        typedef void(*TSignature)(T1...);
        typedef Callback TCallback;
        typedef std::vector InvocationTable;

protected:
        InvocationTable invocations;

public:
        const static int ExpectedFunctorCount = 2;

        Event() : invocations() {
                invocations.reserve(ExpectedFunctorCount);
        }

        template  void Add() {
                TCallback c = DeduceCallback(TFunc).template Bind();
                invocations.push_back(c);
        }

        template  void Add(T& object) {
                Add(&object);
        }

        template  void Add(T* object) {
                TCallback c = DeduceCallback(TFunc).template Bind(object);
                invocations.push_back(c);
        }

        template  void Add(T& object) {
                Add(&object);
        }

        template  void Add(T* object) {
                TCallback c = DeduceCallback(TFunc).template Bind(object);
                invocations.push_back(c);
        }

        void Invoke(T1... t1) {
                for(size_t i = 0; i < invocations.size() ; ++i) invocations[i](std::forward(t1)...); 
        }

        void operator()(T1... t1) {
                Invoke(std::forward(t1)...);
        }

        size_t InvocationCount() { return invocations.size(); }

        template  bool Remove ()          
        { return Remove (DeduceCallback(TFunc).template Bind()); } 
        template  bool Remove (T& object) 
        { return Remove (&object); } 
        template  bool Remove (T* object) 
        { return Remove (DeduceCallback(TFunc).template Bind(object)); } 
        template  bool Remove (T& object) 
        { return Remove (&object); } 
        template  bool Remove (T* object) 
        { return Remove (DeduceCallback(TFunc).template Bind(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;
        }
};

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X