web-dev-qa-db-fra.com

Comment puis-je passer une fonction membre de classe en tant que rappel?

J'utilise une API qui nécessite que je transmette un pointeur de fonction en tant que rappel. J'essaie d'utiliser cette API de ma classe, mais des erreurs de compilation se produisent. 

Voici ce que j'ai fait de mon constructeur:

m_cRedundencyManager->Init(this->RedundencyManagerCallBack);

Cela ne compile pas - j'obtiens l'erreur suivante: 

Erreur 8 Erreur C3867: 'CLoggersInfra :: RedundencyManagerCallBack': Liste des arguments manquants dans l'appel de la fonction; utilisez '& CLoggersInfra :: RedundencyManagerCallBack' pour créer un pointeur sur un membre

J'ai essayé la suggestion d'utiliser &CLoggersInfra::RedundencyManagerCallBack - n'a pas fonctionné pour moi. 

Des suggestions/explication pour ceci??

J'utilise VS2008.

Merci!!

55
ofer

Comme vous venez de le montrer, votre fonction init accepte une fonction non membre. alors faites-le comme ça:

static void Callback(int other_arg, void * this_pointer) {
    CLoggersInfra * self = static_cast<CLoggersInfra*>(this_pointer);
    self->RedundencyManagerCallBack(other_arg);
}

et appelez Init avec

m_cRedundencyManager->Init(&CLoggersInfra::Callback, this);

Cela fonctionne car un pointeur de fonction sur une fonction membre statique n'est pas un pointeur de fonction membre et peut donc être traité comme un simple pointeur sur une fonction libre.

40

C'est une question simple mais la réponse est étonnamment complexe. La réponse courte est que vous pouvez faire ce que vous essayez de faire avec std :: bind1st ou boost :: bind. La réponse la plus longue est ci-dessous.

Le compilateur est correct pour vous suggérer d'utiliser & CLoggersInfra :: RedundencyManagerCallBack. Tout d'abord, si RedundencyManagerCallBack est une fonction membre, la fonction elle-même n'appartient à aucune instance particulière de la classe CLoggersInfra. Il appartient à la classe elle-même. Si vous avez déjà appelé une fonction de classe statique auparavant, vous avez peut-être remarqué que vous utilisiez la même syntaxe SomeClass :: SomeMemberFunction. Comme la fonction elle-même est «statique» dans le sens où elle appartient à la classe plutôt qu’à une instance particulière, vous utilisez la même syntaxe. Le '&' est nécessaire car techniquement, vous ne passez pas directement de fonctions - les fonctions ne sont pas de vrais objets en C++. Au lieu de cela, vous transmettez techniquement l'adresse de la mémoire pour la fonction, c'est-à-dire un pointeur indiquant où les instructions de la fonction commencent en mémoire. La conséquence est la même si, en réalité, vous passez une fonction en tant que paramètre.

Mais ce n'est que la moitié du problème dans ce cas. Comme je l'ai dit, RedundencyManagerCallBack, la fonction n'appartient à aucune instance particulière. Mais il semble que vous souhaitiez le passer en tant que rappel avec une instance particulière à l’esprit. Pour comprendre comment faire cela, vous devez comprendre ce que sont réellement les fonctions membres: des fonctions standard non définies dans une classe avec un paramètre caché supplémentaire.

Par exemple:

class A {
public:
    A() : data(0) {}
    void foo(int addToData) { this->data += addToData; }

    int data;
};

...

A an_a_object;
an_a_object.foo(5);
A::foo(&an_a_object, 5); // This is the same as the line above!
std::cout << an_a_object.data; // Prints 10!

