Startsignale

Die frei verfügbare Boost-Bibliothek lässt das Herz manches C++-Programmierers höherschlagen, da sie dringend benötigte und häufig vermisste Funktionen zur Verfügung stellt. Dieser zweite Tutorial-Teil spannt den Bogen von generalisierten Callback-Funktionen über Threads bis hin zur Signal/Slot-Programmierung und zum Umgang mit Zeit-Ausdrücken.

vorlesen Druckansicht
Lesezeit: 26 Min.
Von
  • Dr. RĂĽdiger Berlich
Inhaltsverzeichnis

Thema des ersten Tutorial-Teils war die Boost.Bind-Bibliothek. Sie erlaubt die Zuordnung von Werten zu beliebigen Argumenten von Funktionen oder Funktionsobjekten sowie die automatische Erzeugung passender Funktionsobjekte. Die gemeinsame Verwendung von Boost.Bind mit der Boost.Function-Bibliothek (Header <boost/function.hpp>) bietet mehr als die Summe der Teile.

Boost.Function implementiert eine generalisierte Callback-Infrastruktur und ermöglicht es zudem, Funktionen und Funktionsobjekte wie Variablen zur späteren Verwendung zu speichern oder als Argument zu übergeben. Eine weitere Besonderheit besteht darin, dass der Benutzer nicht mehr zwischen Funktionsobjekten und -zeigern unterscheiden muss. Ein boost::function-Objekt mit einer bestimmten Signatur ist kompatibel mit allen funktionsartigen Entitäten mit derselben Signatur. Die Signatur ergibt sich aus dem Rückgabewert und den Argumenten.

Zur Erläuterung von Boost.Function soll die Klasse functionTester dienen (Listing 1). Alle Benutzerinteraktionen erfolgen über die beiden Methoden der Klasse - das Beispiel verzichtet daher der Einfachheit halber auf Konstruktor und Destruktor. Beide erzeugt der Compiler automatisch. Die Aufgabe der Klasse ist, mit einer gegebenen Methode einen String auszudrucken. Die Ausgabefunktion selber ist nicht Teil der Klasse, sondern wird mit der Methode functionTester::setMessenger() durch den Benutzer registriert. setMessenger() erhält ein boost::function-Objekt als Argument. Als Template-Argumente übergibt das Programm dem function-Objekt den gewünschten Rückgabewert (in diesem Fall einfach void) sowie ein oder mehrere Funktionsargumente (hier eine konstante Referenz auf einen std::string).

Mehr Infos

Listing 1: Die Klasse functionTester

class functionTester
{
public:
void printMessage(const string& msg){
if(f_)
f_(msg);
else
cout << "No callback registered yet" << endl;
}

void setMessenger(boost::function<void (const string&)> f){
f_ = f;
}
private:
boost::function<void (const string&)> f_;
};

Bei boost::function<void (const string&)> f handelt es sich um ein herkömmliches Objekt, das man entsprechend behandeln kann. Das Beispielprogramm speichert es einfach in einer privaten Variable f_ desselben Typs zur späteren Verwendung. Da das Programm keine Referenz übergeben hat, kopiert es das boost::function-Objekt. Der Umgang mit dieser Klasse ist also nicht viel komplizierter als etwa mit einem Integer-Wert.

Mitteilungen soll die functionTester::printMessage()-Funktion ausgeben. Sie erhält einen std::string als Argument und übergibt es an das gespeicherte boost::function-Objekt zur Auswertung. Natürlich muss hierbei eine gewisse Fehlerprüfung erfolgen - schließlich kann es sein, dass noch keine Funktion registriert wurde. Da boost::function implizit in einen Bool-Wert umgewandelt werden kann, ist dies einfach. Das Beispielprogramm ruft f_(msg) genau dann auf, wenn es in f_ eine passende Funktion gespeichert hat. Ist dies nicht der Fall, gibt es eine Fehlermeldung aus.

Mit den passenden Funktionen und Funktionsobjekten lässt sich die Klasse functionTester ausprobieren. Eine einfache Funktion ohne Rückgabewert void freeMessenger(const string& msg){} sowie eine Klasse messenger erfüllen diesen Zweck (Listing 2).

Mehr Infos

Listing 2: freeMessenger-Funktion und messenger-Klasse

