Fonctions de rappel en C ++

303

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

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

cpx
la source
[This] ( thispointer.com/… ) explique très bien les principes de base des fonctions de rappel et la compréhension du concept.
Anurag Singh

Réponses:

449

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

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

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

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

De nombreuses fonctions de la bibliothèque d'algorithmes standard <algorithm>utilisent des rappels. Par exemple, l' for_eachalgorithme 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 passant des 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é en temps statique / 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 valeur de fonction / dérivation de gradient).
  • Le deuxième 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 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 à se soucier de la façon de déterminer les valeurs de fonction, car elles sont fournies par le rappel logique. Faire bien ces choses est une tâche due à l'utilisateur de la bibliothèque et maintient la bibliothèque mince et plus générique.

En outre, les rappels peuvent activer le comportement d'exécution dynamique.

Imaginez une sorte de classe de moteur de jeu qui a une fonction qui est déclenchée, chaque fois que les utilisateurs appuient sur un bouton de son clavier et un ensemble de fonctions qui contrôlent votre comportement de jeu. Avec les rappels, vous pouvez (re) décider au moment de l'exécution quelle action sera prise.

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_pressedutilise les rappels stockés dans actionspour 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 ainsi changer le comportement d'un appel key_pressed(auquel les appels player_jump) une fois que ce bouton est enfoncé la prochaine fois dans le jeu.

Que sont les callables en C ++ (11)?

Voir Concepts C ++: Callable on cppreference pour une description plus formelle.

La fonctionnalité de rappel peut être réalisée de plusieurs façons en C ++ (11) car plusieurs choses différentes peuvent être appelées * :

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

* Remarque: le pointeur vers les membres de données peut également être appelé, mais aucune fonction n'est appelée du tout.

Plusieurs façons importantes d'écrire des rappels en détail

  • X.1 "Écrire" un rappel dans cet article signifie la syntaxe pour déclarer et 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 lors du passage d'arguments à une fonction utilisant un rappel.

Remarque: à partir de C ++ 17, un appel comme f(...)peut être écrit comme celui std::invoke(f, ...)qui gère également le pointeur vers la casse 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 rappel puisse avoir.

Ayons une fonction simple foo:

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

1.1 Écrire un pointeur de fonction / une notation de type

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 de pointeur de fonction nommé 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 usingdéclaration nous donne la possibilité de rendre les choses un peu plus lisibles, car le typedeffor f_int_tpeut également s'écrire:

using f_int_t = int(*)(int);

Où (au moins pour moi) il est plus clair qu'il f_int_ts'agit du nouvel alias de type et la reconnaissance du type de pointeur de fonction est également plus facile

Et une déclaration d'une fonction utilisant 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 d'utilisation du rappel et types compatibles

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

L'utilisation d'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]);
  }
}

où les rappels possibles 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 vers la fonction membre

