Rückruffunktionen in C ++


303

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

EDIT:
Ich würde gerne ein einfaches Beispiel sehen, um eine Rückruffunktion zu schreiben.


[This] ( thispointer.com/… ) erklärt die Grundlagen der Rückruffunktionen sehr gut und ist leicht verständlich.
Anurag Singh

Antworten:


449

Hinweis: Die meisten Antworten beziehen sich auf Funktionszeiger. Dies ist eine Möglichkeit, eine "Rückruf" -Logik in C ++ zu erreichen, aber derzeit nicht die günstigste, die ich denke.

Was sind Rückrufe (?) Und warum werden sie verwendet (!)

Ein Rückruf ist ein Aufruf (siehe weiter unten), der von einer Klasse oder Funktion akzeptiert wird und zum Anpassen der aktuellen Logik in Abhängigkeit von diesem Rückruf verwendet wird.

Ein Grund für die Verwendung von Rückrufen ist das Schreiben von Generika 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 Rückrufe. Beispielsweise for_eachwendet der Algorithmus 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;
}

Dies kann verwendet werden, um zuerst einen Vektor zu erhöhen und dann zu drucken, indem entsprechende Callables übergeben werden, zum 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 andere Anwendung von Rückrufen ist die Benachrichtigung von Anrufern über bestimmte Ereignisse, die ein gewisses Maß an Flexibilität bei der statischen / Kompilierungszeit ermöglicht.

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

  • Der erste Rückruf wird aufgerufen, wenn ein Funktionswert und der Gradient basierend auf einem Vektor von Eingabewerten erforderlich sind (logischer Rückruf: Funktionswertbestimmung / Gradientenableitung).
  • Der zweite Rückruf wird für jeden Algorithmusschritt einmal aufgerufen und erhält bestimmte Informationen über die Konvergenz des Algorithmus (Benachrichtigungsrückruf).

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 logischen Rückruf bereitgestellt werden. Diese Dinge richtig zu machen, ist eine Aufgabe des Bibliotheksbenutzers und hält die Bibliothek schlank und allgemeiner.

Darüber hinaus können Rückrufe 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, sowie eine Reihe von Funktionen, die Ihr Spielverhalten steuern. Mit Rückrufen können Sie zur Laufzeit (erneut) entscheiden, welche Aktion ausgefü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_presseddie darin gespeicherten Rückrufe actions, um das gewünschte Verhalten zu erhalten, wenn eine bestimmte Taste gedrückt wird. Wenn der Spieler die Schaltfläche zum Springen ändert, kann der Motor anrufen

game_core_instance.update_keybind(newly_selected_key, &player_jump);

und ändern Sie so das Verhalten eines Anrufs auf key_pressed(was die Anrufe player_jump), sobald diese Taste das nächste Mal im Spiel gedrückt wird.

Was sind Callables in C ++ (11)?

Eine formellere Beschreibung finden Sie unter C ++ - Konzepte: Auf cppreference aufrufbar.

Die Rückruffunktionalitä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 können ebenfalls aufgerufen werden, es wird jedoch ü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 "Aufrufen" eines Rückrufs bezieht sich auf die Syntax zum Aufrufen dieser Objekte.
  • X.3 "Verwenden" eines Rückrufs bedeutet die Syntax beim Übergeben von Argumenten an eine Funktion, die einen Rückruf verwendet.

Hinweis: Ab C ++ 17 kann ein Aufruf wie f(...)geschrieben werden, std::invoke(f, ...)der auch den Zeiger auf den Elementfall 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 haben foo:

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

1.1 Schreiben einer Funktionszeiger- / Typnotation

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)

wo ein benannter Funktionszeigertyp 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 usingErklärung gibt uns die Möglichkeit, die Dinge ein wenig lesbarer zu machen, da das typedeffor f_int_tauch wie folgt geschrieben werden kann:

using f_int_t = int(*)(int);

Wo (zumindest für mich) klarer ist, dass f_int_tes sich um den neuen Typalias handelt, ist die Erkennung des Funktionszeigertyps ebenfalls einfacher

Und eine Deklaration einer Funktion unter Verwendung eines Rückrufs vom Funktionszeigertyp 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 Callback-Notation und kompatible Typen

Eine Rückruffunktion, die einen Funktionszeiger verwendet, kann unter Verwendung von Funktionszeigern aufgerufen werden.

