web-dev-qa-db-ger.com

Callback-Funktionen in C ++

Wann und wie verwenden Sie in C++ eine Rückruffunktion?

EDIT:
Ich möchte ein einfaches Beispiel zum Schreiben einer Rückruffunktion sehen.

271
cpx

Anmerkung: Die meisten Antworten beziehen sich auf Funktionszeiger. Dies ist eine Möglichkeit, eine "Rückruf" -Logik in C++ zu erzielen, aber aus heutiger Sicht nicht die günstigste.

Was sind Rückrufe (?) Und warum (!)

Ein Callback ist ein callable (siehe weiter unten), das von einer Klasse oder Funktion akzeptiert wird und verwendet wird, um die aktuelle Logik abhängig von diesem Callback anzupassen.

Ein Grund für die Verwendung von Rückrufen ist das Schreiben von generischem Code, der von der Logik in der aufgerufenen Funktion unabhängig ist und mit verschiedenen Rückrufen wiederverwendet werden kann.

Viele Funktionen der Standardalgorithmusbibliothek <algorithm> Verwenden Callbacks. Zum Beispiel wendet der Algorithmus for_each Einen unären Rückruf auf jedes Element in einer Reihe von Iteratoren an:

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

mit dieser Funktion können Sie zuerst einen Vektor inkrementieren und dann drucken, indem Sie entsprechende Callables übergeben. Beispiel:

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

welche druckt

5 6.2 8 9.5 11.2

Eine weitere Anwendung von Rückrufen ist die Benachrichtigung von Anrufern über bestimmte Ereignisse, wodurch eine gewisse Flexibilität hinsichtlich der statischen Zeit und der Kompilierzeit ermöglicht wird.

Persönlich verwende ich eine lokale Optimierungsbibliothek, die zwei verschiedene Rückrufe verwendet:

  • Der erste Callback wird aufgerufen, wenn ein Funktionswert und der auf einem Vektor von Eingabewerten basierende Gradient benötigt werden (logischer Callback: Funktionswertermittlung/Gradientenableitung).
  • Der zweite Callback wird für jeden Algorithmusschritt einmal aufgerufen und erhält bestimmte Informationen über die Konvergenz des Algorithmus (Notification Callback).

Somit ist der Bibliotheksdesigner nicht dafür verantwortlich, zu entscheiden, was mit den Informationen geschieht, die dem Programmierer über den Benachrichtigungsrückruf gegeben werden, und er muss sich keine Gedanken darüber machen, wie Funktionswerte tatsächlich bestimmt werden, da sie durch den Logikrückruf bereitgestellt werden. Diese Dinge richtig zu machen, ist eine Aufgabe des Bibliotheksbenutzers und hält die Bibliothek schlank und allgemeiner.

Callbacks können darüber hinaus ein dynamisches Laufzeitverhalten ermöglichen.

Stellen Sie sich eine Art Game-Engine-Klasse vor, die eine Funktion hat, die jedes Mal ausgelöst wird, wenn der Benutzer eine Taste auf seiner Tastatur drückt, und eine Reihe von Funktionen, die Ihr Spielverhalten steuern. Mit Callbacks können Sie zur Laufzeit (neu) entscheiden, welche Aktion durchgeführt wird.

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

Hier verwendet die Funktion key_pressed Die in actions gespeicherten Rückrufe, um das gewünschte Verhalten zu erhalten, wenn eine bestimmte Taste gedrückt wird. Wenn der Spieler den Knopf zum Springen ändert, kann der Motor rufen

game_core_instance.update_keybind(newly_selected_key, &player_jump);

und ändern Sie daher das Verhalten eines Anrufs in key_pressed (was der Anruf player_jump), sobald diese Taste beim nächsten Spiel gedrückt wird.

Was sind callables in C++ (11)?

Eine formellere Beschreibung finden Sie unter C++ - Konzepte: Callable auf cppreference.

Callback-Funktionalität kann in C++ (11) auf verschiedene Arten realisiert werden, da sich verschiedene Dinge als aufrufbar herausstellen * :

  • Funktionszeiger (einschließlich Zeiger auf Elementfunktionen)
  • std::function Objekte
  • Lambda-Ausdrücke
  • Ausdrücke binden
  • Funktionsobjekte (Klassen mit überladenem Funktionsaufrufoperator operator())

