web-dev-qa-db-fra.com

5 ans plus tard, existe-t-il quelque chose de mieux que les "Délégués C ++ les plus rapides possibles"?

Je sais que le sujet des "délégués C++" a été fait à mort, et --- http://www.codeproject.com et http://stackoverflow.com profondément couvrir la question.

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

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

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

J'en ai encore besoin pour être 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:

  • ça doit être rapide!
  • il doit être compatible avec les versions plus récentes des compilateurs. J'ai des doutes à ce sujet avec la mise en œuvre de Don, car il déclare explicitement qu'elle n'est pas conforme aux normes.
  • en option, une syntaxe KISS et une facilité d'utilisation est agréable à avoir
  • la multidiffusion serait bien, même si je suis convaincu qu'il est très facile de le 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 du pointeur sur la méthode. Pas besoin de supporter des méthodes statiques, des fonctions libres ou des choses comme ça.

À ce jour, quelle est l'approche recommandée? Utilisez toujours version de Don ? Ou existe-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 performances. Une dépendance à QT n'est pas non plus acceptable.

De plus, j'ai vu des bibliothèques plus récentes pendant la recherche sur Google, comme par exemple cpp-events mais je n'ai trouvé aucun commentaire des utilisateurs, y compris sur SO.

72
Dinaiz

Mise à jour: n article avec le code source complet et une discussion plus détaillée a été publié sur The Code Project.

Eh bien, le problème avec les pointeurs sur les méthodes est qu'ils ne sont pas tous de la même taille. Ainsi, au lieu de stocker directement des pointeurs vers des méthodes, nous devons les "standardiser" afin qu'elles soient de taille constante. C'est ce que Don Clugston tente de réaliser dans son article Code Project. Il le fait en utilisant une connaissance intime des compilateurs les plus populaires. J'affirme qu'il est possible de le faire en C++ "normal" sans nécessiter de telles connaissances.

Considérez le code suivant:

void DoSomething(int)
{
}

void InvokeCallback(void (*callback)(int))
{
    callback(42);
}

int main()
{
    InvokeCallback(&DoSomething);
    return 0;
}

C'est une façon d'implémenter un rappel à l'aide d'un ancien pointeur de fonction simple. Cependant, cela ne fonctionne pas pour les méthodes dans les objets. Corrigeons ceci:

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 peut fonctionner pour les fonctions libres et membres. Ceci, cependant, est maladroit et sujet aux erreurs. Cependant, il existe un modèle - l'utilisation d'une fonction wrapper pour "transmettre" l'appel de fonction statique à un appel de méthode sur l'instance appropriée. Nous pouvons utiliser des modèles pour aider à cela - essayons 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 à écrire une fonction wrapper à chaque fois (au moins pour le cas à 1 argument). Une autre chose que nous pouvons généraliser est le fait que nous passons toujours un pointeur à void*. Au lieu de le passer comme deux valeurs différentes, pourquoi ne pas les assembler? Et pendant que nous le faisons, pourquoi ne pas le généraliser également? Hé, jetons une operator()() pour que nous puissions l'appeler 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 progressons! Mais maintenant, notre problème est le fait que la syntaxe est absolument horrible. La syntaxe semble redondante; le compilateur ne peut-il pas comprendre les types du pointeur vers la méthode elle-même? Malheureusement non, mais nous pouvons l'aider. N'oubliez pas qu'un compilateur peut déduire des types via la déduction d'arguments de modèle dans un appel de fonction. Alors pourquoi ne commençons-nous pas par ça?

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

A l'intérieur de la fonction, nous savons ce que R, T et A1 Est. Et si nous pouvons construire une structure qui peut "contenir" ces types et les renvoyer 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 puisque DeduceMemCallbackTag connaît les types, pourquoi ne pas y mettre notre fonction wrapper comme fonction statique? Et puisque la fonction wrapper est dedans, pourquoi ne pas y mettre le code pour construire notre objet 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>();
}

Le standard C++ nous permet d'appeler des fonctions statiques sur des 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 expression longue, mais nous pouvons mettre cela dans une simple macro (!):

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'objet Callback en ajoutant un booléen sûr. C'est également une bonne idée de désactiver les opérateurs d'égalité car il n'est pas possible de comparer deux objets Callback. Mieux encore, il faut utiliser une spécialisation partielle pour permettre une "syntaxe préférée". Cela 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, celle fournie avec VS 2008), et vous avez besoin d'un compilateur assez récent pour utiliser le code. En inspectant le démontage, le compilateur a pu optimiser la fonction wrapper et l'appel DeduceMemCallback, en réduisant les affectations de pointeurs simples.

Il est simple à utiliser pour les deux côtés du rappel et utilise uniquement (ce que je crois être) le C++ standard. Le code que j'ai montré ci-dessus fonctionne pour les fonctions membres avec 1 argument, mais peut être généralisé à plus d'arguments. Il peut également être généralisé en permettant la prise en charge des fonctions statiques.

Notez que l'objet Callback ne nécessite aucune allocation de tas - ils sont de taille constante grâce à cette procédure de "standardisation". Pour cette raison, il est possible qu'un objet Callback soit membre d'une classe plus grande, car il a un constructeur par défaut. Il est également assignable (les fonctions d'affectation de copie générées par le compilateur sont suffisantes). Il est également sûr pour les types, grâce aux modèles.

120
In silico

Je voulais suivre la réponse de @ Insilico avec un peu de mes propres trucs.

Avant de tomber sur cette réponse, j'essayais également de trouver des rappels rapides qui n'entraînaient pas de frais généraux et étaient uniquement comparables/identifiés par la signature de fonction uniquement. Ce que j'ai fini par créer - avec une aide sérieuse de Klingons Who Happened To Be at a BBQ - fonctionne pour tous les types de fonctions (sauf Lambdas, sauf si vous stockez le Lambda, mais ne l'essayez pas parce qu'il est vraiment difficile et difficile à faire et peut entraîner un robot qui vous prouve à quel point c'est difficile et vous fait manger la merde pour ça ). Merci à @sehe, @nixeagle, @StackedCrooked, @CatPlusPlus, @Xeo, @DeadMG et bien sûr @Insilico pour l'aide à la création du système d'événements. N'hésitez pas à l'utiliser comme vous le souhaitez.

Quoi qu'il en soit, un exemple est en place sur ideone, mais le code source est également à votre disposition (car, depuis que Liveworkspace est tombé en panne, je ne leur fais pas confiance pour les services de compilation louches. Qui sait quand ideone va tomber?!). J'espère que cela sera utile pour quelqu'un qui n'est pas occupé à Lambda/Fonction-objecter le monde en morceaux:

REMARQUE IMPORTANTE: à partir de maintenant (28/11/2012, 9:35 PM) Cette version variadic ne fonctionnera pas avec le Microsoft VC++ 2012 November CTP (Milan). Si vous souhaitez l'utiliser avec cela, vous devrez vous débarrasser de toutes les choses variadiques et énumérer explicitement le nombre d'arguments (et éventuellement spécialiser le modèle à 1 argument pour Event pour void) pour le faire fonctionner. C'est une douleur, et je n'ai réussi à l'écrire que pour 4 arguments avant de me fatiguer (et j'ai décidé que passer plus de 4 arguments était un peu exagéré).

Exemple source

La source:

#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;
        }
};
10
user1357649