Hallo zusammen,

in diesem Post möchte ich euch etwas erzählen, über das Entkoppeln von Objekten durch Callbacks mit C++-Interfaces oder boost::function und boost::bind. Der Post ist diesmal als PDF Datei eingebunden. Hintergrund ist, dass das PDF einem Workshop entstammt, den ich im November 2010 zu eben diesem Thema in unserer Firma gehalten habe. Als Beispiel sind zwei Klassen vorgegeben: eine Klasse A und eine Klasse B. Klasse A inkludiert die Klasse B und benutzt diese. Im ersten Szenario muss die Klasse B von der Klasse A nichts wissen. Im zweiten Szenario muss die Klasse B auch die Klasse A zum Zwecke eines Rückrufs kennen. Dazu benutzt sie einen Zeiger auf A, was diverse Probleme mit sich bringt. In den folgenden Lösungsansätzen wird aufgezeigt, wie mithilfe von C++ – Interfaces oder boost::bind und boost::function die Klasse B von der Klasse A entkoppelt werden kann. In einem weiteren Szenario wird gezeigt, wie man mithilfe von Vorwärtsdeklaration, dem Verwenden eines Zeigers in der Deklaration (anstatt einer kompletten Klasse) dem inkludieren der Klassendeklaration in der CPP-Datei und dem Anlegen des verwendeten Objekts im Konstruktor und dem Zerstören des Objekts im Destruktor, die Include-Abhängigkeiten nach oben terminiert werden können, was sich wiederum positiv auf die Übersetzungszeiten auswirkt. Im Anschluss daran folgen noch ein paar weitere Beispiele zu verzögerten Funktionsaufrufen mithilfe von boost::function und boost::bind. Für Tipps, Korrekturen und konstruktive Kritik (, natürlich aber auch für Zuspruch :-)) bin ich sehr dankbar. Viel Spaß mit dem PDF:

c++_entkoppeln_durch_interfaces_oder_boost_funktions_zeiger

Gruß,

Daniel

Callbacks sind nützlich, um Module voneinander zu entkoppeln. Die Module können miteinander kommunizieren, ohne voneinander zuviel wissen zu müssen. Kennen sich kolaborierende Klassen, leidet die Veränderbarkeit und Wartungsfreundlichkeit des Codes. Die Komplexität steigt und mit ihr dann auch die Zahl der Programmfehler. Auch die Wiederverwendbarkeit leidet unter “verzahntem” Code. Eine Möglichkeit der Entkoppelung besteht in der Verwendung von C++-Interfaces. Die Module kennen dann nur das zu verwendende Interface und die zu benutzede Addresse, sind aber nicht abhängig von dem dahinter stehenden Implementierer (der konkreten Klasse). Bei Verwendung der hier vorgestellten Methodik kennen die Module voneinder nur die zu verwendenden Signaturen und die Addresse, wo diese aufzurufen sind. Nicht abhängig sind diese Module dann von dem gerade zu benutzenden Besitzer, der diese Signatur zur verfügung stellt.

Mit boost::function und boost::bind kann man relativ einfach verzögerte Funktionsaufrufe realisieren. Nützlich sind diese für die gerade genannten Callbacks. Als Vorlage habe ich mir die mir am wichtigsten erscheinenden Fälle hier zusammengetragen:

Freie Funktionen