void freeMessenger(const string& msg){
cout << "Free messenger says: " << msg << endl;
}
class messenger
{
public:
void operator()(const string& msg){
messagePrinter(msg, 1);
}
void messagePrinter(const string& msg, int mode){
if(mode==1)
cout << "Function object says: " << msg << endl;
else
cout << "Member function says: " << msg << endl;
}
};

freeMessenger() sollte selbsterklärend sein. Die Klasse messenger implementiert mit messenger::operator()(const string&) ein Funktionsobjekt. Die Ausgabe des Strings erfolgt in der Memberfunktion freeMessenger::messagePrinter().

Um zwischen dem Aufruf über den operator() und dem direkten Aufruf zu unterscheiden, erhält sie neben dem String ein zusätzliches Argument int mode, mit dem sie den jeweiligen Modus anzeigen kann.

Listing 3 zeigt, wie man die einzelnen Komponenten in main() zusammenbaut. Zunächst legt es einen std::vector von shared_ptr-Objekten an, die auf functionTester-Objekte zeigen. Diesen Container füllt es dann mit functionTester-Objekten. Die folgenden Ausführungen demonstrieren vier verschiedene Fälle.

Mehr Infos

Listing 3: main()

int main(int argc, char **argv){
typedef vector<shared_ptr<functionTester> > ftType;
ftType ftContainer;
for(unsigned int i=0; i<4; ++i){
shared_ptr<functionTester> p(new functionTester);
ftContainer.push_back(p);
}
// Registering a function pointer
ftContainer[0]->setMessenger(&freeMessenger);
// Registering a function object
ftContainer[1]->setMessenger(messenger());
// Registering a member function with partial argument binding
messenger Messenger;
ftContainer[2]->setMessenger(
boost::bind(&messenger::messagePrinter,
&Messenger,_1, 2)
);
// Position 3 intentionally left empty
ftType::iterator it;
for(it=ftContainer.begin(); it!=ftContainer.end(); ++it)
(*it)->printMessage("Hello World");
return 0;
}

Zur shared_ptr-Klasse hat bereits der erste Teil des Tutorials Erläuterungen gegeben. Es handelt sich hierbei um eine Smartpointer-Klasse aus Boost. Sie erspart es dem Entwickler, am Ende der Programmausführung mit delete die dynamisch allozierten Objekte zu löschen.

Mehr Infos

Anlässlich eines Leserkommentars (siehe Leserbriefe Seite 8 der Druckausgabe) sei an dieser Stelle darauf hingewiesen, dass die durch den shared_ptr vorgenommene Allozierung von Objekten in der Form shared_ptr<functionTester> p(new functionTester); anstelle von functionTester *f = new functionTester();shared_ptr<functionTester> p(f); erfolgen sollte (vgl. Listing 3 dieses Artikels; Listing 2 des ersten Tutorial-Teils handhabt dies anders). Der Grund dafür, die Semantik aus Listing 3 zu wählen, sind Ausnahmen, die ein Programm beispielsweise bei der Allozierung des Objekts werfen kann. Für weitere Regeln im Umgang mit shared_ptr sei auf den Abschnitt „Best Practices“ der Dokumentation verwiesen (siehe „Onlinequellen“ [b]).

functionTester::setMessenger() registriert einen Funktionszeiger auf die freie freeMessenger-Funktion. Es folgt ein Funktionsobjekt (messenger() erzeugt dabei ein temporäres Objekt der Klasse messenger). Im dritten Fall erzeugt boost::bind ein Funktionsobjekt, in dem ein direkter Aufruf von functionTester::messagePrinter() erfolgt. Damit die Memberfunktion dies weiß, muss der passende Wert (2) an das mode-Argument gebunden werden. Hier zeigt sich wieder, wie nützlich boost::bind ist. Dem vierten und letzten functionTester-Objekt wird bewusst keine Funktion zugewiesen.

Als letzte Aufgabe bleibt, die vier printMessage()-Funktionen der im Container gespeicherten functionTester-Objekte aufzurufen. Die Ausgabe des Programms sieht so aus:

Free messenger says: Hello World
Function object says: Hello World
Member function says: Hello World
No callback registered yet

Offensichtlich hat das Programm einen Funktionspointer und zwei unterschiedliche Funktionsobjekte erfolgreich als Argumente an functionTester::setMessenger() übergeben und in einer privaten Variablen der Klasse gespeichert. Dies ermöglicht Benutzern wie Bibliotheksdesignern eine bislang nicht dagewesene Freiheit.

