Funzioni di callback in C ++


303

In C ++, quando e come si utilizza una funzione di callback?

EDIT:
Vorrei vedere un semplice esempio per scrivere una funzione di callback.


[This] ( thispointer.com/… ) spiega molto bene le nozioni di base sulle funzioni di callback e comprende facilmente il concetto.
Anurag Singh,

Risposte:


449

Nota: la maggior parte delle risposte riguarda i puntatori a funzione che è una possibilità per ottenere la logica di "callback" in C ++, ma ad oggi non è la più favorevole che penso.

Cosa sono i callback (?) E perché usarli (!)

Un callback è un callable (vedi più avanti) accettato da una classe o funzione, usato per personalizzare la logica corrente in base a quel callback.

Un motivo per utilizzare i callback è scrivere codice generico che è indipendente dalla logica nella funzione chiamata e che può essere riutilizzato con callback diversi.

Molte funzioni della libreria di algoritmi standard <algorithm>utilizzano i callback. Ad esempio, l' for_eachalgoritmo applica un callback unario a ogni elemento in un intervallo di iteratori:

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

che può essere utilizzato per incrementare prima e quindi stampare un vettore passando ad esempio callable appropriati:

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 << " "; });

quale stampa

5 6.2 8 9.5 11.2

Un'altra applicazione dei callback è la notifica ai chiamanti di determinati eventi che consente una certa flessibilità di tempo statico / di compilazione.

Personalmente, utilizzo una libreria di ottimizzazione locale che utilizza due callback diversi:

  • Il primo callback viene chiamato se è richiesto un valore di funzione e il gradiente basato su un vettore di valori di input (callback logico: determinazione del valore di funzione / derivazione del gradiente).
  • Il secondo callback viene chiamato una volta per ogni passaggio dell'algoritmo e riceve determinate informazioni sulla convergenza dell'algoritmo (callback di notifica).

Pertanto, il progettista della biblioteca non ha il compito di decidere cosa succede con le informazioni fornite al programmatore tramite il callback di notifica e non deve preoccuparsi di come determinare effettivamente i valori delle funzioni perché sono forniti dal callback logico. Far funzionare bene queste cose è un compito per l'utente della biblioteca e mantiene la biblioteca sottile e più generica.

Inoltre, i callback possono abilitare il comportamento di runtime dinamico.

Immagina una sorta di classe del motore di gioco che ha una funzione che viene attivata, ogni volta che gli utenti premono un pulsante sulla sua tastiera e una serie di funzioni che controllano il comportamento del tuo gioco. Con i callback è possibile (ri) decidere in fase di esecuzione quale azione verrà intrapresa.

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;
    }
};

Qui la funzione key_pressedutilizza i callback memorizzati actionsper ottenere il comportamento desiderato quando viene premuto un determinato tasto. Se il giocatore sceglie di cambiare il pulsante per saltare, il motore può chiamare

game_core_instance.update_keybind(newly_selected_key, &player_jump);

e quindi modificare il comportamento di una chiamata key_pressed(a cui le chiamate player_jump) una volta che si preme questo pulsante la prossima volta in gioco.

Cosa sono i callable in C ++ (11)?

Vedi concetti C ++: richiamabile su cppreference per una descrizione più formale.

La funzionalità di callback può essere realizzata in diversi modi in C ++ (11) poiché diverse cose risultano essere richiamabili * :

  • Puntatori a funzioni (inclusi puntatori a funzioni membro)
  • std::function oggetti
  • Espressioni Lambda
  • Espressioni di bind
  • Oggetti funzione (classi con operatore di chiamata di funzione sovraccarico operator())

* Nota: anche il puntatore ai membri dei dati può essere richiamato, ma nessuna funzione viene richiamata.

Diversi modi importanti per scrivere i callback in dettaglio

  • X.1 "Scrivere" un callback in questo post significa la sintassi per dichiarare e nominare il tipo di callback.
  • X.2 "Chiamare" un callback si riferisce alla sintassi per chiamare quegli oggetti.
  • X.3 "Utilizzo" di un callback indica la sintassi quando si passano argomenti a una funzione utilizzando un callback.