Die Verwendung einer Funktion, die einen Funktionszeiger-Rückruf akzeptiert, ist ziemlich 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 davon abhängt, wie der Rückruf funktioniert:

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

verwendet 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 Elementfunktion (einer Klasse C) ist ein spezieller Typ eines (und noch komplexeren) Funktionszeigers, für dessen Bearbeitung ein Objekt vom Typ erforderlich ist C.

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

2.1 Zeiger auf Elementnotation / Typnotation schreiben

Ein Zeiger auf den Elementfunktionstyp für eine Klasse That 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 Elementfunktion - 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 den Rückruf einer Mitgliedsfunktion 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 Ckann in Bezug auf ein Objekt vom Typ aufgerufen werden, Cindem Elementzugriffsoperationen für den dereferenzierten Zeiger verwendet werden. Hinweis: Klammern 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 Cverfügbar ist , ist die Syntax äquivalent (wobei der Zeiger auf ebenfalls Cdereferenziert 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 Callback-Notation und kompatible Typen

Eine Rückruffunktion, die einen Elementfunktionszeiger der Klasse verwendet, Tkann unter Verwendung eines Elementfunktionszeigers der Klasse aufgerufen werden T.

Die Verwendung einer Funktion, die einen Zeiger auf den Rückruf von Elementfunktionen verwendet, ist - analog zu Funktionszeigern - ebenfalls recht 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::functionObjekte (Header<functional> )

Die std::functionKlasse ist ein polymorpher Funktions-Wrapper zum Speichern, Kopieren oder Aufrufen von Callables.

3.1 Schreiben einer std::functionObjekt- / Typnotation

Der Typ eines std::functionObjekts, in dem ein aufrufbarer Objekt 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

Die Klasse std::functionhat operator()definiert, mit welcher ihr 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 Callback-Notation und kompatible Typen

Der std::functionRückruf ist allgemeiner als Funktionszeiger oder Zeiger auf Elementfunktionen, da verschiedene Typen übergeben und implizit in ein std::functionObjekt 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 Mitgliedsfunktion

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::functionObjekt 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::bindAusdrücke

Das Ergebnis eines std::bindAusdrucks 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 des Zeigers auf Elementfunktionen 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 ordnungsgemäßen operator()Überladung können ebenfalls in einem std::functionObjekt 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 zu verwendenden Funktionszeigerbeispiels 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]);
  }
}

gibt dieser Funktion viel mehr Nutzen, weil 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. Templated Callback-Typ

Bei Verwendung von Vorlagen kann der Code, der den Rückruf aufruft, noch allgemeiner sein als bei Verwendung von std::functionObjekten.

Beachten Sie, dass Vorlagen eine Funktion zur Kompilierungszeit und ein Entwurfswerkzeug für den Polymorphismus zur Kompilierungszeit sind. Wenn das dynamische Verhalten zur Laufzeit durch Rückrufe erreicht werden soll, helfen Vorlagen, führen jedoch nicht zur Laufzeitdynamik.

4.1 Schreiben (Typnotationen) und Aufrufen von Rückrufen mit Vorlagen

Eine weitere Verallgemeinerung, dh des std_ftransform_every_intCodes von oben, kann mithilfe 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 (sowie einfachsten) Syntax für einen Rückruftyp, die ein einfaches, abzuleitendes Argument mit Vorlagen 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 Vorlagen-Typ abgeleitet wurde F. Die Implementierung von type_namewird am Ende dieses Beitrags angegeben.

Die allgemeinste Implementierung für die unäre Transformation eines Bereichs ist Teil der Standardbibliothek std::transform, die auch in Bezug auf die iterierten Typen als Vorlage dient.

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 Vorlagenrückrufe und kompatible Typen

Die kompatiblen Typen für die Templated std::functionCallback-Methode stdf_transform_every_int_templsind identisch mit den oben genannten Typen (siehe 3.4).

Bei Verwendung der Vorlagenversion 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, wird fooaber nicht verwendet 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 einfache Vorlagenparameter von transform_every_int_templkann 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 oben verwendet

#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
@ BogyJammer: Falls Sie es nicht bemerkt haben: Die Antwort besteht aus zwei Teilen. 1. Eine allgemeine Erklärung von "Rückrufen" mit einem kleinen Beispiel. 2. Eine umfassende Liste verschiedener aufrufbarer Elemente und Möglichkeiten zum Schreiben von Code mithilfe von Rückrufen. Sie können gerne nicht ins Detail gehen oder die gesamte Antwort lesen, aber nur weil Sie keine detaillierte Ansicht wünschen, ist es nicht so, dass die Antwort unwirksam oder "brutal kopiert" ist. Das Thema ist "c ++ Rückrufe". Auch wenn Teil 1 für OP in Ordnung ist, können andere Teil 2 nützlich finden. Sie können auf den Mangel an Informationen oder konstruktive Kritik für den ersten Teil anstatt auf -1 hinweisen.
Pixelchemist

