web-dev-qa-db-fra.com

Fonctions de rappel en C ++

En C++, quand et comment utilisez-vous une fonction de rappel?

EDIT:
J'aimerais voir un exemple simple pour écrire une fonction de rappel.

271
cpx

Note: la plupart des réponses couvrent les pointeurs de fonction, ce qui est une possibilité pour réaliser une logique de "rappel" en C++, mais à ce jour pas la plus favorable, je pense.

Que sont les callbacks (?) Et pourquoi les utiliser (!)

Un rappel est un callable (voir plus bas) accepté par une classe ou une fonction, utilisé pour personnaliser la logique en cours en fonction de ce rappel.

Une des raisons d'utiliser des rappels est d'écrire un code générique indépendant de la logique de la fonction appelée et pouvant être réutilisé avec des rappels différents.

De nombreuses fonctions de la bibliothèque d'algorithmes standard <algorithm> utilisent des rappels. Par exemple, l'algorithme for_each applique un rappel unaire à chaque élément d'une plage d'itérateurs:

template<class InputIt, class UnaryFunction>
UnaryFunction for_each(InputIt first, InputIt last, UnaryFunction f)
{
  for (; first != last; ++first) {
    f(*first);
  }
  return f;
}

qui peut être utilisé pour d'abord incrémenter puis imprimer un vecteur en transmettant les callables appropriés, par exemple:

std::vector<double> v{ 1.0, 2.2, 4.0, 5.5, 7.2 };
double r = 4.0;
std::for_each(v.begin(), v.end(), [&](double & v) { v += r; });
std::for_each(v.begin(), v.end(), [](double v) { std::cout << v << " "; });

qui imprime

5 6.2 8 9.5 11.2

Une autre application des rappels est la notification des appelants de certains événements, ce qui permet une certaine flexibilité statique/temps de compilation.

Personnellement, j'utilise une bibliothèque d'optimisation locale qui utilise deux rappels différents:

  • Le premier rappel est appelé si une valeur de fonction et le gradient basé sur un vecteur de valeurs d'entrée sont requis (rappel logique: détermination de la valeur de la fonction/dérivation du gradient).
  • Le second rappel est appelé une fois pour chaque étape de l'algorithme et reçoit certaines informations sur la convergence de l'algorithme (rappel de notification).

Ainsi, le concepteur de la bibliothèque n’est pas chargé de décider de ce qui se passe avec les informations fournies au programmeur via le rappel de notification et il n’a pas à s’inquiéter de la façon de déterminer les valeurs des fonctions car elles sont fournies par le rappel logique. Bien faire les choses est une tâche qui incombe à l’utilisateur de la bibliothèque et maintient la bibliothèque mince et plus générique.

De plus, les callbacks peuvent activer un comportement d'exécution dynamique.

Imaginez une sorte de classe de moteur de jeu dotée d'une fonction activée, chaque fois que l'utilisateur appuie sur un bouton de son clavier et sur un ensemble de fonctions permettant de contrôler votre comportement de jeu. Avec les rappels, vous pouvez (re) décider au moment de l'exécution quelles actions seront entreprises.

void player_jump();
void player_crouch();

class game_core
{
    std::array<void(*)(), total_num_keys> actions;
    // 
    void key_pressed(unsigned key_id)
    {
        if(actions[key_id]) actions[key_id]();
    }
    // update keybind from menu
    void update_keybind(unsigned key_id, void(*new_action)())
    {
        actions[key_id] = new_action;
    }
};

Ici, la fonction key_pressed utilise les rappels enregistrés dans actions pour obtenir le comportement souhaité lorsqu’une certaine touche est enfoncée. Si le joueur choisit de changer le bouton pour sauter, le moteur peut appeler

game_core_instance.update_keybind(newly_selected_key, &player_jump);

et changez ainsi le comportement d'un appel à key_pressed (que les appels player_jump) une fois que ce bouton est enfoncé la prochaine fois en jeu.

Que sont callables en C++ (11)?

Voir concepts C++: appelable sur cppreference pour une description plus formelle.

La fonctionnalité de rappel peut être réalisée de différentes manières en C++ (11) car plusieurs choses se révèlent être appelables * :

  • Pointeurs de fonction (y compris les pointeurs vers les fonctions membres)
  • std::function objets
  • Expressions lambda
  • Relier les expressions
  • Objets fonction (classes avec l'opérateur d'appel de fonction surchargé operator())