* Hinweis: Zeiger auf Datenelemente sind ebenfalls aufrufbar, aber es wird überhaupt keine Funktion aufgerufen.

Mehrere wichtige Möglichkeiten, um Rückrufe im Detail zu schreiben

  • X.1 "Schreiben" eines Rückrufs in diesem Beitrag bedeutet die Syntax zum Deklarieren und Benennen des Rückruftyps.
  • X.2 "Calling" Ein Callback bezieht sich auf die Syntax zum Aufrufen dieser Objekte.
  • X.3 "Using" a callback (Rückruf verwenden) bezeichnet die Syntax, wenn Argumente mithilfe eines Rückrufs an eine Funktion übergeben werden.

Hinweis: Ab C++ 17 kann ein Aufruf wie f(...) als std::invoke(f, ...) geschrieben werden, der auch den Zeiger auf den Member-Fall behandelt.

1. Funktionszeiger

Ein Funktionszeiger ist der 'einfachste' (in Bezug auf die Allgemeinheit; in Bezug auf die Lesbarkeit wohl der schlechteste) Typ, den ein Rückruf haben kann.

Lassen Sie uns eine einfache Funktion foo haben:

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

1.1 Schreiben eines Funktionszeigers/einer Typennotation

Ein Funktionszeigertyp hat die Notation

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

wobei ein benannter Funktionszeigertyp wie folgt aussehen wird

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;

Die using -Deklaration gibt uns die Möglichkeit, die Dinge ein wenig lesbarer zu machen, da typedef für f_int_t Auch geschrieben werden kann als:

using f_int_t = int(*)(int);

Wo (zumindest für mich) klarer ist, dass f_int_t Der neue Typ-Alias ​​ist und das Erkennen des Funktionszeigertyps auch einfacher ist

Und eine Deklaration einer Funktion mit einem Callback vom Typ Funktionszeiger lautet:

// 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 Rückrufnotation

Die Aufrufnotation folgt der einfachen Funktionsaufrufsyntax:

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 Rückrufnotation und kompatible Typen

Eine Callback-Funktion, die einen Funktionszeiger verwendet, kann mit Funktionszeigern aufgerufen werden.

Die Verwendung einer Funktion, die einen Funktionszeiger-Rückruf akzeptiert, ist recht einfach:

 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 Beispiel

Es kann eine Funktion geschrieben werden, die nicht von der Funktionsweise des Rückrufs abhängt:

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

wo möglich könnten rückrufe sein

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

gebraucht wie

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. Zeiger auf Mitgliedsfunktion

Ein Zeiger auf eine Mitgliedsfunktion (einer Klasse C) ist ein spezieller Typ eines (und noch komplexeren) Funktionszeigers, für dessen Bearbeitung ein Objekt vom Typ C erforderlich ist.

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

2.1 Zeiger auf Elementfunktion/Typnotation schreiben

Ein Zeiger auf den Elementfunktionstyp für eine Klasse T hat die 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)

dabei sieht ein benannter Zeiger auf die Mitgliedsfunktion analog zum Funktionszeiger folgendermaßen aus:

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;

Beispiel: Deklarieren einer Funktion, die einen Zeiger auf einen Member Function Callback als eines ihrer Argumente verwendet:

// 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 Rückrufnotation

Der Zeiger auf die Elementfunktion von C kann in Bezug auf ein Objekt vom Typ C aufgerufen werden, indem Elementzugriffsoperationen für den dereferenzierten Zeiger verwendet werden. Hinweis: Klammer erforderlich !

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
}

Hinweis: Wenn ein Zeiger auf C verfügbar ist, ist die Syntax äquivalent (wobei auch der Zeiger auf C dereferenziert werden muss):

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 Rückrufnotation und kompatible Typen

Eine Rückruffunktion, die einen Mitgliedsfunktionszeiger der Klasse T verwendet, kann mit einem Mitgliedsfunktionszeiger der Klasse T aufgerufen werden.

Die Verwendung einer Funktion, die einen Zeiger auf einen Member Function Callback nimmt, ist - analog zu Funktionszeigern - ebenfalls ganz einfach:

 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 Objekte (Header <functional>)

Die Klasse std::function Ist ein polymorpher Funktionswrapper zum Speichern, Kopieren oder Aufrufen von Callables.

3.1 Schreiben einer Objekt-/Typnotation std::function