1
Der Teil 1 ist nicht anfängerfreundlich und klar genug. Ich kann nicht konstruktiver sein, wenn ich sage, dass es mir nicht gelungen ist, etwas zu lernen. Und der Teil 2 wurde nicht angefordert, überflutet die Seite und kommt nicht in Frage, obwohl Sie so tun, als wäre er nützlich, obwohl er häufig in speziellen Dokumentationen zu finden ist, in denen solche detaillierten Informationen überhaupt gesucht werden. Ich halte definitiv die Gegenstimme. Eine einzige Abstimmung stellt eine persönliche Meinung dar. Bitte akzeptieren und respektieren Sie sie.
Bogey Jammer

24
@BogeyJammer Ich bin nicht neu in der Programmierung, aber ich bin neu in "modern c ++". Diese Antwort gibt mir den genauen Kontext, den ich brauche, um über die Rolle von Rückrufen in C ++ nachzudenken. Das OP hat vielleicht nicht nach mehreren Beispielen gefragt, aber es ist bei SO üblich, in einer unendlichen Suche, eine Welt der Narren zu erziehen, alle möglichen Lösungen für eine Frage aufzuzählen. Wenn es zu viel wie ein Buch liest, kann ich nur raten, ein wenig zu üben, indem ich einige davon lese .
dcow

int b = foobar(a, foo); // call foobar with pointer to foo as callback, das ist ein Tippfehler, oder? foosollte ein Zeiger dafür sein, damit AFAIK funktioniert.
Konoufo

@konoufo: [conv.func]des C ++ 11-Standards sagt: " Ein Wert vom Funktionstyp T kann in einen Wert vom Typ" Zeiger auf T "konvertiert werden." Das Ergebnis ist ein Zeiger auf die Funktion. "Dies ist eine Standardkonvertierung und erfolgt daher implizit. Man könnte hier (natürlich) den Funktionszeiger verwenden.
Pixelchemist

160

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 Rückrufe übergeben möchten, haben die Deklarationen an diese Funktionszeiger 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);
}

1
Im Beispiel für die Klassenmethode ist ein Fehler aufgetreten. Der Aufruf sollte sein: (Instanz. * Rückruf) (1.0f)
CarlJohnson

Vielen Dank für den Hinweis. Ich werde beides hinzufügen, um das Aufrufen durch ein Objekt und durch einen Objektzeiger zu veranschaulichen.
Ramon Zarazua B.

3
Dies hat den Nachteil der Funktion std :: tr1:, dass der Rückruf pro Klasse eingegeben wird. Dies macht es unpraktisch, Rückrufe im C-Stil zu verwenden, wenn das Objekt, das den Aufruf ausführt, die Klasse des aufzurufenden Objekts nicht kennt.
Bleater

Wie könnte ich es tun, ohne typedefden Rückruftyp zu verwenden? Ist es überhaupt möglich?
Tomáš Zato - Wiedereinsetzung Monica

1
Ja, du kannst. typedefist nur syntaktischer Zucker, um es lesbarer zu machen. Ohne wäre typedefdie Definition von DoWorkObject für Funktionszeiger : void DoWorkObject(int (*callback)(float)). Für Mitgliederzeiger wären:void DoWorkObject(int (ClassName::*callback)(float))
Ramon Zarazua B.

68

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 ++ - Rückrufe zu schreiben.


1
In welchem ​​Buch gibt SM aus Interesse dieses Beispiel? Prost :)
Sam-W

5
Ich weiß, dass dies alt ist, aber da ich fast damit begonnen habe und es bei meinem Setup (mingw) nicht funktioniert hat, wird diese Methode nicht unterstützt, wenn Sie die GCC-Version <4.x verwenden. Einige der Abhängigkeiten, die ich verwende, lassen sich ohne viel Arbeit in der gcc-Version> = 4.0.1 nicht kompilieren. Daher kann ich keine guten, altmodischen C-ähnlichen Rückrufe verwenden, die einwandfrei funktionieren.
OzBarry

38

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