Nota: A partire da C ++ 17, è f(...)possibile scrivere una chiamata come std::invoke(f, ...)che gestisce anche il puntatore al caso membro.

1. Puntatori di funzioni

Un puntatore a funzione è il tipo "più semplice" (in termini di generalità; in termini di leggibilità probabilmente il peggiore) che un callback può avere.

Diamo una funzione semplice foo:

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

1.1 Scrivere un puntatore a funzione / notazione di tipo

Un tipo di puntatore a funzione ha la notazione

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

dove apparirà un tipo di puntatore a funzione denominata

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 usingdichiarazione ci dà la possibilità di rendere le cose un po 'più leggibili, poiché il typedeffor f_int_tpuò anche essere scritto come:

using f_int_t = int(*)(int);

Dove (almeno per me) è più chiaro che f_int_tè il nuovo tipo alias e anche il riconoscimento del tipo di puntatore a funzione è più semplice

E una dichiarazione di una funzione che utilizza un callback del tipo di puntatore a funzione sarà:

// 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 Notazione di richiamata

La notazione di chiamata segue la semplice sintassi della chiamata di funzione:

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 Notazione uso callback e tipi compatibili

Una funzione di richiamata che utilizza un puntatore a funzione può essere chiamata utilizzando i puntatori a funzione.

L'uso di una funzione che accetta un callback del puntatore a funzione è piuttosto semplice:

 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 Esempio

È possibile scrivere una funzione che non si basa su come funziona il callback:

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

dove possibili callbacks potrebbero essere

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

usato come

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. Puntatore alla funzione membro

Un puntatore alla funzione membro (di qualche classe C) è un tipo speciale di (e ancora più complesso) puntatore a funzione che richiede un oggetto di tipo Csu cui operare.

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

2.1 Scrivere il puntatore sulla notazione di funzione / tipo membro

Un puntatore al tipo di funzione membro per alcune classi Tha la notazione

// 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)

dove un puntatore denominato alla funzione membro apparirà in analogia al puntatore funzione :

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;

Esempio: dichiarazione di una funzione che prende un puntatore al callback della funzione membro come uno dei suoi argomenti:

// 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 Notazione di richiamata

Il puntatore alla funzione membro di Cpuò essere richiamato, rispetto a un oggetto di tipo C, usando le operazioni di accesso del membro sul puntatore senza riferimenti. Nota: parentesi richiesta!

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
}

Nota: se Cè disponibile un puntatore a, la sintassi è equivalente (dove anche il puntatore Cdeve essere referenziato):

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 Notazione di utilizzo di callback e tipi compatibili

Una funzione di callback che prende un puntatore della funzione membro della classe Tpuò essere chiamata usando un puntatore della funzione membro della classe T.

L'uso di una funzione che porta un puntatore al callback della funzione membro è, in analogia con i puntatori funzione, anche abbastanza semplice:

 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::functionoggetti (intestazione <functional>)

La std::functionclasse è un wrapper di funzione polimorfica per archiviare, copiare o invocare callable.

3.1 Scrivere una std::functionnotazione oggetto / tipo

Il tipo di un std::functionoggetto che memorizza un callable è simile a:

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 Notazione di richiamata

È std::functionstata operator()definita la classe che può essere utilizzata per invocare il suo obiettivo.

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 Notazione di utilizzo di callback e tipi compatibili

Il std::functioncallback è più generico dei puntatori di funzione o del puntatore alla funzione membro poiché è possibile passare tipi diversi e convertirli implicitamente in un std::functionoggetto.

3.3.1 Puntatori di funzioni e puntatori a funzioni membro

Un puntatore a funzione

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

o un puntatore alla funzione membro

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

può essere utilizzato.

3.3.2 Espressioni lambda

Una chiusura senza nome da un'espressione lambda può essere memorizzata in un std::functionoggetto:

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

È std::bindpossibile passare il risultato di un'espressione. Ad esempio associando i parametri a una chiamata del puntatore a funzione:

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 )