Combien de paramètres prend A :: toto? Normalement, nous dirions 1. Mais sous le capot, foo prend vraiment 2. En regardant la définition de A :: foo, il a besoin d'une instance spécifique de A pour que le pointeur 'this' ait un sens (le compilateur a besoin de savoir quoi ' c'est). La manière dont vous spécifiez habituellement ce que vous voulez que cela soit consiste à utiliser la syntaxe MyObject.MyMemberFunction (). Mais il ne s'agit que du sucre syntaxique pour passer l'adresse de MyObject en tant que premier paramètre à MyMemberFunction. De même, lorsque nous déclarons des fonctions membres dans les définitions de classe, nous ne mettons pas «this» dans la liste de paramètres, mais il s’agit simplement d’un cadeau des concepteurs de langage pour sauvegarder la saisie. Au lieu de cela, vous devez spécifier qu'une fonction membre est statique pour pouvoir la désactiver en obtenant automatiquement le paramètre supplémentaire 'this'. Si le compilateur C++ traduisait l'exemple ci-dessus en code C (le compilateur C++ d'origine fonctionnait ainsi), il écrirait probablement quelque chose comme ceci:

struct A {
    int data;
};

void a_init(A* to_init)
{
    to_init->data = 0;
}

void a_foo(A* this, int addToData)
{ 
    this->data += addToData;
}

...

A an_a_object;
a_init(0); // Before constructor call was implicit
a_foo(&an_a_object, 5); // Used to be an_a_object.foo(5);

Pour revenir à votre exemple, il existe maintenant un problème évident. 'Init' veut un pointeur sur une fonction qui prend un paramètre. Mais & CLoggersInfra :: RedundencyManagerCallBack est un pointeur sur une fonction qui prend deux paramètres, son paramètre normal et le paramètre secret 'this'. Par conséquent, vous obtenez toujours une erreur de compilation (remarque: si vous avez déjà utilisé Python, ce genre de confusion est la raison pour laquelle un paramètre 'self' est requis pour toutes les fonctions membres).

La manière détaillée de gérer ceci est de créer un objet spécial qui contient un pointeur sur l'instance souhaitée et une fonction membre appelée quelque chose comme 'run' ou 'execute' (ou qui surcharge l'opérateur '()') qui prend les paramètres. pour la fonction membre, et appelle simplement la fonction membre avec ces paramètres sur l'instance stockée. Mais cela nécessiterait que vous changiez 'Init' pour prendre votre objet spécial plutôt qu'un pointeur de fonction brut, et il semble que Init soit le code de quelqu'un d'autre. Et faire une classe spéciale à chaque fois que ce problème se pose mènera à un gonflement du code.

Alors maintenant, enfin, la bonne solution, boost :: bind et boost :: function, la documentation pour chacun, vous pouvez trouver ici:

boost :: bind docs , boost :: fonction docs

boost :: bind vous permettra de prendre une fonction et un paramètre de cette fonction et de créer une nouvelle fonction dans laquelle ce paramètre est "verrouillé". Donc, si j'ai une fonction qui ajoute deux entiers, je peux utiliser boost :: bind pour créer une nouvelle fonction dans laquelle l'un des paramètres est verrouillé, ce qui signifie 5. Cette nouvelle fonction ne prend qu'un paramètre entier et ajoute toujours 5 spécifiquement. à cela. En utilisant cette technique, vous pouvez "verrouiller" le paramètre "this" masqué pour qu'il soit une instance de classe particulière et générer une nouvelle fonction ne prenant qu'un paramètre, comme vous le souhaitez (notez que le paramètre masqué est toujours le paramètre first paramètre, et les paramètres normaux viennent dans l’ordre après). Regardez la documentation boost :: bind pour des exemples, ils discutent même spécifiquement de son utilisation pour les fonctions membres. Techniquement, il existe une fonction standard appelée std :: bind1st que vous pouvez également utiliser, mais boost :: bind est plus général.Bien sûr, il n'y a qu'une capture de plus. boost :: bind fera une belle boost :: fonction pour vous, mais techniquement ce n'est pas un pointeur de fonction brute comme le souhaite probablement Init. Heureusement, boost fournit un moyen de convertir boost :: function en pointeurs bruts, comme décrit dans StackOverflow ici . La façon dont cela est mis en œuvre dépasse le cadre de cette réponse, même si elle est également intéressante.