Der Typ eines std::function - Objekts, in dem ein Callable gespeichert ist, sieht folgendermaßen aus:

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 Rückrufnotation

In der Klasse std::function Ist operator() definiert, mit dessen Hilfe das Ziel aufgerufen werden kann.

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 Rückrufnotation und kompatible Typen

Der std::function - Rückruf ist allgemeiner als Funktionszeiger oder Zeiger auf Elementfunktionen, da verschiedene Typen übergeben und implizit in ein std::function - Objekt konvertiert werden können.

3.3.1 Funktionszeiger und Zeiger auf Elementfunktionen

Ein Funktionszeiger

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

oder ein Zeiger auf die Elementfunktion

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

kann verwendet werden.

3.3.2 Lambda-Ausdrücke

Ein unbenannter Abschluss eines Lambda-Ausdrucks kann in einem std::function - Objekt gespeichert werden:

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 - Ausdrücke

Das Ergebnis eines std::bind - Ausdrucks kann übergeben werden. Zum Beispiel durch Binden von Parametern an einen Funktionszeigeraufruf:

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 )

Wobei auch Objekte als Objekt für den Aufruf von Zeiger auf Mitgliedsfunktionen gebunden werden können:

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 Funktionsobjekte

Objekte von Klassen mit einer korrekten operator() -Überladung können auch in einem std::function - Objekt gespeichert werden.

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 Beispiel

Ändern des Funktionszeiger-Beispiels zur Verwendung von 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]);
  }
}

diese Funktion ist um ein Vielfaches nützlicher, da wir (siehe 3.3) mehr Möglichkeiten haben, sie zu verwenden:

// 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. Vorgesehener Rückruftyp

Bei Verwendung von Vorlagen kann der Code, der den Rückruf aufruft, noch allgemeiner sein als die Verwendung von std::function - Objekten.

Beachten Sie, dass Vorlagen ein Feature zur Kompilierungszeit und ein Entwurfstool für den Polymorphismus zur Kompilierungszeit sind. Wenn ein dynamisches Laufzeitverhalten durch Rückrufe erreicht werden soll, helfen Vorlagen, induzieren jedoch keine Laufzeitdynamik.

4.1 Schreiben (Typnotationen) und Aufrufen von Callbacks mit Vorlagen

Eine weitere Verallgemeinerung, d. H. Des std_ftransform_every_int - Codes von oben, kann durch die Verwendung von Vorlagen erreicht werden:

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

mit einer noch allgemeineren (und zugleich einfachsten) Syntax für einen Rückruftyp, die ein einfaches, abzuleitendes Argument ist:

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

Hinweis: Die enthaltene Ausgabe gibt den Typnamen aus, der für den Typ F mit Vorlagen abgeleitet wurde. Die Implementierung von type_name Ist am Ende dieses Beitrags angegeben.

Die allgemeinste Implementierung für die unäre Transformation eines Bereichs ist Teil der Standardbibliothek, nämlich std::transform, Die auch in Bezug auf die iterierten Typen berücksichtigt wird.

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 Beispiele für die Verwendung von Callback-Vorlagen und kompatiblen Typen

Die kompatiblen Typen für die Callback-Methode std::function Mit Vorlage stdf_transform_every_int_templ Sind mit den oben genannten Typen identisch (siehe 3.4).

In der Template-Version kann sich die Signatur des verwendeten Rückrufs jedoch geringfügig ändern:

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

Hinweis: std_ftransform_every_int (Version ohne Vorlage; siehe oben) funktioniert mit foo, aber ohne 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";
}

Der Plain-Templated-Parameter von transform_every_int_templ Kann jeder mögliche aufrufbare Typ sein.

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

Der obige Code wird gedruckt:

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 - Implementierung, die oben verwendet wurde

#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

Es gibt auch die C-Methode für Rückrufe: Funktionszeiger

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

Wenn Sie nun Klassenmethoden als Callbacks übergeben möchten, haben die Deklarationen zu diesen Funktionszeigern komplexere Deklarationen. Beispiel:

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

Scott Meyers gibt ein schönes Beispiel:

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

Ich denke, das Beispiel sagt alles.

std::function<> ist die "moderne" Art, C++ - Callbacks zu schreiben.

67
Karl von Moor

Eine Rückruffunktion ist eine Methode, die an eine Routine übergeben und irgendwann von der Routine, an die sie übergeben wird, aufgerufen wird.