Dove anche gli oggetti possono essere associati come oggetto per l'invocazione del puntatore alle funzioni membro:

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 Oggetti funzione

Anche oggetti di classi con un operator()sovraccarico adeguato possono essere memorizzati all'interno di un std::functionoggetto.

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 Esempio

Modifica dell'esempio del puntatore a funzione da utilizzare 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]);
  }
}

dà molta più utilità a quella funzione perché (vedi 3.3) abbiamo più possibilità di usarla:

// 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. Tipo di richiamata basato su modelli

Utilizzando i modelli, il codice che chiama il callback può essere persino più generale dell'uso degli std::functionoggetti.

Si noti che i modelli sono una funzionalità di compilazione e sono uno strumento di progettazione per il polimorfismo in fase di compilazione. Se il comportamento dinamico di runtime deve essere ottenuto tramite callback, i template aiuteranno ma non indurranno la dinamica di runtime.

4.1 Scrivere (notazioni di tipo) e chiamare callback con template

La generalizzazione, ad esempio, del std_ftransform_every_intcodice dall'alto ancora può essere ottenuta usando i template:

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]);
  }
}

con una sintassi ancora più generale (e anche la più semplice) per un tipo di callback che è un argomento semplice, da dedurre:

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]);
  }
}

Nota: l'output incluso stampa il nome del tipo dedotto per il tipo di modello F. L'implementazione di type_nameè data alla fine di questo post.

L'implementazione più generale per la trasformazione unaria di un intervallo fa parte della libreria standard, vale a dire std::transform, che è anche modellata rispetto ai tipi iterati.

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 Esempi che utilizzano callback basati su modelli e tipi compatibili

I tipi compatibili per il std::functionmetodo di callback basato su modelli stdf_transform_every_int_templsono identici ai tipi sopra menzionati (vedere 3.4).

Usando la versione con template tuttavia, la firma del callback usato potrebbe cambiare leggermente:

// 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);

Nota: std_ftransform_every_int(versione non basata su modelli; vedi sopra) funziona con fooma non utilizza 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";
}

Il semplice parametro di modello transform_every_int_templpuò essere ogni possibile tipo richiamabile.

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);

Il codice sopra riportato stampa:

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 implementazione utilizzata sopra

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

35
@BogeyJammer: nel caso non l'avessi notato: la risposta ha due parti. 1. Una spiegazione generale di "callbacks" con un piccolo esempio. 2. Un elenco completo di diversi callable e le modalità di scrittura del codice tramite callback. Ti invitiamo a non approfondire o leggere l'intera risposta, ma solo perché non vuoi una visione dettagliata, non è il caso che la risposta sia inefficace o "brutalmente copiata". L'argomento è "callback c ++". Anche se la parte 1 va bene per OP, altri potrebbero trovare utile la parte 2. Sentiti libero di sottolineare qualsiasi mancanza di informazioni o critiche costruttive per la prima parte invece di -1.
Pixelchemist,

1
La parte 1 non è amichevole per i principianti e abbastanza chiara. Non posso essere più costruttivo dicendo che non è riuscito a impararmi qualcosa. E la parte 2, non è stata richiesta, inondando la pagina ed è fuori discussione anche se si finge che abbia utilità nonostante il fatto che si trova comunemente nella documentazione dedicata in cui tali informazioni dettagliate sono ricercate in primo luogo. Sicuramente mantengo il downvote. Un voto singolo rappresenta un'opinione personale, quindi accettala e rispettala
Bogey Jammer,

24
@BogeyJammer Non sono nuovo nella programmazione ma sono nuovo nel "c ++ moderno". Questa risposta mi fornisce l'esatto contesto di cui ho bisogno per ragionare sul ruolo svolto dai callback, in particolare in c ++. L'OP potrebbe non aver richiesto più esempi, ma è consuetudine su SO, in una ricerca senza fine per educare un mondo di sciocchi, per elencare tutte le possibili soluzioni a una domanda. Se sembra troppo simile a un libro, l'unico consiglio che posso offrire è esercitarsi un po 'leggendone alcuni .
dc