* Remarque: les pointeurs vers les membres de données sont également appelables, mais aucune fonction n'est appelée.

Plusieurs manières d'écrire importantes callbacks en détail

  • X.1 "Écrire" un rappel dans cette publication signifie la syntaxe permettant de déclarer et de nommer le type de rappel.
  • X.2 "Appeler" un rappel fait référence à la syntaxe pour appeler ces objets.
  • X.3 "Utiliser" un rappel signifie la syntaxe utilisée lors du passage d'arguments à une fonction à l'aide d'un rappel.

Remarque: à partir de C++ 17, un appel comme f(...) peut être écrit ainsi: std::invoke(f, ...), qui gère également le pointeur sur la casse d'un membre.

1. Pointeurs de fonction

Un pointeur de fonction est le type le plus simple (en termes de généralité; en termes de lisibilité, sans doute le pire) qu'un callback puisse avoir.

Ayons une fonction simple foo:

int foo (int x) { return 2+x; }

1.1 Écrire une notation de type pointeur/fonction

Un type de pointeur de fonction a la notation

return_type (*)(parameter_type_1, parameter_type_2, parameter_type_3)
// i.e. a pointer to foo has the type:
int (*)(int)

où un type nommé fonction pointeur ressemblera à

return_type (* name) (parameter_type_1, parameter_type_2, parameter_type_3)

// i.e. f_int_t is a type: function pointer taking one int argument, returning int
typedef int (*f_int_t) (int); 

// foo_p is a pointer to function taking int returning int
// initialized by pointer to function foo taking int returning int
int (* foo_p)(int) = &foo; 
// can alternatively be written as 
f_int_t foo_p = &foo;

La déclaration using nous donne la possibilité de rendre les choses un peu plus lisibles, puisque la typedef de f_int_t peut également être écrite ainsi:

using f_int_t = int(*)(int);

Où (du moins pour moi), il est plus clair que f_int_t est le nouveau alias de type et que la reconnaissance du type de pointeur de fonction est également plus facile

Et une déclaration d'une fonction à l'aide d'un rappel de type pointeur de fonction sera:

// foobar having a callback argument named moo of type 
// pointer to function returning int taking int as its argument
int foobar (int x, int (*moo)(int));
// if f_int is the function pointer typedef from above we can also write foobar as:
int foobar (int x, f_int_t moo);

1.2 Notation d'appel de rappel

La notation d'appel suit la syntaxe d'appel de fonction simple:

int foobar (int x, int (*moo)(int))
{
    return x + moo(x); // function pointer moo called using argument x
}
// analog
int foobar (int x, f_int_t moo)
{
    return x + moo(x); // function pointer moo called using argument x
}

1.3 Notation de rappel et types compatibles

Une fonction de rappel prenant un pointeur de fonction peut être appelée à l'aide de pointeurs de fonction.

Utiliser une fonction qui prend un rappel de pointeur de fonction est assez simple:

 int a = 5;
 int b = foobar(a, foo); // call foobar with pointer to foo as callback
 // can also be
 int b = foobar(a, &foo); // call foobar with pointer to foo as callback

1.4 Exemple

Une fonction peut être écrite qui ne dépend pas du fonctionnement du rappel:

void tranform_every_int(int * v, unsigned n, int (*fp)(int))
{
  for (unsigned i = 0; i < n; ++i)
  {
    v[i] = fp(v[i]);
  }
}

si possible, les rappels pourraient être

int double_int(int x) { return 2*x; }
int square_int(int x) { return x*x; }

utilisé comme

int a[5] = {1, 2, 3, 4, 5};
tranform_every_int(&a[0], 5, double_int);
// now a == {2, 4, 6, 8, 10};
tranform_every_int(&a[0], 5, square_int);
// now a == {4, 16, 36, 64, 100};

2. Pointeur sur la fonction membre

Un pointeur sur une fonction membre (de la classe C) est un type particulier de pointeur de fonction (et même plus complexe) qui nécessite un objet de type C.

struct C
{
    int y;
    int foo(int x) const { return x+y; }
};

2.1 Écrire un pointeur sur la notation de fonction/type de membre

Un pointeur vers le type de fonction membre pour une classe T a la notation