Dies ist sehr nützlich, um wiederverwendbare Software zu erstellen. Beispielsweise verwenden viele Betriebssystem-APIs (wie 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.


63
Diese Antwort sagt einem durchschnittlichen Programmierer wirklich nichts, was er nicht wusste. Ich lerne C ++, während ich mit vielen anderen Sprachen vertraut bin. Was Rückruf im Allgemeinen ist, geht mich nichts an.
Tomáš Zato - Wiedereinsetzung Monica

17

Die akzeptierte Antwort ist sehr nützlich und ziemlich umfassend. Das OP gibt jedoch an

Ich würde gerne ein einfaches Beispiel sehen , um eine Rückruffunktion zu schreiben.

Also los geht's, ab C ++ 11 sind std::functionkeine Funktionszeiger und ähnliches erforderlich:

#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_hashesmit verschiedenen Implementierungen von Hash-Funktionen aufrufen möchten. Zu diesem Zweck habe ich eine einfache bereitgestellt. Es empfängt eine Zeichenfolge, gibt ein int zurück (einen Hashwert der bereitgestellten Zeichenfolge), und alles, std::function<int (const std::string&)>woran Sie sich aus dem Syntaxteil erinnern müssen, ist , eine solche Funktion als Eingabeargument der Funktion zu beschreiben, die sie aufruft.


Aus all diesen Antworten hat mir klar gemacht, was Rückrufe sind und wie man sie verwendet. Vielen Dank.
Mehar Charan Sahai

@MeharCharanSahai Freut mich zu hören :) Gern geschehen.
Miljen Mikic

9

In C ++ gibt es kein explizites Konzept für eine Rückruffunktion. Rückrufmechanismen werden häufig über Funktionszeiger, Funktorobjekte oder Rückrufobjekte implementiert. Die Programmierer müssen die Rückruffunktion 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 ++ bieten alles, was Sie zum Implementieren von Rückruffunktionen benötigen. Die häufigste und trivialste Methode zum Implementieren einer Rückruffunktion besteht darin, einen Funktionszeiger als Funktionsargument zu übergeben.

Rückruffunktionen und Funktionszeiger sind jedoch nicht gleichbedeutend. 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 für verschiedene Gartenarten 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. Zu sagen, dass etwas eine Rückruffunktion ist, impliziert eine überdurchschnittliche Trennung zwischen der aufrufenden Funktion und der aufgerufenen spezifischen Funktion, eine lockerere konzeptionelle Kopplung zwischen dem Anrufer und dem Angerufenen, wobei der Anrufer die explizite Kontrolle darüber hat, was aufgerufen wird.

In der .NET-Dokumentation für IFormatProvider heißt es beispielsweise, dass "GetFormat eine Rückrufmethode ist" , obwohl es sich nur um eine gewöhnliche Schnittstellenmethode handelt. Ich glaube nicht, dass irgendjemand argumentieren würde, dass alle virtuellen Methodenaufrufe Rückruffunktionen sind. Was GetFormat zu einer Rückrufmethode macht, ist nicht die Mechanik, wie es übergeben oder aufgerufen wird, sondern die Semantik des Aufrufers, der die GetFormat-Methode des Objekts auswählt.

Einige Sprachen enthalten Funktionen mit expliziter Rückrufsemantik, die sich normalerweise auf Ereignisse und die Ereignisbehandlung beziehen. Zum Beispiel hat C # den Ereignistyp mit Syntax und Semantik, die 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.

C und C ++ hingegen binden das semantische Konzept der Rückruffunktionen nicht annähernd so explizit ein. Die Mechanismen sind da, die integrierte Semantik nicht. Sie können Rückruffunktionen problemlos implementieren, aber um etwas Anspruchsvolleres zu erhalten, das eine explizite Rückrufsemantik enthält, müssen Sie es auf dem aufbauen, was C ++ bietet, beispielsweise was Qt mit seinen Signalen und Slots gemacht hat .

Kurz gesagt, C ++ bietet alles, was Sie zum Implementieren von Rückrufen benötigen, häufig recht einfach und trivial mithilfe von Funktionszeigern. Was es nicht gibt, sind Schlüsselwörter und Funktionen, deren Semantik für Rückrufe spezifisch ist, wie z. B. Erhöhen , Ausgeben , Handles , Ereignis + = usw. Wenn Sie aus einer Sprache mit diesen Elementtypen stammen, wird die native Rückrufunterstützung in C ++ unterstützt wird sich kastriert fühlen.