Ne vous inquiétez pas si cela semble ridiculement difficile - votre question recoupe plusieurs coins plus sombres de C++, et boost :: bind est extrêmement utile une fois que vous l’apprenez.

Mise à jour C++ 11: Au lieu de boost :: bind, vous pouvez maintenant utiliser une fonction lambda qui capture 'this'. En gros, le compilateur génère la même chose pour vous.

C++11 update: Instead of boost::bind you can now use a lambda function that captures 'this'. This is basically having the compiler generate the same thing for you.

98
Joseph Garvin

Cette réponse est une réponse à un commentaire ci-dessus et ne fonctionne pas avec VisualStudio 2008 mais devrait être préférée avec les compilateurs plus récents.


En attendant, vous n’êtes plus obligé d’utiliser un pointeur vide et aucun boost supplémentaire n’est nécessaire puisque std::bind et std::function sont disponibles. Un avantage (par rapport aux pointeurs vides) est la sécurité du type puisque le type de retour et les arguments sont explicitement définis à l'aide de std::function:

// std::function<return_type(list of argument_type(s))>
void Init(std::function<void(void)> f);

Ensuite, vous pouvez créer le pointeur de fonction avec std::bind et le transmettre à Init:

auto cLoggersInfraInstance = CLoggersInfra();
auto callback = std::bind(&CLoggersInfra::RedundencyManagerCallBack, cLoggersInfraInstance);
Init(callback);

Exemple complet pour utiliser std::bind avec des membres, des membres statiques et des fonctions non membres:

#include <functional>
#include <iostream>
#include <string>

class RedundencyManager // incl. Typo ;-)
{
public:
    // std::function<return_type(list of argument_type(s))>
    std::string Init(std::function<std::string(void)> f) 
    {
        return f();
    }
};

class CLoggersInfra
{
private:
    std::string member = "Hello from non static member callback!";

public:
    static std::string RedundencyManagerCallBack()
    {
        return "Hello from static member callback!";
    }

    std::string NonStaticRedundencyManagerCallBack()
    {
        return member;
    }
};

std::string NonMemberCallBack()
{
    return "Hello from non member function!";
}

int main()
{
    auto instance = RedundencyManager();

    auto callback1 = std::bind(&NonMemberCallBack);
    std::cout << instance.Init(callback1) << "\n";

    // Similar to non member function.
    auto callback2 = std::bind(&CLoggersInfra::RedundencyManagerCallBack);
    std::cout << instance.Init(callback2) << "\n";

    // Class instance is passed to std::bind as second argument.
    // (heed that I call the constructor of CLoggersInfra)
    auto callback3 = std::bind(&CLoggersInfra::NonStaticRedundencyManagerCallBack,
                               CLoggersInfra()); 
    std::cout << instance.Init(callback3) << "\n";
}

Sortie possible:

Hello from non member function!
Hello from static member callback!
Hello from non static member callback!

De plus, en utilisant std::placeholders vous pouvez passer dynamiquement des arguments au rappel (par exemple, ceci active l’utilisation de return f("MyString"); dans Init si f a un paramètre de chaîne).

10
Roi Danton

Quel argument Init prend-il? Quel est le nouveau message d'erreur?

Les pointeurs de méthodes en C++ sont un peu difficiles à utiliser. Outre le pointeur de méthode lui-même, vous devez également fournir un pointeur d'instance (dans votre cas this). Peut-être que Init l'attend comme un argument séparé?

3
Konrad Rudolph

Est-ce que m_cRedundencyManager peut utiliser les fonctions membres? La plupart des callbacks sont configurés pour utiliser des fonctions standard ou des fonctions membres statiques. Jetez un coup d’œil à cette page à C++ FAQ Lite pour plus d’informations.