// can have more or less parameters
return_type (T::*)(parameter_type_1, parameter_type_2, parameter_type_3)
// i.e. a pointer to C::foo has the type
int (C::*) (int)

où un nommé pointeur vers une fonction membre ressemblera, de manière analogue au pointeur de la fonction, à ceci:

return_type (T::* name) (parameter_type_1, parameter_type_2, parameter_type_3)

// i.e. a type `f_C_int` representing a pointer to member function of `C`
// taking int returning int is:
typedef int (C::* f_C_int_t) (int x); 

// The type of C_foo_p is a pointer to member function of C taking int returning int
// Its value is initialized by a pointer to foo of C
int (C::* C_foo_p)(int) = &C::foo;
// which can also be written using the typedef:
f_C_int_t C_foo_p = &C::foo;

Exemple: déclaration d'une fonction prenant un pointeur vers un rappel de fonction membre comme un de ses arguments:

// C_foobar having an argument named moo of type pointer to member function of C
// where the callback returns int taking int as its argument
// also needs an object of type c
int C_foobar (int x, C const &c, int (C::*moo)(int));
// can equivalently declared using the typedef above:
int C_foobar (int x, C const &c, f_C_int_t moo);

2.2 Notation d'appel de rappel

Le pointeur sur la fonction membre de C peut être appelé, en ce qui concerne un objet de type C, à l'aide d'opérations d'accès de membre sur le pointeur déréférencé. Note: parenthèse obligatoire!

int C_foobar (int x, C const &c, int (C::*moo)(int))
{
    return x + (c.*moo)(x); // function pointer moo called for object c using argument x
}
// analog
int C_foobar (int x, C const &c, f_C_int_t moo)
{
    return x + (c.*moo)(x); // function pointer moo called for object c using argument x
}

Remarque: si un pointeur sur C est disponible, la syntaxe est équivalente (le pointeur sur C doit également être déréférencé):

int C_foobar_2 (int x, C const * c, int (C::*meow)(int))
{
    if (!c) return x;
    // function pointer meow called for object *c using argument x
    return x + ((*c).*meow)(x); 
}
// or equivalent:
int C_foobar_2 (int x, C const * c, int (C::*meow)(int))
{
    if (!c) return x;
    // function pointer meow called for object *c using argument x
    return x + (c->*meow)(x); 
}

2.3 Notation d'utilisation de rappel et types compatibles

Une fonction de rappel prenant un pointeur de fonction membre de classe T peut être appelée à l'aide d'un pointeur de fonction membre de classe T.

Utiliser une fonction qui prend un pointeur sur le rappel de fonction membre est -en analogie avec les pointeurs de fonction- assez simple également:

 C my_c{2}; // aggregate initialization
 int a = 5;
 int b = C_foobar(a, my_c, &C::foo); // call C_foobar with pointer to foo as its callback

3. std::function objets (en-tête <functional>)

La classe std::function est une enveloppe polymorphe pour stocker, copier ou appeler des callables.

3.1 Écrire une notation std::function objet/type

Le type d'un objet std::function stockant un appelable se présente comme suit:

std::function<return_type(parameter_type_1, parameter_type_2, parameter_type_3)>

// i.e. using the above function declaration of foo:
std::function<int(int)> stdf_foo = &foo;
// or C::foo:
std::function<int(const C&, int)> stdf_C_foo = &C::foo;

3.2 Notation d'appel de rappel

La classe std::function a operator() définie qui peut être utilisée pour appeler sa cible.

int stdf_foobar (int x, std::function<int(int)> moo)
{
    return x + moo(x); // std::function moo called
}
// or 
int stdf_C_foobar (int x, C const &c, std::function<int(C const &, int)> moo)
{
    return x + moo(c, x); // std::function moo called using c and x
}

3.3 Notation d'utilisation de rappel et types compatibles

Le rappel std::function est plus générique que les pointeurs de fonction ou le pointeur sur une fonction membre, car différents types peuvent être passés et convertis implicitement en un objet std::function.

3.3.1 Pointeurs de fonction et pointeurs vers les fonctions membres

Un pointeur de fonction

int a = 2;
int b = stdf_foobar(a, &foo);
// b == 6 ( 2 + (2+2) )

ou un pointeur sur la fonction membre

int a = 2;
C my_c{7}; // aggregate initialization
int b = stdf_C_foobar(a, c, &C::foo);
// b == 11 == ( 2 + (7+2) )