Un pointeur sur une fonction membre (d'une classe C) est un type spécial de pointeur de fonction (et encore plus complexe) qui nécessite un objet de type Cpour fonctionner.

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

2.1 Écriture du pointeur sur la fonction membre / notation de type

Un pointeur sur le type de fonction membre d'une classe Ta 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 pointeur nommé vers la fonction membre ressemblera, par analogie au pointeur de la fonction:

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éclarer une fonction en prenant un pointeur sur le rappel de fonction membre comme l'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 vers la fonction membre de Cpeut être invoqué, par rapport à un objet de type Cen utilisant des opérations d'accès membre sur le pointeur déréférencé. Remarque: parenthèses requises!

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 vers Cest disponible, la syntaxe est équivalente (où le pointeur vers Cdoit é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 du rappel et types compatibles

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

Utiliser une fonction qui prend un pointeur vers un rappel de fonction membre est aussi, par analogie avec les pointeurs de fonction, assez simple:

 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::functionobjets (en-tête <functional>)

La std::functionclasse est un wrapper de fonction polymorphe pour stocker, copier ou appeler des callables.

3.1 Écrire une std::functionnotation objet / type

Le type d'un std::functionobjet stockant un appelable ressemble à:

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::functiona operator()défini ce qui peut être utilisé 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 du rappel et types compatibles

Le std::functionrappel est plus générique que les pointeurs de fonction ou le pointeur sur la fonction membre car différents types peuvent être passés et convertis implicitement en std::functionobjet.

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 sans nom d'une expression lambda peut être stockée dans un std::functionobjet:

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::bindexpressions

Le résultat d'une std::bindexpression 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'invocation du pointeur vers 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 fonction

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

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 à 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 de 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 l'utilisation d' std::functionobjets.

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

4.1 Écriture (notations de type) et appel de modèles de rappels

La généralisation, c'est-à-dire que le std_ftransform_every_intcode ci-dessus peut être encore réalisé 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 de 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 de type déduit pour le type basé sur un modèle F. La mise en œuvre de type_nameest donnée à la fin de ce post.

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 basée sur des modèles 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 des modèles et des types compatibles

Les types compatibles pour la std::functionméthode de rappel basée sur des modèles stdf_transform_every_int_templsont identiques aux types mentionnés ci-dessus (voir 3.4).

Cependant, en utilisant la version basée sur des modèles, 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 foomais ne l'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 modèle simple de transform_every_int_templpeut être tout type appelable possible.

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 imprime:

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;
}
Pixelchemist
la source
35
@BogeyJammer: Au cas où vous ne l'auriez pas remarqué: La réponse est en deux parties. 1. Une explication générale des "rappels" avec un petit exemple. 2. Une liste complète des différents callables et des façons d'écrire du code à l'aide de rappels. Vous êtes invités à ne pas fouiller dans les détails ou à lire la réponse entière, mais simplement parce que vous ne voulez pas une vue détaillée, ce n'est pas le cas que la réponse soit inefficace ou "copiée brutalement". Le sujet est "rappels c ++". Même si la partie 1 convient à OP, d'autres peuvent trouver la partie 2 utile. N'hésitez pas à signaler tout manque d'information ou critique constructive pour la première partie au lieu de -1.
Pixelchemist
1
La partie 1 n'est pas assez conviviale pour les débutants et assez claire. Je ne peux pas être plus constructif en disant qu'il n'a pas réussi à m'apprendre quelque chose. Et la partie 2, n'a pas été demandée, inondant la page et est hors de question même si vous prétendez qu'elle a de l'utilité malgré le fait qu'elle se trouve généralement dans la documentation dédiée où de telles informations détaillées sont recherchées en premier lieu. Je garde définitivement le downvote. Un vote unique représente une opinion personnelle, veuillez donc l'accepter et la respecter.
Bogey Jammer
24
@BogeyJammer Je ne suis pas nouveau en programmation mais je suis nouveau en "modern c ++". Cette réponse me donne le contexte exact dont j'ai besoin pour raisonner sur le rôle joué par les rappels en particulier dans c ++. L'OP n'a peut-être pas demandé plusieurs exemples, mais il est habituel sur SO, dans une quête sans fin d'éduquer un monde de fous, d'énumérer toutes les solutions possibles à une question. S'il se lit trop comme un livre, le seul conseil que je puisse offrir est de pratiquer un peu en lisant quelques-uns d'entre eux .
dcow
int b = foobar(a, foo); // call foobar with pointer to foo as callback, c'est une faute de frappe non? foodevrait être un pointeur pour que cela fonctionne AFAIK.
konoufo
@konoufo: [conv.func]du standard C ++ 11 dit: " Une valeur de type fonction T peut être convertie en une valeur de type" pointeur vers T. " Le résultat est un pointeur sur la fonction. "Il s'agit d'une conversion standard et en tant que telle se produit implicitement. On pourrait (bien sûr) utiliser le pointeur de fonction ici.
Pixelchemist
160

Il y a aussi la façon C de faire des rappels: 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 souhaitez passer 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);
}
Ramon Zarazua B.
la source
1
Il y a une erreur dans l'exemple de méthode de classe. L'invocation doit être: (instance. * Callback) (1.0f)
CarlJohnson
Merci d'avoir fait remarquer cela. Je vais ajouter les deux pour illustrer l'invocation via un objet et via un pointeur d'objet.
Ramon Zarazua B.
3
Cela présente l'inconvénient de std :: tr1: function en ce que le rappel est typé par classe; cela rend impossible l'utilisation de rappels de style C lorsque l'objet effectuant l'appel ne connaît pas la classe de l'objet à appeler.
bleater
Comment pourrais-je le faire sans typedefle type de rappel? Est-ce même possible?
Tomáš Zato - Rétablir Monica
1
Oui, vous pouvez. typedefest juste du sucre syntaxique pour le rendre plus lisible. Sans typedefla définition de DoWorkObject pour les pointeurs de fonction serait: void DoWorkObject(int (*callback)(float)). Pour les pointeurs membres:void DoWorkObject(int (ClassName::*callback)(float))
Ramon Zarazua B.
68

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 façon "moderne" d'écrire des rappels C ++.