Mettre à jour: La déclaration de fonction que vous avez fournie montre que m_cRedundencyManager attend une fonction de la forme: void yourCallbackFunction(int, void *). Les fonctions membres sont donc inacceptables en tant que rappels dans ce cas. Une fonction membre statique peut travail, mais si cela est inacceptable dans votre cas, le code suivant fonctionnerait également. Notez qu'il utilise un casting pervers de void *.


// in your CLoggersInfra constructor:
m_cRedundencyManager->Init(myRedundencyManagerCallBackHandler, this);

// in your CLoggersInfra header:
void myRedundencyManagerCallBackHandler(int i, void * CLoggersInfraPtr);

// in your CLoggersInfra source file:
void myRedundencyManagerCallBackHandler(int i, void * CLoggersInfraPtr)
{
    ((CLoggersInfra *)CLoggersInfraPtr)->RedundencyManagerCallBack(i);
}
3
e.James

Un pointeur sur une fonction membre de la classe n’est pas identique à un pointeur sur une fonction. Un membre de la classe prend un argument supplémentaire implicite (le this pointeur) et utilise une convention d'appel différente.

Si votre API attend une fonction de rappel non membre, c'est ce que vous devez lui transmettre.

3
jalf

Je peux voir que l'init a le remplacement suivant: 

Init (CALLBACK_FUNC_EX callback_func, void * callback_parm)

où CALLBACK_FUNC_EX est typedef void (* CALLBACK_FUNC_EX) (int, void *);

1
ofer

Nécromancie.
Je pense que les réponses à ce jour sont un peu obscures.

Faisons un exemple:

Supposons que vous ayez un tableau de pixels (tableau de valeurs ARGB int8_t)

// A RGB image
int8_t* pixels = new int8_t[1024*768*4];

Maintenant, vous voulez générer un fichier PNG. Pour cela, vous appelez la fonction toJpeg

bool ok = toJpeg(writeByte, pixels, width, height);

où writeByte est une fonction de rappel

void writeByte(unsigned char oneByte)
{
    fputc(oneByte, output);
}

Le problème ici: FILE * output doit être une variable globale.
Très mauvais si vous êtes dans un environnement multithread (par exemple, un serveur http).

Il faut donc trouver un moyen de faire en sorte que la sortie soit une variable non globale tout en conservant la signature de rappel.

La solution immédiate qui nous vient à l’esprit est une fermeture que nous pouvons émuler en utilisant une classe avec une fonction membre.

class BadIdea {
private:
    FILE* m_stream;
public:
    BadIdea(FILE* stream)  {
        this->m_stream = stream;
    }

    void writeByte(unsigned char oneByte){
            fputc(oneByte, this->m_stream);
    }

};

Et ensuite

FILE *fp = fopen(filename, "wb");
BadIdea* foobar = new BadIdea(fp);

bool ok = TooJpeg::writeJpeg(foobar->writeByte, image, width, height);
delete foobar;
fflush(fp);
fclose(fp);

Cependant, contrairement aux attentes, cela ne fonctionne pas.

La raison en est que les fonctions membres C++ sont un peu implémentées comme les fonctions d'extension C #.

Donc vous avez

class/struct BadIdea
{
    FILE* m_stream;
}

et

static class BadIdeaExtensions
{
    public static writeByte(this BadIdea instance, unsigned char oneByte)
    {
         fputc(oneByte, instance->m_stream);
    }

}

Ainsi, lorsque vous souhaitez appeler writeByte, vous devez transmettre non seulement l'adresse de writeByte, mais également l'adresse de l'instance BadIdea.

Alors, quand vous avez un typedef pour la procédure writeByte, et que cela ressemble à ceci

typedef void (*WRITE_ONE_BYTE)(unsigned char);

Et vous avez une signature writeJpeg qui ressemble à ceci

bool writeJpeg(WRITE_ONE_BYTE output, uint8_t* pixels, uint32_t 
 width, uint32_t height))
    { ... }