peut être utilisé.

3.3.2 Expressions Lambda

Une fermeture non nommée d'une expression lambda peut être stockée dans un objet std::function:

int a = 2;
int c = 3;
int b = stdf_foobar(a, [c](int x) -> int { return 7+c*x; });
// b == 15 ==  a + (7*c*a) == 2 + (7+3*2)

3.3.3 std::bind expressions

Le résultat d'une expression std::bind peut être transmis. Par exemple, en liant des paramètres à un appel de pointeur de fonction:

int foo_2 (int x, int y) { return 9*x + y; }
using std::placeholders::_1;

int a = 2;
int b = stdf_foobar(a, std::bind(foo_2, _1, 3));
// b == 23 == 2 + ( 9*2 + 3 )
int c = stdf_foobar(a, std::bind(foo_2, 5, _1));
// c == 49 == 2 + ( 9*5 + 2 )

Où également les objets peuvent être liés en tant qu'objet pour l'appel du pointeur sur les fonctions membres:

int a = 2;
C const my_c{7}; // aggregate initialization
int b = stdf_foobar(a, std::bind(&C::foo, my_c, _1));
// b == 1 == 2 + ( 2 + 7 )

3.3.4 Objets de fonction

Les objets de classes ayant une surcharge operator() appropriée peuvent également être stockés dans un objet std::function.

struct Meow
{
  int y = 0;
  Meow(int y_) : y(y_) {}
  int operator()(int x) { return y * x; }
};
int a = 11;
int b = stdf_foobar(a, Meow{8});
// b == 99 == 11 + ( 8 * 11 )

3.4 Exemple

Modification de l'exemple de pointeur de fonction pour utiliser std::function

void stdf_tranform_every_int(int * v, unsigned n, std::function<int(int)> fp)
{
  for (unsigned i = 0; i < n; ++i)
  {
    v[i] = fp(v[i]);
  }
}

donne beaucoup plus d’utilité à cette fonction car (voir 3.3) nous avons plus de possibilités pour l’utiliser:

// using function pointer still possible
int a[5] = {1, 2, 3, 4, 5};
stdf_tranform_every_int(&a[0], 5, double_int);
// now a == {2, 4, 6, 8, 10};

// use it without having to write another function by using a lambda
stdf_tranform_every_int(&a[0], 5, [](int x) -> int { return x/2; });
// now a == {1, 2, 3, 4, 5}; again

// use std::bind :
int nine_x_and_y (int x, int y) { return 9*x + y; }
using std::placeholders::_1;
// calls nine_x_and_y for every int in a with y being 4 every time
stdf_tranform_every_int(&a[0], 5, std::bind(nine_x_and_y, _1, 4));
// now a == {13, 22, 31, 40, 49};

4. Type de rappel basé sur un modèle

À l'aide de modèles, le code appelant le rappel peut être encore plus général que d'utiliser des objets std::function.

Notez que les templates sont une fonctionnalité au moment de la compilation et sont un outil de conception pour le polymorphisme au moment de la compilation. Si le comportement dynamique de l'exécution doit être obtenu via des rappels, les modèles aideront, mais ils n'induiront pas de dynamique d'exécution.

4.1 Écrire (notations de type) et appeler des rappels basés sur un modèle

La généralisation du code std_ftransform_every_int ci-dessus peut encore être réalisée en utilisant des modèles:

template<class R, class T>
void stdf_transform_every_int_templ(int * v,
  unsigned const n, std::function<R(T)> fp)
{
  for (unsigned i = 0; i < n; ++i)
  {
    v[i] = fp(v[i]);
  }
}

avec une syntaxe encore plus générale (ainsi que la plus simple) pour un type de rappel étant un argument basé sur un modèle, simple à déduire:

template<class F>
void transform_every_int_templ(int * v, 
  unsigned const n, F f)
{
  std::cout << "transform_every_int_templ<" 
    << type_name<F>() << ">\n";
  for (unsigned i = 0; i < n; ++i)
  {
    v[i] = f(v[i]);
  }
}

Remarque: La sortie incluse imprime le nom du type déduit pour le type basé sur un modèle F. L'implémentation de type_name est donnée à la fin de cet article.