Dies ist sehr nützlich, um wiederverwendbare Software zu erstellen. Beispielsweise verwenden viele Betriebssystem-APIs (z. B. die Windows-API) häufig Rückrufe.

Wenn Sie beispielsweise mit Dateien in einem Ordner arbeiten möchten, können Sie eine API-Funktion mit Ihrer eigenen Routine aufrufen und Ihre Routine wird einmal pro Datei im angegebenen Ordner ausgeführt. Dadurch kann die API sehr flexibel sein.

38
Reed Copsey

Die akzeptierte Antwort ist sehr nützlich und ziemlich umfassend. Allerdings heißt es im OP

Ich möchte ein einfaches Beispiel sehen, um eine Rückruffunktion zu schreiben.

Ab C++ 11 haben Sie also std::function, Sodass Sie keine Funktionszeiger und ähnliches mehr benötigen:

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

Dieses Beispiel ist übrigens irgendwie real, weil Sie die Funktion print_hashes Mit verschiedenen Implementierungen von Hash-Funktionen aufrufen möchten, zu diesem Zweck habe ich eine einfache bereitgestellt. Es empfängt einen String, gibt einen int (einen Hash-Wert des bereitgestellten Strings) zurück und alles, woran Sie sich im Syntax-Teil erinnern müssen, ist std::function<int (const std::string&)>, das eine solche Funktion als Eingabeargument der Funktion beschreibt, die dies tun wird rufe es auf.

13
Miljen Mikic

In C++ gibt es kein explizites Konzept für eine Rückruffunktion. Rückrufmechanismen werden häufig über Funktionszeiger, Funktionsobjekte oder Rückrufobjekte implementiert. Die Programmierer müssen Rückruffunktionen explizit entwerfen und implementieren.

Bearbeiten basierend auf Feedback:

Trotz des negativen Feedbacks, das diese Antwort erhalten hat, ist es nicht falsch. Ich werde versuchen, besser zu erklären, woher ich komme.

C und C++ haben alles, was Sie zum Implementieren von Rückruffunktionen benötigen. Die gebräuchlichste und einfachste Methode zum Implementieren einer Rückruffunktion besteht darin, einen Funktionszeiger als Funktionsargument zu übergeben.

Rückruffunktionen und Funktionszeiger sind jedoch keine Synonyme. Ein Funktionszeiger ist ein Sprachmechanismus, während eine Rückruffunktion ein semantisches Konzept ist. Funktionszeiger sind nicht die einzige Möglichkeit, eine Rückruffunktion zu implementieren. Sie können auch Funktoren und sogar virtuelle Funktionen der Gartenvielfalt verwenden. Was einen Funktionsaufruf zu einem Rückruf macht, ist nicht der Mechanismus zum Identifizieren und Aufrufen der Funktion, sondern der Kontext und die Semantik des Aufrufs. Etwas zu sagen, was eine Rückruffunktion ist, impliziert eine größere als normale Trennung zwischen der aufrufenden Funktion und der aufgerufenen bestimmten Funktion, eine lockerere konzeptionelle Kopplung zwischen dem Anrufer und dem Angerufenen, wobei der Anrufer die explizite Kontrolle darüber hat, was gerufen wird. Es ist diese verschwommene Vorstellung von einer lockereren konzeptionellen Kopplung und einer aufrufergesteuerten Funktionsauswahl, die etwas zu einer Rückruffunktion macht, nicht die Verwendung eines Funktionszeigers.

In der .NET-Dokumentation für IFormatProvider heißt es beispielsweise "GetFormat ist eine Rückrufmethode" , obwohl es sich nur um eine handelt Run-of-the-Mill-Schnittstellenmethode. Ich glaube nicht, dass irgendjemand argumentieren würde, dass alle virtuellen Methodenaufrufe Callback-Funktionen sind. Was GetFormat zu einer Callback-Methode macht, ist nicht die Mechanik der Übergabe oder des Aufrufs, sondern die Semantik des Aufrufers, der auswählt, welche GetFormat-Methode des Objekts aufgerufen wird.