Im vierten functionTester-Objekt ist keine Funktion gespeichert. printMessage() fängt diesen Fehler ab, indem es die Fähigkeit des boost::function-Objekts nutzt, sich in einen Bool-Wert wandeln zu lassen. Hier zeigt sich wieder das durchdachte Design der Bibliothek.

Boost.Bind und Boost.Function besitzen noch eine Schwesterbibliothek: Boost.Lambda. Sie soll hier nur gestreift werden. Die aus Lisp und Python bekannte Technik der anonymen Funktionen (auch Lambda-Funktionen genannt) erlaubt es, am Ort eines Aufrufs Funktionen ohne Namen zu erstellen. Wenn Funktionen nur an einer Stelle im Programmtext vorkommen, kann man so die Komplexität einer Anwendung reduzieren. Boost implementiert diese Möglichkeit nun auch für C++. Bemerkenswert ist, dass Boost.Lambda ohne Änderungen des C++-Standards auskommt. Der Artikel wird im Folgenden noch mehrfach auf Boost.Function und Boost.Bind eingehen, Boost.Lambda kommt jedoch nicht weiter zur Sprache.

Eine Bibliothek, die von Boost.Function stark profitiert, ist Boost.Thread. Threads sind parallele AusfĂĽhrungseinheiten in einem Programm. Durch die parallele Nutzung etwa verschiedener Prozessoren steigt (im Idealfall) die AusfĂĽhrungsgeschwindigkeit.

Bekannte, portable Thread-Implementierungen sind beispielsweise die Posix Threads (mit einer C-API) oder die Thread-Bibliothek von Trolltechs Qt. Erstere ist unter C++ etwas beschwerlich zu nutzen. Callbacks werden über Funktionspointer realisiert, und das Design kann (aufgrund der Einschränkungen der Programmiersprache C) nicht der unter C++ eigentlich obligatorischen Typsicherheit folgen. Qts Thread-Bibliothek ist recht komfortabel und auch für freie Programme einsetzbar (sofern diese unter der GPL stehen). Nicht jeder mag allerdings seine eigenen Programme ebenfalls unter die GPL stellen oder alternativ eine Qt-Lizenz erwerben. Hier kommt das ebenfalls portable Boost ins Spiel.

Ein Thread muss die Funktion kennen, die er nach seinem Start ausführen soll. In Qt leitet man hierzu eine Klasse von QThread ab und implementiert eine run()-Methode. Obwohl einsichtig, schränkt dieses Design etwas ein. Boost verlässt sich hier voll auf die Boost.Function-Bibliothek und erscheint somit flexibler. Die auszuführenden Methoden werden nach Wahl als Funktionsobjekt oder als -pointer übergeben. Boost.Function kommt mit beiden zurecht. Einen einfachen Fall zeigt Listing 4.

Mehr Infos

Listing 4: Programm mit einem Thread

unsigned int global_store = 0;
const unsigned int MAXCOUNTER = 500;
void counter(unsigned int n = 1){
while(true){
if(global_store > MAXCOUNTER) break;
if(++global_store%100 == 0)
cout << "In counter " << n << ": " << global_store << endl;
}
}

int main(int argc, char **argv){
boost::thread cntThrd1(&counter);
cntThrd1.join();
return 0;
}

Eine Funktion counter() inkrementiert eine globale Variable global_store, bis diese einen Maximalwert erreicht hat. Alle 100 Inkrementierungen gibt sie den aktuellen Status aus. Das Beispiel startet nur einen Thread in main() (genau genommen zwei, da main() weiter aktiv bleibt). Um später verschiedene Threads unterscheiden zu können, erhält counter() ein Argument n mit dem Standardwert 1.

Der Start des Threads erfolgt durch die Instanziierung eines boost::thread-Objekts, dem ein Funktionszeiger auf counter() ĂĽbergeben wird. Um das Funktionsargument muss sich der Programmierer wegen des Standardwerts von 1 an dieser Stelle nicht kĂĽmmern.

main() wartet mit boost::thread::join() auf das Ende der Ausführung von counter(). Ist dies erreicht, beendet sich das Programm. Das Beispiel ist zugegebenermaßen noch nicht sehr aufregend. Es soll jetzt auf mehrere Threads erweitert werden. Dies betrifft zunächst die counter()-Funktion (Listing 5).

Mehr Infos

Listing 5: Erweiterte counter()-Funktion