L'implémentation la plus générale pour la transformation unaire d'une plage fait partie de la bibliothèque standard, à savoir std::transform, qui est également gabarit par rapport aux types itérés.

template<class InputIt, class OutputIt, class UnaryOperation>
OutputIt transform(InputIt first1, InputIt last1, OutputIt d_first,
  UnaryOperation unary_op)
{
  while (first1 != last1) {
    *d_first++ = unary_op(*first1++);
  }
  return d_first;
}

4.2 Exemples utilisant des rappels basés sur un modèle et des types compatibles

Les types compatibles pour la méthode de rappel std::function basée sur un modèle stdf_transform_every_int_templ sont identiques aux types mentionnés ci-dessus (voir 3.4).

Cependant, en utilisant la version basée sur un modèle, la signature du rappel utilisé peut changer un peu:

// Let
int foo (int x) { return 2+x; }
int muh (int const &x) { return 3+x; }
int & woof (int &x) { x *= 4; return x; }

int a[5] = {1, 2, 3, 4, 5};
stdf_transform_every_int_templ<int,int>(&a[0], 5, &foo);
// a == {3, 4, 5, 6, 7}
stdf_transform_every_int_templ<int, int const &>(&a[0], 5, &muh);
// a == {6, 7, 8, 9, 10}
stdf_transform_every_int_templ<int, int &>(&a[0], 5, &woof);

Remarque: std_ftransform_every_int (version non basée sur un modèle; voir ci-dessus) fonctionne avec foo mais n'utilise pas muh.

// Let
void print_int(int * p, unsigned const n)
{
  bool f{ true };
  for (unsigned i = 0; i < n; ++i)
  {
    std::cout << (f ? "" : " ") << p[i]; 
    f = false;
  }
  std::cout << "\n";
}

Le paramètre basé sur un modèle de transform_every_int_templ peut être constitué de tous les types appelables possibles.

int a[5] = { 1, 2, 3, 4, 5 };
print_int(a, 5);
transform_every_int_templ(&a[0], 5, foo);
print_int(a, 5);
transform_every_int_templ(&a[0], 5, muh);
print_int(a, 5);
transform_every_int_templ(&a[0], 5, woof);
print_int(a, 5);
transform_every_int_templ(&a[0], 5, [](int x) -> int { return x + x + x; });
print_int(a, 5);
transform_every_int_templ(&a[0], 5, Meow{ 4 });
print_int(a, 5);
using std::placeholders::_1;
transform_every_int_templ(&a[0], 5, std::bind(foo_2, _1, 3));
print_int(a, 5);
transform_every_int_templ(&a[0], 5, std::function<int(int)>{&foo});
print_int(a, 5);

Le code ci-dessus est imprimé:

1 2 3 4 5
transform_every_int_templ <int(*)(int)>
3 4 5 6 7
transform_every_int_templ <int(*)(int&)>
6 8 10 12 14
transform_every_int_templ <int& (*)(int&)>
9 11 13 15 17
transform_every_int_templ <main::{lambda(int)#1} >
27 33 39 45 51
transform_every_int_templ <Meow>
108 132 156 180 204
transform_every_int_templ <std::_Bind<int(*(std::_Placeholder<1>, int))(int, int)>>
975 1191 1407 1623 1839
transform_every_int_templ <std::function<int(int)>>
977 1193 1409 1625 1841

type_name implémentation utilisée ci-dessus

#include <type_traits>
#include <typeinfo>
#include <string>
#include <memory>
#include <cxxabi.h>

template <class T>
std::string type_name()
{
  typedef typename std::remove_reference<T>::type TR;
  std::unique_ptr<char, void(*)(void*)> own
    (abi::__cxa_demangle(typeid(TR).name(), nullptr,
    nullptr, nullptr), std::free);
  std::string r = own != nullptr?own.get():typeid(TR).name();
  if (std::is_const<TR>::value)
    r += " const";
  if (std::is_volatile<TR>::value)
    r += " volatile";
  if (std::is_lvalue_reference<T>::value)
    r += " &";
  else if (std::is_rvalue_reference<T>::value)
    r += " &&";
  return r;
}
380
Pixelchemist

Il y a aussi la façon de faire des callbacks en C: les pointeurs de fonction

//Define a type for the callback signature,
//it is not necessary, but makes life easier

//Function pointer called CallbackType that takes a float
//and returns an int
typedef int (*CallbackType)(float);  


void DoWork(CallbackType callback)
{
  float variable = 0.0f;

  //Do calculations

  //Call the callback with the variable, and retrieve the
  //result
  int result = callback(variable);

  //Do something with the result
}

int SomeCallback(float variable)
{
  int result;

  //Interpret variable

  return result;
}

int main(int argc, char ** argv)
{
  //Pass in SomeCallback to the DoWork
  DoWork(&SomeCallback);
}

Maintenant, si vous voulez transmettre des méthodes de classe en tant que rappels, les déclarations de ces pointeurs de fonction ont des déclarations plus complexes, par exemple:

//Declaration:
typedef int (ClassName::*CallbackType)(float);

//This method performs work using an object instance
void DoWorkObject(CallbackType callback)
{
  //Class instance to invoke it through
  ClassName objectInstance;

  //Invocation
  int result = (objectInstance.*callback)(1.0f);
}

//This method performs work using an object pointer
void DoWorkPointer(CallbackType callback)
{
  //Class pointer to invoke it through
  ClassName * pointerInstance;

  //Invocation
  int result = (pointerInstance->*callback)(1.0f);
}

int main(int argc, char ** argv)
{
  //Pass in SomeCallback to the DoWork
  DoWorkObject(&ClassName::Method);
  DoWorkPointer(&ClassName::Method);
}
153
Ramon Zarazua B.

Scott Meyers donne un bel exemple:

class GameCharacter;
int defaultHealthCalc(const GameCharacter& gc);

class GameCharacter
{
public:
  typedef std::function<int (const GameCharacter&)> HealthCalcFunc;

  explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)
  : healthFunc(hcf)
  { }

  int healthValue() const { return healthFunc(*this); }

private:
  HealthCalcFunc healthFunc;
};

Je pense que l'exemple dit tout.

std::function<> est la manière "moderne" d'écrire des rappels C++.

67
Karl von Moor

Une fonction de rappel est une méthode transmise à une routine et appelée à un moment donné par la routine à laquelle elle est transmise.

Ceci est très utile pour créer des logiciels réutilisables. Par exemple, de nombreuses API de système d'exploitation (telles que l'API Windows) utilisent énormément les rappels.

Par exemple, si vous souhaitez utiliser des fichiers dans un dossier, vous pouvez appeler une fonction API avec votre propre routine et celle-ci sera exécutée une fois par fichier dans le dossier spécifié. Cela permet à l'API d'être très flexible.

38
Reed Copsey

La réponse acceptée est très utile et assez complète. Cependant, le PO indique

Je voudrais voir un exemple simple pour écrire une fonction de rappel.

Alors voilà, à partir de C++ 11, vous avez std::function donc il n’est pas nécessaire de disposer de pointeurs de fonctions ni de choses similaires:

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

void print_hashes(std::function<int (const std::string&)> hash_calculator) {
    std::string strings_to_hash[] = {"you", "saved", "my", "day"};
    for(auto s : strings_to_hash)
        std::cout << s << ":" << hash_calculator(s) << std::endl;    
}

int main() {
    print_hashes( [](const std::string& str) {   /** lambda expression */
        int result = 0;
        for (int i = 0; i < str.length(); i++)
            result += pow(31, i) * str.at(i);
        return result;
    });
    return 0;
}

Cet exemple est d'ailleurs assez réel, car vous souhaitez appeler la fonction print_hashes avec différentes implémentations de fonctions de hachage. À cet effet, j’en ai fourni un simple. Il reçoit une chaîne, retourne un int (une valeur de hachage de la chaîne fournie), et tout ce que vous devez retenir de la partie syntaxique est std::function<int (const std::string&)> qui décrit cette fonction comme un argument d'entrée de la fonction qui l'invoquera. .

13
Miljen Mikic

Il n'y a pas de concept explicite d'une fonction de rappel en C++. Les mécanismes de rappel sont souvent implémentés via des pointeurs de fonction, des objets foncteur ou des objets de rappel. Les programmeurs doivent explicitement concevoir et implémenter la fonctionnalité de rappel.

Modifier basé sur les commentaires:

Malgré les réactions négatives que cette réponse a reçues, ce n’est pas faux. Je vais essayer de mieux expliquer d'où je viens.