1
2
3
4
5
6
7
8
namespace free_functions
{
  void f_1()
  {
    boost::format f("%1%: called with args: void. It returned: void.");
    f % __FUNCTION__;
    std::cout << f.str();
  }

Diese freie Funktion kann man mit einem Objekt vom Type boost::function zu einem späteren Zeitpunkt (Callback) dann wie folgt aufrufen:

9
10
11
12
// Anlegen. Hier auf dem Stack. Für Objekte dann als member:
boost::function<void (void)> fn(boost::bind(&free_functions::f_1));
// Später zum Event-Zeitpunkt dann der Aufruf:
fn();

Als Template-Parameter (Zeile 11) übergibt man die Signatur der aufzurufenden Funktion. Hier: Return=void, 1.Argument=void. bind wird als Argument die Addresse der aufzurufenden Funktion übergeben (Zeile 11). Es baut dann den Funktor, den das function-object speichert. Über den überladenen Klammer-Operator kann dann die Funktion indirekt aufgerufen werden (Zeile 12).

Für eine freie Funktion mit Rückgabewert und Parameter der Art …

13
14
15
16
17
18
19
20
21
22
namespace free_functions
{
  int  f_2(double d, const std::string& s)
  {
    const int result = 1;
    boost::format f("%1%: called w. args: %2%, %3%. Returned: %4%");
    f % __FUNCTION__ % d % s % result;
    std::cout << f.str();
    return result;
  }

… sieht der Code zum Erzeugen und Aufrufen dann so aus:

13
14
15
16
17
18
19
20
21
22
23
// Erzeugen
boost::function<int (double, std::string)>
fn(
  boost::bind(
    &free_functions::f_2,
    _1, // bind-Platzhalter für ersten Parameter
    _2  // bind-Platzhalter für zweiten Parameter
));
 
// Der Aufruf:
const int result = fn(42.0,"hello world");

_1 ist ein Platzhalter für bind. Er weist bind an für die letzendlich aufzurufende Funktion als ersten Parameter den ersten Parameter des Funktor-Aufrufes, also dem überladenen Klammer-Operator des boost::function-Objektes, zu benutzen. Ist im vorliegenden Beispiel der double-Wert 42.0. Entsprechendes gillt für den zweiten Parameter. Die Reihenfolge bei bind entspricht der Reihenfolge der Parameter der auzurufenden Funktion, die Nummer des bind-Platzhalters der Reihenfolge der Parameter bei Aufruf des boost::function-Objektes. Definiert sind die Platzhalter _1 – _9. Ein Ändern der Reihenfolge oder das Nichtbeachten von Parametern ist möglich, so lange die Typen der Argumente zumindest konvertierbar sind:

24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
namespace free_functions
{
 // Argumente in der Reihenfolge austauschbar:
 void f_3(int a1, int a2, int a3)
  {
    boost::format f("%1% called. a1: %2%, a2: %3%, a3: %4%.");
    f % __FUNCTION__ % a1 % a2 % a3;
    std::cout << f.str() << std::endl;
  }
}
 
...
 
boost::function
fn_normal_order(
  boost::bind(
  &free_functions::f_3,
  _1,_2,_3));
 
boost::function
fn_mixed_order(
   boost::bind(
   &free_functions::f_3,
   _3,_2,_1));
 
fn_normal_order(1,2,3); // 1 2 3
fn_mixed_order(1,2,3);  // 3 2 1
fn_normal_order(1,2,"3");// (!) Fehler beim kompilieren

Member Funktionen

All das gerade Gesagte gillt auch für Member Funktionen von Objekten. Hier muß zusätzlich (natürlich) noch die Adresse des Objektes, bzw. die zu verwendende Instanz mitangegeben werden:

52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
namespace classes
{
  class A
  {
  public:
    ...
    // Definition der Methode:
    int  f_2(double d, const std::string& s)
    {
      const int result = 1;
      boost::format f(
        "%1% called. instance-addr: 0x%2%. d: %3%, s: %4%. return: %5%");
      f % __FUNCTION__ % this % d % s % result;
      std::cout << f.str() << std::endl;
      return result;
    }
 
    ...
 
// Anlegen der Instanz:
classes::A a;
 
...
 
// Definition des Callback-Objektes:
boost::function<int (double,std::string)> fn(
  boost::bind(
    &classes::A::f_2, // Methoden-Adresse
    &a, // Zusätzlich hier die zu verwendende Instanz mit angeben.
    _1,
    _2
));
 
...
 
// Irgendwann der indirekte Aufruf von a.f_2(...):
int result = fn(1.0,"hello member function");

In anderen Sprachen nennt man solche Funktionszeiger auf Memberfunktionen bestimmer Instanzen “Delegates”.

Source

Ein vc++2008-projekt mit dem Beispielprogramm kann hier downgeloded werden.
Boost-Version: 1.42 aufwärts. Proj.C++.Additional Include Directories:
Pfad zu boost angeben, Proj.Linker.General.Additional Library Directories:
Pfad zu boost\stage\lib angeben.

play_with_boost_function_vc++2008_proj

Ausblick

In einem weiteren Post werde ich noch aufzeigen, wie man mit boost::bind eine Member-Funktion von verschiedenen Instanzen einer Klasse in einer STL-Collection aufrufen kann.

Bis zum nächsten Post,

Daniel