Karl von Moor
la source
1
Par intérêt, dans quel livre SM donne-t-il cet exemple? Santé :)
sam-w
5
Je sais que c'est vieux, mais parce que j'ai presque commencé à le faire et que cela ne fonctionne pas sur ma configuration (mingw), si vous utilisez la version GCC <4.x, cette méthode n'est pas prise en charge. Certaines des dépendances que j'utilise ne se compileront pas sans beaucoup de travail dans la version gcc> = 4.0.1, donc je suis coincé avec l'utilisation de bons rappels à l'ancienne de style C, qui fonctionnent très bien.
OzBarry
38

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

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 fortement les rappels.

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

Reed Copsey
la source
63
Cette réponse ne donne vraiment rien au programmeur moyen. J'apprends le C ++ tout en étant familier avec de nombreux autres langages. Ce qu'est le rappel en général ne me concerne pas.
Tomáš Zato - Rétablir Monica
17

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

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

Alors voilà, à partir de C ++ 11, vous avez std::functiondonc pas besoin de pointeurs de fonction et d'autres 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 en quelque sorte réel, car vous souhaitez appeler une fonction print_hashesavec différentes implémentations de fonctions de hachage, à cet effet, j'en ai fourni une simple. Il reçoit une chaîne, renvoie un int (une valeur de hachage de la chaîne fournie), et tout ce dont vous devez vous souvenir dans la partie syntaxique est de std::function<int (const std::string&)>décrire cette fonction comme un argument d'entrée de la fonction qui l'invoquera.

Miljen Mikic
la source
parmi toutes ces réponses ci-dessus, celle-ci m'a fait comprendre ce que sont les rappels et comment les utiliser. Merci.
Mehar Charan Sahai
@MeharCharanSahai Heureux de l'entendre :) Vous êtes les bienvenus.
Miljen Mikic
9

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

Modifier en fonction des commentaires:

Malgré les commentaires négatifs reçus par cette réponse, 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. La manière la plus courante et la plus banale d'implémenter une fonction de rappel est de passer un pointeur de fonction comme 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, tandis 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 variété de jardin. Ce qui fait qu'un fonction appelle un rappel n'est pas le mécanisme utilisé pour identifier et appeler la fonction, mais le contexte et la sémantique de l'appel. Dire que 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é.

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 ne pense pas que quiconque dirait que tous les appels de méthode virtuelle 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 passée ou invoquée, mais la sémantique de l'appelant qui choisit la méthode GetFormat de l'objet qui sera appelée.

Certains langages 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 d' événement avec une syntaxe et une sémantique explicitement conçues autour du concept des rappels. 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és ou de pointeurs de fonction. Dans ces cas, le concept sémantique d'un rappel est intégré au langage lui-même.

C et C ++, d'autre part, n'intègre pas le concept sémantique des fonctions de rappel de manière aussi explicite. Les mécanismes sont là, la sémantique intégrée ne l'est pas. Vous pouvez très bien implémenter des fonctions de rappel, mais pour obtenir quelque chose de plus sophistiqué qui inclut une sémantique de rappel explicite, vous devez le construire en plus de ce que C ++ fournit, comme ce que Qt a fait avec leurs signaux et emplacements .