C et C++ ont tout ce dont vous avez besoin pour implémenter des fonctions de rappel. Le moyen le plus courant et le plus simple d'implémenter une fonction de rappel consiste à passer un pointeur de fonction en tant qu'argument de fonction.

Cependant, les fonctions de rappel et les pointeurs de fonction ne sont pas synonymes. Un pointeur de fonction est un mécanisme de langage, alors qu'une fonction de rappel est un concept sémantique. Les pointeurs de fonction ne sont pas le seul moyen d'implémenter une fonction de rappel - vous pouvez également utiliser des foncteurs et même des fonctions virtuelles de type jardin. Ce qui appelle un rappel de fonction n’est pas le mécanisme utilisé pour identifier et appeler la fonction, mais le contexte et la sémantique de l’appel. Dire quelque chose est une fonction de rappel implique une séparation supérieure à la normale entre la fonction appelante et la fonction spécifique appelée, un couplage conceptuel plus lâche entre l'appelant et l'appelé, l'appelant ayant un contrôle explicite sur ce qui est appelé. C'est cette notion floue de couplage conceptuel plus lâche et de sélection de fonction pilotée par l'appelant qui fait de quelque chose une fonction de rappel, et non l'utilisation d'un pointeur de fonction.

Par exemple, la documentation .NET pour IFormatProvider indique que "GetFormat est une méthode de rappel", même s'il ne s'agit que d'une méthode d'interface standard. Je pense que personne ne dirait que tous les appels de méthodes virtuelles sont des fonctions de rappel. Ce qui fait de GetFormat une méthode de rappel n’est pas la mécanique de la façon dont elle est transmise ou invoquée, mais la sémantique de l’appelant qui sélectionne la méthode GetFormat de l’objet qui sera appelée.

Certaines langues incluent des fonctionnalités avec une sémantique de rappel explicite, généralement liées aux événements et à la gestion des événements. Par exemple, C # a le type événement, avec une syntaxe et une sémantique explicitement conçues autour du concept de rappel. Visual Basic a sa clause Handles, qui déclare explicitement qu'une méthode est une fonction de rappel tout en faisant abstraction du concept de délégué ou de pointeur de fonction. Dans ces cas, le concept sémantique de rappel est intégré au langage lui-même.

C et C++, d’autre part, n’incorporent pas le concept sémantique des fonctions de rappel presque aussi explicitement. Les mécanismes sont là, pas la sémantique intégrée. Vous pouvez très bien implémenter des fonctions de rappel, mais pour obtenir quelque chose de plus sophistiqué incluant une sémantique explicite, vous devez le construire au-dessus de ce que C++ fournit, comme ce que Qt a fait avec Signals and Slots .

En un mot, C++ a ce dont vous avez besoin pour mettre en oeuvre des rappels, souvent assez facilement et de manière triviale, à l’aide de pointeurs de fonction. Ce qu'il n'a pas, ce sont des mots-clés et des fonctionnalités dont la sémantique est spécifique aux callbacks, tels que raise, emit, Handles, event + =, etc. Si vous venez d'une langue avec ces types d'éléments, la prise en charge native des rappels en C++ vous semblera stérilisée.

8
Darryl

Les fonctions de rappel font partie du standard C, donc du C++. Mais si vous travaillez avec C++, je vous suggérerais d'utiliser plutôt le modèle d'observateur : http://en.wikipedia.org/ wiki/Observer_pattern

6
AudioDroid

Voir la définition ci-dessus où il est indiqué qu'une fonction de rappel est transmise à une autre fonction et appelée à un moment donné.

En C++, il est souhaitable que les fonctions de rappel appellent une méthode de classes. Lorsque vous faites cela, vous avez accès aux données du membre. Si vous utilisez la méthode C pour définir un rappel, vous devrez l'indiquer à une fonction membre statique. Ce n'est pas très souhaitable.

Voici comment utiliser les rappels en C++. Supposons 4 fichiers. Une paire de fichiers .CPP/.H pour chaque classe. La classe C1 est la classe avec une méthode que nous voulons rappeler. C2 rappelle la méthode de C1. Dans cet exemple, la fonction de rappel prend 1 paramètre que j'ai ajouté pour les lecteurs. L'exemple ne montre aucun objet instancié et utilisé. Un cas d'utilisation de cette implémentation est lorsque vous avez une classe qui lit et stocke des données dans un espace temporaire et une autre qui post-traite les données. Avec une fonction de rappel, le rappel peut être traité pour chaque ligne de données lue. Cette technique supprime les frais généraux de l'espace temporaire requis. Cela est particulièrement utile pour les requêtes SQL qui renvoient une grande quantité de données qui doit ensuite être post-traitée.