Einige Sprachen enthalten Funktionen mit expliziter Rückrufsemantik, die sich normalerweise auf Ereignisse und die Ereignisbehandlung beziehen. Beispielsweise hat C # den Ereignistyp , wobei Syntax und Semantik explizit auf das Konzept von Rückrufen ausgelegt sind. Visual Basic verfügt über die Handles -Klausel, die eine Methode explizit als Rückruffunktion deklariert und gleichzeitig das Konzept von Delegaten oder Funktionszeigern abstrahiert. In diesen Fällen ist das semantische Konzept eines Rückrufs in die Sprache selbst integriert.

Dagegen wird in C und C++ das semantische Konzept von Rückruffunktionen fast nicht so explizit eingebettet. Die Mechanismen sind da, die integrierte Semantik nicht. Sie können Rückruffunktionen problemlos implementieren. Um jedoch eine komplexere Funktion zu erhalten, die eine explizite Rückrufsemantik umfasst, müssen Sie diese auf den Funktionen von C++ aufbauen, z. B. auf denen, die Qt mit ihrem Signale und Slots ausgeführt hat.

Kurz gesagt, C++ bietet alles, was Sie zum Implementieren von Rückrufen benötigen, und zwar häufig recht einfach und trivial mithilfe von Funktionszeigern. Was es nicht hat, sind Schlüsselwörter und Funktionen, deren Semantik spezifisch für Rückrufe ist, wie raise , emit , Behandelt , Ereignis + = , usw. Wenn Sie aus einer Sprache mit diesen Elementtypen kommen, fühlt sich die native Callback-Unterstützung in C++ neutralisiert an.

8
Darryl

Callback-Funktionen sind Teil des C-Standards und daher auch Teil von C++. Aber wenn Sie mit C++ arbeiten, würde ich vorschlagen, dass Sie stattdessen das Beobachtermuster verwenden: http://en.wikipedia.org/ wiki/Observer_pattern

6
AudioDroid

In der obigen Definition wird angegeben, dass eine Rückruffunktion an eine andere Funktion übergeben und zu einem bestimmten Zeitpunkt aufgerufen wird.

In C++ ist es wünschenswert, dass Callback-Funktionen eine Klassenmethode aufrufen. Dabei haben Sie Zugriff auf die Mitgliedsdaten. Wenn Sie die C-Methode zum Definieren eines Rückrufs verwenden, müssen Sie ihn auf eine statische Elementfunktion verweisen. Dies ist nicht sehr wünschenswert.

So können Sie Rückrufe in C++ verwenden. Angenommen, 4 Dateien. Ein Paar CPP/H-Dateien für jede Klasse. Klasse C1 ist die Klasse mit einer Methode, die wir zurückrufen möchten. C2 ruft die Methode von C1 auf. In diesem Beispiel verwendet die Rückruffunktion 1 Parameter, den ich aus Gründen der Lesbarkeit hinzugefügt habe. Das Beispiel zeigt keine Objekte, die instanziiert und verwendet werden. Ein Anwendungsfall für diese Implementierung ist, wenn Sie eine Klasse haben, die Daten liest und in einem temporären Bereich speichert, und eine andere, die die Daten nachbearbeitet. Mit einer Callback-Funktion kann der Callback dann für jede gelesene Datenzeile diese verarbeiten. Diese Technik reduziert den Overhead des benötigten temporären Platzes. Dies ist besonders nützlich für SQL-Abfragen, die eine große Datenmenge zurückgeben, die anschließend nachbearbeitet werden muss.

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

Mit Boost's signals2 können Sie generische Member-Funktionen (ohne Templates!) Und threadsicher abonnieren.

Beispiel: Document-View-Signale können zur Implementierung flexibler Document-View-Architekturen verwendet werden. Das Dokument enthält ein Signal, mit dem sich jede der Ansichten verbinden kann. Die folgende Document-Klasse definiert ein einfaches Textdokument, das mehrere Ansichten unterstützt. Beachten Sie, dass ein einzelnes Signal gespeichert wird, mit dem alle Ansichten verbunden werden.

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

Als nächstes können wir anfangen, Ansichten zu definieren. Die folgende TextView-Klasse bietet eine einfache Ansicht des Dokumenttexts.

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

Ich habe einen Code, den ich vor langer Zeit geschrieben habe. ich wollte einen baum in der richtigen reihenfolge durchlaufen (linker knoten, wurzelknoten, rechter knoten) und wann immer ich zu einem komme Node ich wollte in der lage sein, eine beliebige funktion aufzurufen damit es alles kann.

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