int b = foobar(a, foo); // call foobar with pointer to foo as callback, questo è un errore di battitura giusto? foodovrebbe essere un puntatore affinché questo funzioni AFAIK.
konoufo,

@konoufo: [conv.func]dello standard C ++ 11 dice: " Un valore di tipo T di funzione può essere convertito in un valore di tipo" puntatore a T. " Il risultato è un puntatore alla funzione. "Questa è una conversione standard e come tale avviene implicitamente. Si potrebbe (ovviamente) usare qui il puntatore a funzione.
Pixelchemist,

160

Esiste anche il modo C di eseguire callback: puntatori a funzioni

//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);
}

Ora, se si desidera passare a metodi di classe come callback, le dichiarazioni a tali puntatori a funzioni hanno dichiarazioni più complesse, ad esempio:

//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);
}

1
C'è un errore nell'esempio del metodo di classe. L'Invocazione dovrebbe essere: (esempio. * Callback) (1.0f)
CarlJohnson

Grazie per averlo sottolineato. Aggiungerò entrambi per illustrare l'invocazione attraverso un oggetto e tramite un puntatore oggetto.
Ramon Zarazua B.

3
Questo ha lo svantaggio di std :: tr1: funzione in quanto il callback viene digitato per classe; ciò rende poco pratico l'uso dei callback in stile C quando l'oggetto che esegue la chiamata non conosce la classe dell'oggetto da chiamare.
bleater,

Come potrei farlo senza typedefil tipo di callback? È anche possibile?
Tomáš Zato - Ripristina Monica il

1
Si, puoi. typedefè solo zucchero sintattico per renderlo più leggibile. Senza typedef, la definizione di DoWorkObject per puntatori a funzione potrebbe essere: void DoWorkObject(int (*callback)(float)). I consigli dei membri sarebbero:void DoWorkObject(int (ClassName::*callback)(float))
Ramon Zarazua B.

68

Scott Meyers fa un bell'esempio:

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;
};

Penso che l'esempio dica tutto.

std::function<> è il modo "moderno" di scrivere callback C ++.


1
Per interesse, in quale libro SM fornisce questo esempio? Saluti :)
sam-w,

5
So che questo è vecchio, ma poiché ho quasi iniziato a farlo e alla fine non ha funzionato sulla mia configurazione (mingw), se stai usando GCC versione <4.x, questo metodo non è supportato. Alcune delle dipendenze che sto usando non verranno compilate senza molto lavoro nella versione gcc> = 4.0.1, quindi sono bloccato con l'uso di callback in stile C vecchio stile, che funzionano bene.
OzBarry,

38

Una funzione di callback è un metodo passato in una routine e chiamato ad un certo punto dalla routine a cui è passato.

Questo è molto utile per creare software riutilizzabili. Ad esempio, molte API del sistema operativo (come l'API di Windows) utilizzano fortemente i callback.

Ad esempio, se si desidera lavorare con i file in una cartella, è possibile chiamare una funzione API, con la propria routine, e la routine viene eseguita una volta per file nella cartella specificata. Ciò consente all'API di essere molto flessibile.


63
Questa risposta in realtà non è un programmatore medio che dice qualcosa che non sapeva. Sto imparando il C ++ mentre ho familiarità con molte altre lingue. Qual è il callback in generale non mi riguarda.
Tomáš Zato - Ripristina Monica il

17

La risposta accettata è molto utile e abbastanza completa. Tuttavia, l'OP afferma

Vorrei vedere un semplice esempio per scrivere una funzione di callback.

Quindi ecco qua, da C ++ 11 hai std::functionquindi non c'è bisogno di puntatori di funzioni e cose simili:

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

Questo esempio è in qualche modo reale, perché si desidera chiamare la funzione print_hashescon diverse implementazioni di funzioni hash, a questo scopo ne ho fornito uno semplice. Riceve una stringa, restituisce un int (un valore hash della stringa fornita) e tutto ciò che è necessario ricordare dalla parte della sintassi è std::function<int (const std::string&)>che descrive tale funzione come argomento di input della funzione che lo invocherà.


tra tutte quelle risposte sopra, questa mi ha fatto capire quali sono i callback e come usarli. Grazie.
Mehar Charan Sahai,

@MeharCharanSahai Lieto di sentirlo :) Prego.
Miljen Mikic,