1
Zum Glück war dies nicht die erste Antwort, die ich beim Besuch dieser Seite gelesen habe, sonst hätte ich sofort einen Sprung gemacht!
Ubugnu

6

Rückruffunktionen sind Teil des C-Standards und daher auch Teil von C ++. Wenn Sie jedoch mit C ++ arbeiten, würde ich empfehlen , stattdessen das Beobachtermuster zu verwenden: http://en.wikipedia.org/wiki/Observer_pattern


1
Rückruffunktionen sind nicht unbedingt gleichbedeutend mit der Ausführung einer Funktion über einen Funktionszeiger, der als Argument übergeben wurde. Nach einigen Definitionen enthält der Begriff Rückruffunktion die zusätzliche Semantik, einen anderen Code über etwas zu benachrichtigen, das gerade passiert ist, oder es ist an der Zeit, dass etwas passiert. Aus dieser Perspektive ist eine Rückruffunktion nicht Teil des C-Standards, kann jedoch einfach mithilfe von Funktionszeigern implementiert werden, die Teil des Standards sind.
Darryl

3
"Teil des C-Standards, daher auch Teil von C ++." Dies ist ein typisches Missverständnis, aber dennoch ein Missverständnis :-)
Begrenzte Versöhnung

Ich muss zustimmen. Ich werde es so lassen, wie es ist, da es nur dann mehr Verwirrung stiften wird, wenn ich es jetzt ändere. Ich wollte damit sagen, dass Funktionszeiger (!) Teil des Standards sind. Etwas anderes zu sagen - da stimme ich zu - ist irreführend.
AudioDroid

Inwiefern sind Rückruffunktionen "Teil des C-Standards"? Ich denke nicht, dass die Tatsache, dass es Funktionen und Zeiger auf Funktionen unterstützt, bedeutet, dass es Rückrufe spezifisch als Sprachkonzept kanonisiert. Außerdem wäre das, wie erwähnt, für C ++ nicht direkt relevant, selbst wenn es korrekt wäre. Und es ist besonders nicht relevant, wenn das OP gefragt hat, wann und wie Rückrufe in C ++ verwendet werden sollen (eine lahme, zu breite Frage, aber dennoch), und Ihre Antwort ist eine reine Link-Ermahnung, stattdessen etwas anderes zu tun.
underscore_d

4

Siehe die obige Definition, in der angegeben wird, dass eine Rückruffunktion an eine andere Funktion übergeben und irgendwann aufgerufen wird.

In C ++ ist es wünschenswert, dass Rückruffunktionen eine Klassenmethode aufrufen. Wenn Sie dies tun, 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 für die Leser 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 im temporären Speicherplatz speichert, und eine andere, die die Daten nachbearbeitet. Mit einer Rückruffunktion kann der Rückruf diese für jede gelesene Datenzeile verarbeiten. Diese Technik reduziert den Overhead des erforderlichen temporären Platzes. Dies ist besonders nützlich für SQL-Abfragen, die eine große Datenmenge zurückgeben, die dann 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);
}

0

Mit den Signalen2 von Boost2 können Sie generische Mitgliedsfunktionen (ohne Vorlagen!) Und threadsicher abonnieren.

Beispiel: Mit Dokumentenansichtsignalen können flexible Dokumentansichtsarchitekturen implementiert werden. Das Dokument enthält ein Signal, mit dem jede der Ansichten eine Verbindung herstellen 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 beginnen, Ansichten zu definieren. Die folgende TextView-Klasse bietet eine einfache Ansicht des Dokumenttextes.

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

Die akzeptierte Antwort ist umfassend, bezieht sich aber auf die Frage, die ich hier nur als einfaches Beispiel anführen möchte. Ich hatte einen Code, den ich vor langer Zeit geschrieben hatte. Ich wollte einen Baum in der richtigen Reihenfolge durchlaufen (linker Knoten, dann Wurzelknoten, dann rechter Knoten) und wann immer ich zu einem Knoten komme, wollte ich in der Lage sein, eine beliebige Funktion aufzurufen, damit er 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);
}

1
Wie beantwortet es die Frage?
Rajan Sharma

Sie wissen, dass akzeptierte Antworten korrekt und umfassend sind und ich denke, dass es im Allgemeinen nichts mehr zu sagen gibt. Aber ich poste ein Beispiel für meine Verwendung von Rückruffunktionen.
Ehsan Ahmadi
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.