unsigned int global_store = 0;
const unsigned int MAXCOUNTER = 500;
void counter(unsigned int n = 1){
while(true){
if(global_store > MAXCOUNTER) break;
if(++global_store%100 == 0)
cout << "In counter " << n << ": " << global_store << endl;
}

Hier wird es schon interessanter. Das Programm soll mehr als eine counter()-Funktion gleichzeitig betreiben. Es gibt jedoch eine Reihe von Ressourcen, die von mehreren Funktionen gemeinsam genutzt werden. Offensichtlich ist dies bei der global_store-Variable, die alle Funktionen schreiben sollen. Diesen Zugriff gilt es zu synchronisieren, sonst herrscht Chaos.

Den Zugriff reguliert man deshalb oft mit einem Mutex. Er muss vor jedem Zugriff auf eine gemeinsame Ressource blockiert werden (Boost.Thread sorgt dafür, dass dies sicher vor verschiedenen Ausführungssträngen erfolgen kann). Bei blockiertem Mutex hält die Ausführung eines Threads an. Erst wenn der Mutex wieder freigegeben wurde und der Thread ihn für sich beanspruchen konnte (andere Threads können um diesen Mutex ebenfalls konkurrieren), kann seine Ausführung weitergehen. Halten sich alle Threads an die Konvention, vor dem Zugriff auf eine gemeinsame Ressource einen Lock auf einen Mutex zu setzen, ist die Gefahr gebannt. Dabei sollte allerdings klar sein, dass das Anhalten eines Threads nicht gerade zur Beschleunigung eines Programms beiträgt.

Nicht nur gemeinsame Variablen gilt es zu schützen. Auch Funktionsaufrufe können kritisch sein, wenn diese nicht auf die Verwendung mit Threads zugeschnitten sind. Die geänderte counter()-Funktion macht dies offensichtlich.

Innerhalb der while()-Schleife versucht das Programm zunächst, den Zugriff auf den Mutex cnt_mutex zu erhalten. Wie in Boost üblich, bedient sich das Beispiel dabei der Technik, automatische Variablen die Freigabe einer Ressource übernehmen zu lassen. In diesem Fall gibt das scoped_lock-Objekt automatisch den Mutex frei, wenn es das Ende seiner Lebensdauer erreicht. Um diesen Prozess zu steuern, rahmen geschweifte Klammern den entsprechenden Bereich ein. So entsteht ein eigener Geltungsbereich.

Innerhalb dieses Bereichs prüft das Programm - nachdem es den Zugriff auf den Mutex erlangt hat - zunächst, ob die Zählvariable ihren Maximalwert überschritten hat. Hier zeigt sich übrigens, warum in diesem Fall eine Endlosschleife sinnvoll ist. Eine Abfrage while(global_store < MAXCOUNTER) wäre zwar möglich (das Lesen von global_store außerhalb des Mutex-Schutzes ist unkritisch, da es sich um eine atomare Operation handelt). Jedoch wird der Mutex erst danach in Anspruch genommen. Es gibt also keine Sicherheit, dass beim Zugriff auf global_store nicht ein anderer Thread dessen Wert bereits wieder geändert hat. Alle Schreib- und Lesezugriffe auf die gemeinsam genutzten Ressourcen sollten daher im Schutz des Mutex erfolgen. Aus demselben Grund speichert counter() den aktuellen Wert von global_store in einer lokalen Variablen zur späteren Verwendung.

Der gleichzeitige Zugriff auf std::cout aus mehreren Threads gleichzeitig kann wegen des Caching ĂĽbrigens Probleme verursachen. Auch hier ist der Schutz eines gesperrten Mutex hilfreich.

Mit dieser Infrastruktur lässt sich global_store nun schon aus mehreren Threads gleichzeitig hochzählen. Allerdings ginge dies bei einem Maximalwert von 1000 zu schnell - wahrscheinlich kämen nicht alle Threads zum Zuge.

Daher wird festgelegt, dass der Thread nach der Freigabe des Mutex eine Weile inaktiv ist. Dies wĂĽrde man normalerweise mit der usleep()-Funktion realisieren. Im Regelfall ist sie aber nicht fĂĽr die Benutzung in Threads geeignet. Deshalb kommt hier die Boost.Thread-eigene sleep-Funktion zum Einsatz. Die Berechnung einer Zufallszahl soll verhindern, dass die Threads immer gleich lang schlafen. Hier zeigt sich wieder, dass man bei der Thread-Programmierung meist nicht einfach eine der Standardfunktionen der C-Bibliothek verwenden kann, da diese beispielsweise gelegentlich einen internen Status speichern. Die gemeinsame Verwendung aus mehreren Threads kann dann zu Fehlern fĂĽhren. Der Einfachheit halber verwendet das Beispielprogramm eine frei verfĂĽgbare Implementierung von Zufallszahlen: die Klasse MTRand aus MersenneTwister.h von Richard J. Wagner [c].

Da ein lokales Objekt erzeugt wird, das keine globalen Variablen verwendet, ist dies unkritisch. Als Eingabe für den Zufallszahlengenerator dient der zwischengespeicherte Wert von global_store. MTRand::randInt(1000000) liefert einen Integer-Wert im Bereich [0,1000000], der für die zufällige Wartezeit verwendet werden kann. Leider ist der Umgang mit boost::thread::sleep() etwas umständlich.

Der Start der Threads erfolgt erneut in main() (Listing 6). Im Unterschied zum ersten Beispiel verwendet dieses eine thread_group und fĂĽgt diesem Container fĂĽr Thread-Objekte auf drei verschiedene Weisen Threads hinzu. Auch hier zeigen sich wieder die Vorteile von Boost.Function.

Mehr Infos

Listing 6: Start der Threads

class counter_class{
public:
void operator()(){ counter(2); }
};

int main(int argc, char **argv){
boost::thread_group thrd_grp;
counter_class cnt_class;
boost::function<void (void)> bf_counter = boost::bind(counter, 3);

thrd_grp.create_thread(&counter);
thrd_grp.create_thread(cnt_class);
thrd_grp.create_thread(bf_counter);

thrd_grp.join_all();
return 0;
}

Der erste Thread verwendet einen Funktionspointer auf counter(). Im zweiten Fall kommt ein Funktionsobjekt der Klasse counter_class zum Einsatz. Und schließlich wird mit boost::bind ein boost::function-Objekt initialisiert, das dem dritten Thread als Argument übergeben wird. Die Fälle lassen sich durch Indizes unterscheiden, die Argumente der counter()-Funktion sind. Ein Ausschnitt aus der Ausgabe des Programms sieht aus wie folgt:

In counter function 2: 600
In counter function 3: 700
In counter function 2: 800
In counter function 1: 900

Es ist ĂĽbrigens sinnvoll zu ĂĽberprĂĽfen, ob ĂĽberhaupt eine Thread-Implementierung in Boost zur VerfĂĽgung steht. Dies kann im Kopfteil des Programmtexts geschehen. Wann immer Threads in Boost verfĂĽgbar sind, ist BOOST_HAS_THREADS definiert. Man kann also einfach schreiben:

#ifndef BOOST_HAS_THREADS
#error "Error: No thread support
#endif

Den Rest erledigt in diesem Fall der Präprozessor.

Selbst dieses einfache Beispiel sollte deutlich gemacht haben, dass Thread-Programmierung alles andere als eine triviale Angelegenheit ist. Wer sich dennoch bislang nicht hat abschrecken lassen, kann ĂĽber die Boost-Webseite [a] und ein Online-Tutorial [d] weitere Anregungen zur Thread-Programmierung mit Boost sammeln. Die Webseiten bieten darĂĽber hinaus eine Reihe von Hilfestellungen zu diesem Thema.

Besonders erwähnenswert erscheint die threadpool-Bibliothek von Philipp Henkel [f]. Sie ist auf die Verwendung mit Boost abgestimmt, hat aber noch keinen Eingang in die Kernsammlung gefunden. Dem Namen entsprechend implementiert threadpool das Thread Pool Pattern. Die Aufgabe von threadpool kommt damit der eines Vermittlers (Broker, Dispatcher) gleich: Hat man eine Reihe M an Aufgaben, die durch eine Anzahl N von Threads abgearbeitet werden sollen, so weist threadpool den Threads ihre jeweiligen Aufgaben zu. Hat ein Thread seine Aufgabe beendet, erhält er eine neue aus der Liste noch unbearbeiteter Tasks. Die Anzahl der Aufgaben ist typischerweise deutlich größer als die der Threads.

Je nach Implementierung bedeutet das Erzeugen und Zerstören von Threads einen signifikanten Aufwand. Mit threadpool kann man das vermeiden. Für die Bibliothek bedeutet das zwar einigen Verwaltungsaufwand, allerdings sieht der Benutzer nichts von der Komplexität dieses Vorgehens. Er weist threadpool lediglich Aufgaben zu und überlässt der Bibliothek die Verwaltungsarbeit. Der Kasten „Bewegliche Ziele“ enthält einige weitere Details zu threadpool.

Das Beispiel aus Listing 5 und 6 hätte man mit einem Thread-sicheren Signal/Slot-Mechanismus möglicherweise einfacher implementieren können. Ein solcher steht unter Boost leider (noch) nicht zur Verfügung (auch wenn die Vault-Bibliotheken bereits einen Prototypen enthalten). Ist man jedoch bereit, auf Threads zu verzichten, bietet die Boost.Signals-Bibliothek von Douglas Gregor eine ausgereifte Implementierung des Signal/Slot-Konzepts an. Wiederum kommen dabei die Konzepte von Boost.Function ins Spiel.

Bei dem Signal/Slot-Mechanismus handelt es sich um eine weitere Möglichkeit, Funktionen einer Klasse aus einer anderen heraus aufzurufen (beschrieben als „Senden eines Signals“). Der Aufruf kann auch die Übergabe von Parametern und die Rückgabe von Werten beinhalten.

Das ist an sich nicht revolutionär. Bedeutung erlangt es dadurch, dass der Aufrufer nicht darüber Bescheid wissen muss, welche Funktion sein Signal aktiviert. Das bestimmt der Programmierer außerhalb der aufrufenden und ausführenden Klasse, indem er Signale mit Slots verbindet. Dabei ist es erlaubt, einem Signal mehrere Slots zuzuweisen, sodass ein Signal mehr als eine Zielfunktion aktiviert.

Bekannt geworden ist dieser Mechanismus vermutlich durch Trolltechs Implementierung in der Qt-Bibliothek, die unter anderem der KDE-Oberfläche von Linux zugrunde liegt. Trolltech bemüht hierzu Makros und den Meta Object Compiler moc zusammen mit eigenen Schlüsselwörtern. Dies kommt fast einer Erweiterung des C++-Sprachstandards gleich. Boost kommt ohne solche Umwege aus. Umgekehrt ist aber zu erwähnen, dass Qt in der aktuellen Version durchaus Signal/Slot-Verbindungen zwischen Threads zulässt und (nicht nur) damit wohl flexibler ist.

Ziel dieses Abschnitts ist es, eine Art Time Server zu erstellen. Über dasselbe Signal soll er - je nach Slot - die aktuelle Zeit oder die seit dem 1.1.2001 vergangene ausgeben. Um dies zu bewältigen, zunächst ein Blick auf eine weitere Boost-Komponente - Jeff Garlands date_time-Bibliothek.

Ein einfaches Beispiel demonstriert das Vorgehen (Listing 7, Header: <boost/date_time.hpp>). Zunächst fragt das Programm in Abständen von 0,1 und 0,2 Sekunden nach der aktuellen Zeit, mit einer Auflösung im Mikrosekundenbereich. Die Werte speichert es in ptime-Objekte (Posix Time). Aus ihnen kann man intuitiv die vergangene Zeit berechnen, etwa durch den Aufruf time_duration tx = (t2-t1);.

Mehr Infos

Listing 7: date_time

int main(int argc, char **argv){
cout << "Test verschiedener Zeitabstaende" << endl;
ptime t1 = microsec_clock::local_time();
usleep(100000);
ptime t2 = microsec_clock::local_time();

ptime t3 = microsec_clock::local_time();
usleep(200000);
ptime t4 = microsec_clock::local_time();

time_duration tx = (t2-t1);
time_duration ty = (t4-t3);
cout << "tx=" << tx << ", ty=" << ty << endl;

if(tx >= ty) cout << "tx ist groesser" << endl;
else cout << "ty ist groesser" << endl;

ptime t5(date(2001, Jan, 1));
ptime t6(second_clock::local_time());
cout << "Stunden seit dem 1.1.2001: " << (t6-t5).hours() << endl;

cout << "Zeitintervall aus Stunden, Minuten, Sekunden:" << endl;
time_duration td = hours(17) + minutes(22) + seconds(3);
cout << "h:m:s = " << td.hours() << ":" << td.minutes() << ":" << td.seconds() << endl;
cout << "Gesamt-Sekunden = " << td.total_seconds() << endl;

cout << "Initialisierung mit String \"00:00:00.000\"" << endl;
time_duration empty(duration_from_string("00:00:00.000"));
cout << "total_seconds = " << empty.total_seconds() << endl;
}

time_duration-Objekte lassen sich auch miteinander vergleichen. tx und ty sind beides time_duration-Objekte. Der Vergleich tx >= ty erlaubt eine Aussage darüber, ob tx größer (oder eventuell gleich) ty ist. Die Ausgabe kann in verschiedener Granularität erfolgen, etwa in Stunden, Minuten oder Sekunden. Umgekehrt lässt sich ein time_duration-Objekt zum Beispiel mit Stunden, Minuten oder Sekunden initialisieren.

Die Ausgabe von Listing 7 lautet:

Test verschiedener Zeitabstaende
tx=00:00:00.101221, ty=00:00:00.201106
ty ist groesser
Stunden seit dem 1.1.2001: 60074
Zeitintervall aus Stunden, Minuten, Sekunden:
h:m:s = 17:22:3
Gesamt-Sekunden = 62523
Initialisierung mit String "00:00:00.000"
total_seconds = 0

Die Verwendung von date_time ist so intuitiv und die Dokumentation auf der Boost-Homepage so gut, dass keine weiteren Erläuterungen zu dieser Bibliothek notwendig sein dürften. Daher zurück zum Beispielprogramm.

Zunächst führt es drei kleine Klassen ein: total_time_server, time_difference_server und time_retriever (Listing 8). Die ersten beiden Klassen implementieren Funktionsobjekte, die bei Aufruf einen std:string mit einem Zeitausdruck zurückgeben. Im ersten Fall ist dies die aktuelle Zeit, im zweiten die seit einem als Argument übergebenen Startzeitpunkt vergangene Zeit. Die Funktionsweise beider Klassen sollte nach der Einführung der date_time-Bibliothek verständlich sein.

Mehr Infos

Listing 8: Zwei Time Server

class total_time_server{
public:
string operator()(void){
ptime t1(second_clock::local_time());
return to_simple_string(t1);
}
};
class time_difference_server{
public:
string operator()(string startDate){
ptime t1(from_simple_string(startDate));
ptime t2(second_clock::local_time());
time_duration td = t2-t1;
return to_simple_string(td);
}
};
class time_retriever
{
public:
boost::signal<string (void)> getTime;
boost::signal<string (void),
aggregate_values\<vector<string> > > getAllTimes;
};

time_retriever enthält zwei Signale. Das erste - getTime - ist zur Speicherung eines einzelnen Slots (also der Zielfunktion) gedacht. Die Syntax sollte nach der Einführung in Boost.Function zu Beginn dieses Artikels klar sein.

Das zweite Signal ist fĂĽr die gleichzeitige Speicherung verschiedener Slots gedacht. Beim Aufruf dieses Signals stellt sich die Frage, wie man die RĂĽckgabewerte der Slots verwenden soll. Ohne weitere Festlegung ist das der RĂĽckgabewert des zuletzt aufgerufenen Slots. Wer will, kann ĂĽber einen weiteren Template-Parameter im Signal getAllTimes selbst festlegen, wie die RĂĽckgabewerte zu verwenden sind. Im vorliegenden Fall sollen alle RĂĽckgabewerte (es handelt sich ja um Strings) in einem vector<string> gespeichert werden.

Die Implementierung von aggregate_values<> ist der Dokumentation zu Boost.Signals entlehnt und soll hier nicht weiter besprochen werden. Interessenten finden weitere Informationen auf der Boost-Website [g]. Hier geht es jetzt mit der main()-Funktion des Signal-Beispiels weiter (Listing 9).

Mehr Infos

Listing 9: main() des Signal-Beispiels

int main(int argc, char **argv){
total_time_server tts;
time_difference_server tds;
time_retriever tr;
boost::signals::connection c=tr.getTime.connect(tts);
cout << tr.getTime() << endl;
c.disconnect();
c=tr.getTime.connect(boost::bind<string>(tds,"2001-01-01"));
cout << tr.getTime() << endl;
c.disconnect();
if(!c.connected()) cout << "Connection terminated!" << endl;
boost::signals::connection c1=tr.getAllTimes.connect(1,tts);
boost::signals::connection c2=
tr.getAllTimes.connect(0,boost::bind<string>(tds,"2001-01-01"));
vector<string> results = tr.getAllTimes();
copy(results.begin(), results.end(),
ostream_iterator<string>(cout, " "));
cout << endl;
return 0;
}

Zunächst erfolgt die Instanziierung der Objekte der Signal- und Slot-Klassen. Der Aufruf connect() des Signals verbindet Signal und Slot. Im ersten Fall ist dies einfach, da total_time_server::operator() keine Argumente erwartet. Das Signal getTime des time_retriever-Objekts kann man nun wie eine normale Memberfunktion aufrufen.

Der Aufruf von disconnect() trennt die Verbindung zwischen Signal und Slot wieder. Stattdessen wird der time_difference_server mit dem getTime-Signal verbunden. Da der time_difference_server::operator() ein Argument erwartet (den Offset fĂĽr die Berechnung der Zeitspanne), muss erneut boost::bind aushelfen. Das Vorgehen sollte klar sein: boost::bind erzeugt selbst wieder ein Funktionsobjekt, das time_difference_server::operator() mit dem passenden Argument aufruft. Was nicht passt, wird passend gemacht.

Nach dem erneuten Trennen der Verbindung ĂĽberprĂĽft der connection::connected()-Aufruf, ob noch eine Verbindung existiert. Nach demselben Muster werden beide Slots gleichzeitig mit dem time_retriever::getAllTimes-Signal verbunden. Wie gehabt ist die Aktivierung des Signals identisch mit einem normalen Funktionsaufruf. Anders als zu Beginn ist das Ergebnis aber die AusfĂĽhrung beider Funktionsobjekte. Dank aggregate_values ist das Resultat beider Aufrufe in einem vector<string> gespeichert. Dessen Inhalt wird zur Kontrolle ausgeben. Die Ausgabe von Listing 9 sieht etwa aus wie folgt:

2007-Nov-09 02:35:50
60074:35:50
Connection terminated!
60074:35:50 2007-Nov-09 02:35:50

Hier ist zunächst der separate Aufruf beider Slots ersichtlich, gefolgt von der Trennung der Verbindung. Es folgt der (fast) gleichzeitige Aufruf beider Slots über getAllTimes(). Auch hier lässt sich Boost.Signals intutitiv verwenden.

Neben all dem Lob der Bibliothek soll ein Problem nicht verschwiegen werden. Viele Klassen und Funktionen sind auf Templates aufgebaut, die tief ineinander verschachtelt sind. Selbst kleine Fehler führen so zu Meldungen, für deren Interpretation Erfahrung hilfreich ist. Oft erstreckt sich eine Meldung über mehrere Zeilen, und von diesen Fehlermeldungen kann es Hunderte auf einmal geben. Ähnliches gilt für die Fehlersuche - Template-Klassen geben sich alles andere als zugänglich in einem Debugger.

Allerdings ist dies kein spezifisches Boost-Problem, sondern dasselbe gilt für die STL. Manche Boost-Bibliotheken versuchen jedoch, durch kleine „Fallen“ Fehler bereits zur Übersetzungszeit zu identifizieren. Und das führt zu mehr Meldungen der genannten Art.

Der dritte Teil des Boost-Tutorials wird einen Schwerpunkt auf die Serialisierung von Objekten und auf die Netzwerkkommunikation legen. Boost stellt hierfĂĽr Funktionen zur VerfĂĽgung, die man in der C-Famile sonst nur von Sprachen wie Java und C# kennt.

Dr. RĂĽdiger Berlich
fĂĽhrt am Institut fĂĽr Wissenschaftliches Rechnen des Karlsruhe Institute of Technology (KIT) eine AusgrĂĽndung aus dem Bereich der Parameteroptimierung in verteilten Umgebungen durch.

Mehr Infos

iX-TRACT

  • Die Boost.Function-Bibliothek erleichtert C++-Programmierern den Umgang mit Callbacks, unter anderem dadurch, dass sie nicht mehr zwischen Funktionsobjekten und Zeigern unterscheiden mĂĽssen.
  • Boost.Thread profitiert von Boost.Function und trägt dafĂĽr Sorge, dass mehrere Threads in einem Programm problemlos parallel laufen können.
  • Auch die Boost.Signals-Bibliothek setzt auf die Konzepte von Boost.Function. Der Signal/Slot-Mechanismus erlaubt es, Funktionen einer Klasse aus einer anderen heraus aufzurufen, ohne dass der Aufrufer wissen muss, welche Funktion sein Signal aktiviert.
Mehr Infos

Tutorialinhalt