9

Non esiste un concetto esplicito di una funzione di callback in C ++. I meccanismi di callback sono spesso implementati tramite puntatori a funzione, oggetti functor o oggetti callback. I programmatori devono progettare e implementare esplicitamente la funzionalità di callback.

Modifica in base al feedback:

Nonostante il feedback negativo che questa risposta ha ricevuto, non è sbagliata. Proverò a fare un lavoro migliore spiegando da dove vengo.

C e C ++ hanno tutto il necessario per implementare le funzioni di callback. Il modo più comune e banale per implementare una funzione di callback è passare un puntatore a funzione come argomento di funzione.

Tuttavia, le funzioni di callback e gli indicatori di funzione non sono sinonimi. Un puntatore a funzione è un meccanismo linguistico, mentre una funzione di callback è un concetto semantico. I puntatori a funzione non sono l'unico modo per implementare una funzione di callback: puoi anche usare funzioni e persino funzioni virtuali di varietà da giardino. Ciò che rende una funzione una chiamata non è il meccanismo utilizzato per identificare e chiamare la funzione, ma il contesto e la semantica della chiamata. Dire che qualcosa è una funzione di callback implica una separazione maggiore della normale tra la funzione di chiamata e la funzione specifica chiamata, un accoppiamento concettuale più libero tra il chiamante e il chiamante, con il chiamante che ha un controllo esplicito su ciò che viene chiamato.

Ad esempio, la documentazione .NET per IFormatProvider afferma che "GetFormat è un metodo di callback" , anche se è solo un metodo di interfaccia run-of-the-mill. Non penso che qualcuno sosterrebbe che tutte le chiamate al metodo virtuale sono funzioni di callback. Ciò che rende GetFormat un metodo di callback non è la meccanica di come viene passato o invocato, ma la semantica del chiamante che sceglie quale metodo GetFormat dell'oggetto verrà chiamato.

Alcune lingue includono funzionalità con semantica di callback esplicita, in genere correlate agli eventi e alla gestione degli eventi. Ad esempio, C # ha il tipo di evento con sintassi e semantica esplicitamente progettato attorno al concetto di callback. Visual Basic ha la sua clausola Handles , che dichiara esplicitamente un metodo come una funzione di callback mentre sottrae il concetto di delegati o puntatori a funzioni. In questi casi, il concetto semantico di callback è integrato nella lingua stessa.

C e C ++, d'altra parte, non incorporano il concetto semantico di funzioni di callback in modo altrettanto esplicito. I meccanismi ci sono, la semantica integrata no. Puoi implementare bene le funzioni di callback, ma per ottenere qualcosa di più sofisticato che includa la semantica di callback esplicita devi costruirla sulla base di ciò che offre C ++, come quello che Qt ha fatto con i loro segnali e slot .

In breve, C ++ offre ciò di cui hai bisogno per implementare i callback, spesso abbastanza facilmente e banalmente usando i puntatori a funzione. Ciò che non ha sono parole chiave e caratteristiche la cui semantica è specifica per i callback, come raise , emit , Handles , event + = , ecc. Se provieni da una lingua con questi tipi di elementi, il supporto di callback nativo in C ++ si sentirà castrato.


1
fortunatamente questa non è stata la prima risposta che ho letto quando ho visitato questa pagina, altrimenti avrei fatto un rimbalzo immediato!
ubugnu,

6

Le funzioni di callback fanno parte dello standard C, quindi anche del C ++. Ma se stai lavorando con C ++, ti suggerirei invece di utilizzare il modello di osservatore : http://en.wikipedia.org/wiki/Observer_pattern