il est fondamentalement impossible de passer une fonction membre à deux adresses à un pointeur de fonction à une adresse (sans modifier writeJpeg), et il n'y a aucun moyen de le contourner.

La meilleure chose à faire en C++ consiste à utiliser une fonction lambda:

FILE *fp = fopen(filename, "wb");
auto lambda = [fp](unsigned char oneByte) { fputc(oneByte, fp);  };
bool ok = TooJpeg::writeJpeg(lambda, image, width, height);

Cependant, lambda ne faisant rien de différent, il est nécessaire de modifier la signature de writeJpeg en passant une instance à une classe cachée (telle que la classe "BadIdea").

L’avantage de lambda par rapport à une classe manuelle est qu’il vous suffit de modifier un type

typedef void (*WRITE_ONE_BYTE)(unsigned char);

à

using WRITE_ONE_BYTE = std::function<void(unsigned char)>; 

Et alors vous pouvez laisser tout le reste intact.

Vous pouvez aussi utiliser std :: bind

auto f = std::bind(&BadIdea::writeByte, &foobar);

Mais cela, en coulisse, crée simplement une fonction lambda, qui nécessite également le changement de typedef.

Donc non, il n'y a aucun moyen de passer une fonction membre à une méthode qui nécessite un pointeur de fonction statique.

Mais les lambdas sont la solution de facilité, à condition que vous contrôliez la source.
Sinon, vous n'avez pas de chance.
Vous ne pouvez rien faire avec C++.

Note:
std :: function requiert #include <functional>

Cependant, puisque C++ vous permet également d'utiliser C, vous pouvez le faire avec libffcall in plain C, si vous voulez bien lier une dépendance.

Téléchargez libffcall à partir de GNU (au moins sur Ubuntu, n'utilisez pas le paquet fourni par la distribution - il est cassé), décompressez.

./configure
make
make install

gcc main.c -l:libffcall.a -o ma

principal c:

#include <callback.h>

// this is the closure function to be allocated 
void function (void* data, va_alist alist)
{
     int abc = va_arg_int(alist);

     printf("data: %08p\n", data); // hex 0x14 = 20
     printf("abc: %d\n", abc);

     // va_start_type(alist[, return_type]);
     // arg = va_arg_type(alist[, arg_type]);
     // va_return_type(alist[[, return_type], return_value]);

    // va_start_int(alist);
    // int r = 666;
    // va_return_int(alist, r);
}



int main(int argc, char* argv[])
{
    int in1 = 10;

    void * data = (void*) 20;
    void(*incrementer1)(int abc) = (void(*)()) alloc_callback(&function, data);
    // void(*incrementer1)() can have unlimited arguments, e.g. incrementer1(123,456);
    // void(*incrementer1)(int abc) starts to throw errors...
    incrementer1(123);
    // free_callback(callback);
    return EXIT_SUCCESS;
}

Et si vous utilisez CMake, ajoutez la bibliothèque de l'éditeur de liens après add_executable

add_library(libffcall STATIC IMPORTED)
set_target_properties(libffcall PROPERTIES
        IMPORTED_LOCATION /usr/local/lib/libffcall.a)
target_link_libraries(BitmapLion libffcall)

ou vous pouvez simplement relier dynamiquement libffcall

target_link_libraries(BitmapLion ffcall)

Remarque:
Vous pouvez éventuellement inclure les en-têtes et les bibliothèques libffcall ou créer un projet cmake avec le contenu de libffcall.

0
Stefan Steiger

Cette question et réponse du C++ FAQ Lite couvre votre question et les considérations liées à la réponse, je pense. Petit extrait de la page Web que j'ai liée:

Ne pas.

Parce qu'une fonction membre n'a pas de sens sans objet à invoquer vous ne pouvez pas le faire directement (si le système X Window était réécrit en C++, il transmettrait probablement des références aux objets environnants. . fonction requise et probablement beaucoup plus).

0
Onorio Catenacci