En un mot, C ++ a ce dont vous avez besoin pour implémenter des rappels, souvent assez facilement et de manière triviale en utilisant des 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 rappels, tels que lever , emit , Handles , event + = , etc. Si vous venez d'un langage avec ces types d'éléments, le support de rappel natif en C ++ se sentira castré.

Darryl
la source
1
heureusement ce n'était pas la première réponse que j'ai lue en visitant cette page, sinon j'aurais fait un rebond immédiat!
ubugnu
6

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

AudioDroid
la source
1
Les fonctions de rappel ne sont pas nécessairement synonymes d'exécuter une fonction via un pointeur de fonction passé en argument. Selon certaines définitions, le terme fonction de rappel porte la sémantique supplémentaire de notifier un autre code de quelque chose qui vient de se produire, ou c'est le moment où quelque chose devrait se produire. De ce point de vue, une fonction de rappel ne fait pas partie de la norme C, mais peut être facilement implémentée à l'aide de pointeurs de fonction, qui font partie de la norme.
Darryl
3
"partie de la norme C, donc également partie de C ++." C'est un malentendu typique, mais un malentendu néanmoins :-)
Expiation limitée
Je suis d'accord. Je vais le laisser tel quel, car cela ne causera plus de confusion que si je le change maintenant. Je voulais dire que le pointeur de fonction (!) Fait partie de la norme. Dire quelque chose de différent - je suis d'accord - est trompeur.
AudioDroid
En quoi les fonctions de rappel font-elles partie de la norme C? Je ne pense pas que le fait qu'il prenne en charge les fonctions et les pointeurs vers les fonctions signifie qu'il canonise spécifiquement les rappels en tant que concept de langage. En outre, comme mentionné, cela ne serait pas directement pertinent pour C ++ même s'il était exact. Et ce n'est surtout pas pertinent lorsque l'OP a demandé "quand et comment" utiliser les rappels en C ++ (une question boiteuse, trop large, mais néanmoins), et votre réponse est un avertissement de lien uniquement pour faire quelque chose de différent à la place.
underscore_d
4

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

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 des membres. Si vous utilisez la méthode C pour définir un rappel, vous devrez le pointer vers 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 le plaisir des lecteurs. L'exemple ne montre aucun objet instancié et utilisé. Un cas d'utilisation pour cette implémentation est lorsque vous avez une classe qui lit et stocke les données dans un espace temporaire et une autre qui post traite les données. Avec une fonction de rappel, pour chaque ligne de données lues, le rappel peut ensuite le traiter. Cette technique réduit les frais généraux de l'espace temporaire requis. Il est particulièrement utile pour les requêtes SQL qui renvoient une grande quantité de données qui doivent ensuite être post-traitées.

/////////////////////////////////////////////////////////////////////
// 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);
}
Gravy Jones
la source
0

Les signaux de Boost2 vous permettent de souscrire des fonctions membres génériques (sans modèles!) Et de manière threadsafe.

Exemple: les signaux Document-View peuvent être utilisés pour implémenter des architectures Document-View flexibles. Le document contiendra un signal auquel chacune des vues pourra se connecter. La classe Document suivante définit un document texte simple qui prend en charge plusieurs vues. Notez qu'il stocke 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;
};
crizCraig
la source
0

La réponse acceptée est complète mais liée à la question, je veux juste mettre un exemple simple ici. J'avais un code que je l'avais écrit il y a longtemps. je voulais traverser un arbre de manière ordonnée (nœud gauche puis nœud racine puis nœud droit) et chaque fois que j'atteins un nœud, je voulais pouvoir appeler une fonction arbitraire pour qu'il puisse 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);
}
Ehsan Ahmadi
la source
1
comment répond-il à la question?
Rajan Sharma
vous savez que la réponse acceptée est correcte et complète et je pense qu'il n'y a plus rien à dire en général. mais je poste un exemple de mon utilisation des fonctions de rappel.
Ehsan Ahmadi