1
Le funzioni di callback non sono necessariamente sinonimo di esecuzione di una funzione tramite un puntatore a funzione passato come argomento. Secondo alcune definizioni, il termine funzione di callback porta la semantica aggiuntiva di notificare qualche altro codice di qualcosa che è appena accaduto, o che è tempo che qualcosa dovrebbe accadere. Da quel punto di vista, una funzione di callback non fa parte dello standard C, ma può essere facilmente implementata usando i puntatori a funzione, che fanno parte dello standard.
Darryl

3
"parte dello standard C, quindi anche parte del C ++." Questo è un tipico malinteso, ma comunque un malinteso :-)
Espiazione limitata

Devo essere d'accordo Lo lascerò così com'è, poiché causerà più confusione solo se lo cambio ora. Volevo dire che il puntatore a funzione (!) Fa parte dello standard. Dire qualcosa di diverso da questo - sono d'accordo - è fuorviante.
AudioDroid,

In che modo le funzioni di callback sono "parte dello standard C"? Non penso che il fatto che supporti funzioni e puntatori a funzioni significhi che canonizza specificamente i callback come concetto di linguaggio. Inoltre, come detto, ciò non sarebbe direttamente rilevante per il C ++ anche se fosse accurato. E in particolare non è rilevante quando l'OP ha chiesto "quando e come" utilizzare i callback in C ++ (una domanda troppo bassa, troppo ampia, ma comunque), e la tua risposta è un monito solo link a fare qualcosa di diverso.
underscore_d

4

Vedi la definizione sopra dove afferma che una funzione di callback viene passata ad un'altra funzione e ad un certo punto viene chiamata.

In C ++ è auspicabile che le funzioni di callback chiamino un metodo di classi. Quando lo fai hai accesso ai dati dei membri. Se si utilizza il modo C per definire un callback, è necessario puntarlo a una funzione membro statica. Questo non è molto desiderabile.

Ecco come è possibile utilizzare i callback in C ++. Assumi 4 file. Una coppia di file .CPP / .H per ogni classe. La classe C1 è la classe con un metodo che vogliamo richiamare. C2 richiama il metodo di C1. In questo esempio la funzione di callback accetta 1 parametro che ho aggiunto per il bene dei lettori. L'esempio non mostra alcun oggetto istanziato e utilizzato. Un caso d'uso per questa implementazione è quando si dispone di una classe che legge e archivia i dati nello spazio temporaneo e un'altra che pubblica i dati. Con una funzione di callback, per ogni riga di dati letti il ​​callback può quindi elaborarlo. Questa tecnica elimina le spese generali dello spazio temporaneo richiesto. È particolarmente utile per le query SQL che restituiscono una grande quantità di dati che devono essere successivamente elaborati.

/////////////////////////////////////////////////////////////////////
// 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);
}

0

Boost's signal2 ti consente di abbonare funzioni membro generiche (senza template!) E in modo sicuro.

Esempio: i segnali di Document-View possono essere utilizzati per implementare architetture flessibili di Document-View. Il documento conterrà un segnale a cui ciascuna vista può connettersi. La seguente classe Document definisce un semplice documento di testo che supporta viste multiple. Si noti che memorizza un singolo segnale a cui saranno collegate tutte le viste.

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;
};

Successivamente, possiamo iniziare a definire le viste. La seguente classe TextView fornisce una semplice visualizzazione del testo del documento.

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

La risposta accettata è esaustiva ma collegata alla domanda, voglio solo fare un semplice esempio qui. Avevo un codice che avevo scritto molto tempo fa. volevo attraversare un albero in modo ordinato (nodo sinistro quindi nodo radice quindi nodo destro) e ogni volta che raggiungo un nodo volevo essere in grado di chiamare una funzione arbitraria in modo che potesse fare tutto.

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);
}

1
come risponde alla domanda?
Rajan Sharma,

sai che la risposta accettata è corretta e completa e penso che non ci sia altro da dire in generale. ma inserisco un esempio del mio utilizzo delle funzioni di callback.
Ehsan Ahmadi,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.