/////////////////////////////////////////////////////////////////////
// C1 H file

class C1
{
    public:
    C1() {};
    ~C1() {};
    void CALLBACK F1(int i);
};

/////////////////////////////////////////////////////////////////////
// C1 CPP file

void CALLBACK C1::F1(int i)
{
// Do stuff with C1, its methods and data, and even do stuff with the passed in parameter
}

/////////////////////////////////////////////////////////////////////
// C2 H File

class C1; // Forward declaration

class C2
{
    typedef void (CALLBACK C1::* pfnCallBack)(int i);
public:
    C2() {};
    ~C2() {};

    void Fn(C1 * pThat,pfnCallBack pFn);
};

/////////////////////////////////////////////////////////////////////
// C2 CPP File

void C2::Fn(C1 * pThat,pfnCallBack pFn)
{
    // Call a non-static method in C1
    int i = 1;
    (pThat->*pFn)(i);
}
4
Gravy Jones

Le Boost signaux2 vous permet de souscrire des fonctions membres génériques (sans modèles!) Et de manière thread-safe.

Exemple: les signaux de vue de document peuvent être utilisés pour mettre en œuvre des architectures souples de vue de document. Le document contiendra un signal auquel chacune des vues peut se connecter. La classe Document suivante définit un document texte simple prenant en charge plusieurs vues. Notez qu'il enregistre un seul signal auquel toutes les vues seront connectées.

class Document
{
public:
    typedef boost::signals2::signal<void ()>  signal_t;

public:
    Document()
    {}

    /* Connect a slot to the signal which will be emitted whenever
      text is appended to the document. */
    boost::signals2::connection connect(const signal_t::slot_type &subscriber)
    {
        return m_sig.connect(subscriber);
    }

    void append(const char* s)
    {
        m_text += s;
        m_sig();
    }

    const std::string& getText() const
    {
        return m_text;
    }

private:
    signal_t    m_sig;
    std::string m_text;
};

Ensuite, nous pouvons commencer à définir des vues. La classe TextView suivante fournit une vue simple du texte du document.

class TextView
{
public:
    TextView(Document& doc): m_document(doc)
    {
        m_connection = m_document.connect(boost::bind(&TextView::refresh, this));
    }

    ~TextView()
    {
        m_connection.disconnect();
    }

    void refresh() const
    {
        std::cout << "TextView: " << m_document.getText() << std::endl;
    }
private:
    Document&               m_document;
    boost::signals2::connection  m_connection;
};
0
crizCraig

J'ai un code que je l'ai écrit il y a longtemps. Je voulais traverser un arbre avec un chemin dans l'ordre (nœud gauche puis nœud racine puis nœud droit) et chaque fois que j'arrive à un Node je voulais pouvoir appeler une fonction arbitraire pour qu'elle pourrait tout faire.

void inorder_traversal(Node *p, void *out, void (*callback)(Node *in, void *out))
{
    if (p == NULL)
        return;
    inorder_traversal(p->left, out, callback);
    callback(p, out); // call callback function like this.
    inorder_traversal(p->right, out, callback);
}


// Function like bellow can be used in callback of inorder_traversal.
void foo(Node *t, void *out = NULL)
{
    // You can just leave the out variable and working with specific node of tree. like bellow.
    // cout << t->item;
    // Or
    // You can assign value to out variable like below
    // Mention that the type of out is void * so that you must firstly cast it to your proper out.
    *((int *)out) += 1;
}
// This function use inorder_travesal function to count the number of nodes existing in the tree.
void number_nodes(Node *t)
{
    int sum = 0;
    inorder_traversal(t, &sum, foo);
    cout << sum;
}

 int main()
{

    Node *root = NULL;
    // What These functions perform is inserting an integer into a Tree data-structure.
    root = insert_tree(root, 6);
    root = insert_tree(root, 3);
    root = insert_tree(root, 8);
    root = insert_tree(root, 7);
    root = insert_tree(root, 9);
    root = insert_tree(root, 10);
    number_nodes(root);
}
0
Ehsan Ahmadi