Funktionsobjekte
Wie kann man eigentlich Algorithmen der Standardbibliothek noch flexibler gestalten? Eine mögliche Antwort lautet: Funktionsobjekte. C++ bietet die Möglichkeit, Objekte zu bauen, die sich aufrufen lassen wie Funktionen. Alles, was man dafür tun muss, ist die Überladung des richtigen Operators. Damit geht dann alles Mögliche: Übergabe als Parameter an andere Funktionen, Speichern von Kontext etc.
Video
Code
|
|
Erklärung
In der C++-Standardbibliothek gibt es einige Anwendungsfälle, wo die vorhandenen Algorithmen Informationen von außen
brauchen. C++11 bietet bspw. Algorithmen wie std::copy_if
oder std::remove_if
, die eine Bedingung erwarten, mit der sie
entscheiden können, ob ein Objekt zu kopieren oder entfernen ist. Ebenso braucht std::sort
ein Sortierkriterium. Im
Standardfall verwendet der Algorithmus einfach den operator<()
des ValueTypes der gegebenen Iteratoren. Das muss aber
nicht immer passen. Manchmal möchte man nach ganz anderen Kriterien sortieren oder man hat für den gegebenen ValueType
gar
keinen operator<()
definiert. Zu dem Zweck bietet der Algorithmus an, eine Vergleichsfunktion zu übergeben, die eine
einfache Frage beantwortet: wenn ich zwei Objekte a
und b
habe, kommt a
vor b
?
Nun bietet C++ erstmal nicht die Möglichkeit, Funktionen als Parameter zu übergeben (es gibt zwar Funktionspointer, aber
darüber schweigen wir hier mal grad). Funktionen sind schlicht keine first-class-citizen im Typsystem, wie das in anderen
Sprachen, wie Python oder LISP der Fall ist. Dafür bietet C++ was anderes: man kann ein beliebiges Objekt wie eine Funktion
aufrufen, indem man für den entsprechenden Typ den operator()
implementiert und ihn so zum Funktionsobjekttyp macht.
Objekte kann man dann ja wieder beliebig durch die Gegend reichen, mit Zustand anreichern oder in Datenstrukturen speichern.
Dort, wo sie dann gebraucht werden, werden sie einfach wie Funktionen aufgerufen.
Im Beispiel oben ist die Klasse CompareWrapper
ein Funktionsobjekt. Verantwortlich dafür ist der in Zeile 18 zu findende
operator()
(Achtung: doppelte Klammer! Die erste, leere Klammer gehört zum Operatornamen, die zweite enthält die
eigentliche Parameterliste. Ein Operator mit ohne Parameter sähe also so aus: operator()()
) der einen Funktionsaufruf mit
zwei Parametern implementiert. Erzeugen wir also ein Objekt der Klasse, bspw. CompareWrapper c
, dann können wir dieses
Objekt als Funktion mit zwei Parametern aufrufen: c(param1, param2)
. Tatsächlich aufgerufen wird dann der entsprechenden
Operator.
Von diesem Aufruf Gebrauch macht std::sort
: das Objekt, welches als dritter Parameter übergeben wird, muss ein
Funktionsobjekt sein, das man mit zwei zum ValueType
der gegebenen Iteratoren (in unserem Fall Wrapper
) kompatiblen
Objekten aufrufen kann. Im Beispiel ist diese Anforderung durch die beiden Wrapper const &
erfüllt. std::sort
erwartet
als Antwort ein true
, wenn das erste übergebene Objekt vor dem zweiten kommen muss und ansonsten ein false. Die Antwort
muss eine Halbordnung definieren, sprich: wenn a < b
gilt, und b < c
, dann muss auch a < c
gelten. b < a
, c < a
und c < b
dürfen hingegen nicht gelten. Eigentlich logisch, aber speziell wenn man eigene Vergleiche für Objekte mit
mehreren Feldern implementiert, dann kann das schonmal leicht chaotisch werden. std::sort
läuft zwar auch dann, wenn diese
Bedingungen nicht erfüllt sind (es kann sie ja eh nicht prüfen), man sollte allerdings vom Ergebnis nicht überrascht sein.
Der Aufruf von std::sort
ist dann ganz einfach: wir übergeben – wie so oft bei den Algorithmen – die Bereichsgrenzen in
Form von Iteratoren und als dritten Parameter das Vergleichsobjekt. Der Algorithmus arbeitet dann in-place, d.h. die Objekte
des Bereichs sind sortiert, nachdem der Aufruf zurückkehrt. Zusätzlicher Platz wird nicht benötigt (was für große Bereiche
durchaus interessant ist).