Kapitel 10. Erweiterte Datenkonzepte und Literale

In diesem Kapitel:

Datenkonzepte werden in C++11 deutlich erweitert. Zum einen führt C++11 neue Ideen wie konstante Ausdrücke, Raw-String-Literale und benutzerdefinierte Literale ein, zum anderen rundet es bestehende Konzepte ab. Dies betrifft die erweiterten PODs, unbeschränkte Unions, streng typisierte Aufzählungstypen, die bessere Unicode-Unterstützung und das neue Nullzeigerliteral nullptr.

Konstante Ausdrücke

Konstante Ausdrücke sind Ausdrücke, die zur Übersetzungszeit evaluiert werden können. Das Konzept der konstanten Ausdrücke wurde in C++11 erweitert. Es umfasst in C++11 Funktionen und benutzerdefinierte Typen.

Statische und dynamische Initialisierung

Den Unterschied zwischen statischer und dynamischer Initialisierung soll Listing 10.1 aufzeigen. Dabei bezeichnet die statische Initialisierung die Initialisierung zur Übersetzungszeit und die dynamische die zur Laufzeit.

constExpression.cpp

Sowohl die Funktion square (Zeile 3) als auch die Funktion squareToSquare (Zeile 4) in Listing 10.1 sind konstante Ausdrücke. Der Beweis wird durch static_assert in den Zeilen 10 und 11 erbracht. Aufrufe werden zur Übersetzungszeit evaluiert, sodass das Ergebnis als Konstante vorliegt. Damit ist es möglich, das Array arrayNew WithConstExpressioFunction in Zeile 19 direkt zu initialisieren. Dies ist mit C++98 nicht möglich, da ein Funktionsaufruf immer ein Aufruf zur Laufzeit ist. Das Array arrayNewWithConstExpression in Zeile 18 wird indirekt über den Funktionsaufruf initialisiert. Dazu ist es notwendig, dass constExpr in Zeile 15 als konstanter Ausdruck definiert wird.

Der Programmlauf ergibt das erwartete Ergebnis aus Abbildung 10.1.

Variablen, Funktionen und benutzerdefinierte Typen

C++11 unterstützt drei Typen von konstanten Ausdrücken: Variablen, Funktionen und benutzerdefinierte Typen. Letztere werden auch benutzerdefinierte Literale genannt. An jeden dieser Typen sind bestimmte strenge Bedingungen geknüpft, damit sie zur Übersetzungszeit evaluiert werden können.

Benutzerdefinierte Literale werden zu konstanten Ausdrücken, wenn sie mit konstanten Ausdrücken aufgerufen werden. Erhalten diese aber ein dynamisches Argument, verhalten sie sich wie gewöhnliche benutzerdefinierte Datentypen und werden zur Laufzeit evaluiert (Listing 10.2).

myDouble.cpp

In Listing 10.2 wird das Literal MyDouble definiert. Dies besitzt zwei konstante Ausdrücke, einen Konstruktor und die Methode getSum() in den Zeilen 8 und 9. Verwenden lässt sich der Datentyp sowohl statisch (Zeile 19) als auch dynamisch (Zeile 28). Der entscheidende Punkt ist, ob die Argumente konstante Ausdrücke sind. Auch der Rvalue 10.5 im Konstruktoraufruf ist zulässig. Da myDynVal in Zeile 27 ein dynamischer Wert ist, kann myDyn nicht als constexpr definiert werden. Die ganze Funktionalität der Klasse MyDouble steht aber zur Verfügung, um ein Objekt zur Laufzeit zu instanziieren. Die Ausgabe ist unabhängig davon, ob der Code zur Übersetzungs- oder zur Laufzeit ausgeführt wird.

unruh.cpp

Aufgabe 10-1

Wie alles begann:

Erwin Unruh (Erwin Unruh, 2002) schrieb 1994 auf dem C++-Standardisierungs-Meeting in San Diego sein berühmtes Primzahlenprogramm, das die Primzahlen zur Übersetzungszeit berechnet. Damit war der Beweis erbracht, dass Berechnungen zur Übersetzungszeit durch Template-Instanziierung ausgeführt werden können. In seinem Buch ist eine leicht modifizierte Form zu sehen, die aktuelle Compiler ausführen können.

Das Ergebnis des Algorithmus ist in den Compiler-Warnungen versteckt. Die wesentlichen Zeilen wurden aus den Compiler-Warnungen herausgefiltert und bringen die ersten Primzahlen bis zur 15 hervor.

unruh.cpp: In Elementfunktion »void Prime_print<i>::f()
[mit int i = 13]«:
unruh.cpp: In Elementfunktion »void Prime_print<i>::f()
[mit int i = 11]«:
unruh.cpp: In Elementfunktion »void Prime_print<i>::f()
[mit int i = 7]«:
unruh.cpp: In Elementfunktion »void Prime_print<i>::f()
[mit int i = 5]«:
unruh.cpp: In Elementfunktion »void Prime_print<i>::f()
[mit int i = 3]«:
unruh.cpp: In Elementfunktion »void Prime_print<i>::f()
[mit int i = 2]«:

Vergleichen Sie die Berechnung der Primzahlen durch konstante Ausdrücke von Daniel Krügler mit der durch Template-Metaprogramming von Erwin Unruh. Welche Technik ist einfacher zu verstehen?

primeNumbers.cpp

Aufgabe 10-2

Entscheiden Sie zur Übersetzungszeit, ob eine gegebene Zahl eine Primzahl ist.

Erwin Unruhs Programm lässt sich mit Template-Metaprogramming deutlich lesbarer schreiben. Versuchen Sie Ihr Glück.

Plain Old Data folgen dem C-Standardlayout. Damit können sie direkt mit den C-Funktionen memcpy und memmove kopiert und verschoben oder auch mit memset initialisiert werden. Ihre Definition ist aber zu restriktiv für C++, daher wurden die Regeln für PODs in C++11 erweitert.

Eine Klasse ist ein POD, wenn sie trivial ist, ein Standardlayout besitzt und alle ihre nicht statischen Datenelemente PODs sind.

trivial

Eine Klasse oder Struktur ist trivial, wenn sie

Standardlayout

Eine Klasse oder Struktur besitzt ein Standardlayout, wenn sie

Um herauszufinden, ob ein Datentyp ein POD ist, hilft Template-Metaprogramming mit der neuen Type-Traits-Bibliothek (Listing 10.5).

isPod.cpp

Die Funktion std::is_pod (Zeilen 29 und 30) der Type-Traits-Bibliothek wird zur Übersetzungszeit ausgewertet und gibt zurück, ob das Template-Argument ein POD ist. Die Ausgabe gibt Aufschluss.

Aufgabe 10-3

Performance zählt.

Es mag verwunderlich erscheinen, warum in einem Buch über das neue C++ die C-Funktionen memcpy, memmove und memset erwähnt werden, werden doch memcyp und memmove durch std::copy und std::move in der STL und memset über Initialisiererlisten-Konstruktoren angeboten. Um an der Performanceschraube zu drehen, greifen die C++-Algorithmen gern auf die C-Algorithmen zurück. Eine typische Anwendung von memcpy im Copy-Algorithmus ist in Kapitel 19 im Abschnitt „Type-Traits“ dargestellt.

Lange Rede, kurzer Sinn: Machen Sie sich mit dem Einsatzgebiet der C-Funktionen memcpy, memmove und memset vertraut.

Unions beherbergen Datentypen, die sich denselben Speicherbereich teilen. Dabei wird die Größe der Union durch die Größe ihres größten Datentyps vorgegeben. Sie spielen eine wichtige Rolle bei der Implementierung von Bibliotheken und Frameworks. Für die Anwendung von Unions gibt es zwei typische Bereiche:

Einschränkungen von C++98 Unions

An die klassischen C++98-Unions sind einige Einschränkungen gebunden, so dürfen sie

Die letzte Einschränkung gilt nicht mehr in C++11. In C++11 ist eine Union erlaubt, die zum Beispiel einen std::string beinhaltet. Dies führt aber dazu, dass die speziellen Elementfunktionen der Union gelöscht werden. Für deren Implementierung hat nun der Programmierer zu sorgen. Werden aus der Union UnionWithString in Listing 10.6, Zeile 14, der Konstruktor und der Destruktor entfernt, zeigt der GCC unmissverständlich einen Fehler an (Abbildung 10.4).

union.cpp

Werden der Standardkonstruktor und der Destruktor für die Union definiert, lässt sich die Union UnionWithString verwenden (Abbildung 10.5).

Die Ausgabe zeigt schön, dass die Länge der Union MemorySizeChar (Zeile 4) bzw. MemorySizeDouble (Zeile 9) in Listing 10.6 von der Größe ihres größten Datentyps abhängt. Während char 1 Byte beansprucht, benötigt double 8 Byte. Ab Zeile 41 wird der Typ UnionWithString verwendet. Zuerst wird der Wert der Variablen uWithString.s ausgegeben, die in der Initialisiererliste des Konstruktors gesetzt wird, danach wird die Variable uWithString.i gesetzt und deren Wert ausgegeben. Beim Wechsel von uWithString.s nach uWithString.i muss explizit der Destruktor in Zeile 45 aufgerufen werden. Genau das Gegenteil ist in Zeile 51 notwendig, wenn uWithString.s mit dem Operator placement-new wieder auf einen Wert gesetzt wird.

Die weiteren Details rund um Unions lassen sich schön im C++ Reference Guide (Kalev, 2004 ) von Danny Kalev nachlesen.

Aufgabe 10-4

Alignment-Unterstützung in C++11.

Die Alleinstellungsmerkmale von Unions beginnen zu bröckeln. Mit C++11 werden zwei neue Schlüsselwörter alignas und alignof eingeführt. Damit lässt sich die Speicherausrichtung für Datentypen setzen (alignas) und ermitteln (alignof). Die Anwendung ist sehr explizit (Stroustrup, 2011) und daher der impliziten Speicherausrichtung mit unbeschränkten Unions vorzuziehen.

Den besten Überblick über das neue Feature geben die zwei Vorschläge von Attila Farkas »Adding Alignment Support to the C++ Programming Language« (Feher, 2002) und von Lawrence Crowl »C and C++ Alignment Compatibility« (Crowl, 2010). Machen Sie sich mit der Alignment-Unterstützung in C++11 vertraut.

Die neuen scoped und streng typisierten Aufzählungstypen (scoped and strongly typed enums) räumen mit drei Problemen der klassischen Aufzählungstypen auf.

Typsicher, scoped und mit definiertem zugrunde liegendem Typ

Der neue streng typisierte Aufzählungstyp enum class Color1 ist einfach erklärt.

Optional kann statt class struct und der zugrunde liegende Typ angegeben und der Enumerator über einen konstanten Ausdruck initialisiert werden.

Das Schlüsselwort class bzw. struct unterscheidet insbesondere die klassischen Aufzählungstypen von den C++11-Aufzählungstypen und drückt es explizit aus, dass die C++11-Aufzählungstypen nur über ihren Bereich adressiert werden können. Wird der zugrunde liegende integrale Typ nicht angegeben, ist int der Default-Typ. Listing 10.10 zeigt Color1 und Color2 im Einsatz.

enum.cpp

Die Ausgabe des Programms in Listing 10.10 in Abbildung 10.6 zeigt, dass Color2 mit 1 Byte deutlich kompakter als Color1 ist, der 4 Bytes beansprucht. Der Grund liegt in der Definition des Aufzählungstyps. Während Color1 (Zeile 3) den Standarddatentyp int verwendet, wird für Color2 (Zeile 9) explizit char spezifiziert. useMe in Zeile 15 verwendet als Typ des Parameters Color2. Schön ist im Funktionskörper der Funktion und der Definition des Enumerators color2Red in Zeile 41 zu sehen, dass nur ein qualifizierter Zugriff auf den Enumerator zulässig ist.

enumAdd.cpp

Aufgabe 10-5

Vergleichen Sie die klassischen mit den neuen Aufzählungstypen.

In Listing 10.11 geschieht ein bisschen Arithmetik mit den Aufzählungstypen. Vergleichen Sie die Anwendung von OldEnum und NewEnum.

Die Anwendung des klassischen Aufzählungstyps OldEnum ist recht einfach. In Zeile 19 kann direkt auf die Elemente des Aufzählungstyps zugegriffen werden, da sie im globalen Namensraum verfügbar sind. Diese Elemente des OldEnum konvertieren implizit nach int, sodass die einzelnen Elemente addiert werden können. Das ist mit NewEnum in Zeile 20 nicht möglich. Hier müssen die Elemente mit NewEnum:: qualifiziert aufgerufen und explizit nach int konvertiert werden.

Aufgabe 10-6

Werden Sie mit den Anwendungsfällen der streng typisierten Aufzählungstypen vertraut.

In der neuen C++11-Standardbibliothek werden die streng typisierten Aufzählungstypen gern für Fehlercodes benutzt. Schauen Sie ihre Anwendung im Sourcecode der STL an.

Die Besonderheit eines Raw-String-Literals ist, dass die in ihm enthaltenen Zeichen nicht interpretiert werden. Damit erlauben es Raw-String-Literale bei regulären Ausdrücken oder auch Pfadangaben, den Backslash »\« direkt zu verwenden, da er keine Fluchtsequenz mehr darstellt.

Ein Raw-String wird in C++11 durch R"(raw string)" definiert. Optional ist auch die Syntax R"Trenner(raw string)Trenner" möglich. An den String-Trenner sind Bedingungen geknüpft. So darf er maximal 16 Zeichen lang sein und weder Leerzeichen noch öffnende »)« oder schließende »)« Klammern oder den Backslash »\« enthalten.

rawString.cpp

Das Programm rawString in Listing 10.12 ergibt ausgeführt das erwartete Ergebnis. Im normalen String (Zeile 8) werden der Tabulator und der Zeilenumbruch bei der Ausgabe korrekt interpretiert. Die zwei Raw-Strings (Zeilen 12 und 16) geben den String unverändert aus. In einem Raw-String kann auch ein Anführungszeichen (") eingebettet und ausgegeben werden.

Aufgabe 10-7

Welche Anwendungsfälle von Raw-String-Literalen fallen Ihnen ein?

Reguläre Ausdrücke oder auch Pfadangaben unter Windows sind typische Anwendungsfälle für Raw-String-Literale. Dies sind aber natürlich nicht alle. Welche weiteren Anwendungsfälle gibt es für Raw-String-Literale?

costumizeRawString.cpp

Aufgabe 10-8

Wie lässt sich ein Raw-String-Literal definieren, das ein (")) enthält?

Angenommen, Ihr regulärer Ausdruck, den Sie als Raw-String-Literal definieren wollen, soll die Zeichenkette (")) enthalten. Dies ist die Zeichenkombination, mit der ein Raw-String-Literal beendet wird. Wie können Sie das Problem lösen?

Definieren Sie ein Raw-String-Literal, das die Zeichenkombination (")) enthält, und geben Sie es aus.

Der zweite neue Typ von String-Literalen sind die Unicode-String-Literale. Zwar besitzt C++98 die sogenannten Wide-Strings, die durch ein L"wide String" eingeleitet werden. Diese haben aber einen entscheidenden Nachteil, sodass sie nicht plattformunabhängig verwendet werden können. Ihre Länge ist nicht exakt spezifiziert.

UTF-8, UTF-16 und UTF-32

C++11 unterstützt die drei Unicode-Kodierungen UTF-8, UTF-16 und UTF-32. Für UTF-16 und UTF-32 wurde C++11 um zwei neue Zeichentypen char16_t und char32_t erweitert.

Durch die Zeichenkombination \u oder \U eingeleitet, können in Unicode-Strings Unicode-Codepunkte eingebettet werden (Zeilen 6, 10 und 14 in Listing 10.13). Dabei muss nach dem \u eine 16-Bit-, hingegen nach dem \U eine 32-Bit-Hexadezimalzahl stehen. Die Unicode-Präfixe u8, u und U oder auch die Wide-String-Präfixe L können mit dem Raw-String-Präfix R kombiniert werden (Zeilen 22, 23 und 24), wobei das Raw-String-Präfix an letzter Stelle folgen muss.

unicodeString.cpp

Konvertierung zwischen den Kodierungen

Für die Konvertierung zwischen den verschiedenen Kodierungen steht das Klassen-Template codecvt, eine sogenannte Fassette (facet), bereit. Das Klassen-Template codecvt verlangt drei Typparameter:

template <class internT, class externT, class stateT>
class codecvt;

Dabei beschreiben die Typparameter die Konvertierung:

  • internT: Zeichentyp für den internen Zeichensatz

  • externT: Zeichentyp für den externen Zeichensatz

  • stateT: Status der Konvertierung

Über die Methoden codecvt::in und codecvt:out wird die Richtung der Konvertierung vorgegeben. Für das Klassen-Template codecvt stehen Spezialisierungen bereit, die die tatsächlich implementierten Konvertierungen spezifizieren. In Tabelle 10.3 und Tabelle 10.4 werden die verschiedenen Konvertierungen dargestellt.

Zuerst die Konvertierungen, die im klassischen C++ möglich sind.

Mit dem neuen Zeichentyp bringt C++11 einige neue Template-Spezialisierungen mit.

wstring_convert wbuffer_convert

Die neuen Klassen-Templates std::wstring_convert und std::wbuffer_convert vollziehen ihre Konvertierung direkt ohne einen Stream oder eine Locale. Beide werden über eine codecvt-Fassette parametrisiert. Dabei wirkt std::wstring_convert auf einem String und std::wbuffer_convert auf einem Byte-Stream-Puffer.

Die nicht so ganz einfachen Details zu IOStreams, Fassetten und Locales lassen sich in dem Standardwerk »C++ IOStreams and Locales« von Angelika Langer und Klaus Kreft nachlesen (Langer & Kreft, 2000).

Aufgabe 10-9

Was jeder Softwareentwickler mindestens und unbedingt über Unicode und Zeichensätze wissen muss (kein Pardon!).

Verbindlicher lässt sich die Übungsaufgabe nicht beschreiben als der Titel des Dokuments »The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets (No Excuses!)« von Joel on Software (Spolsky, 2003). Die deutsche Übersetzung von Hans-Werner Heinzen ist auch verfügbar (Heinzen, 2009).

Aufgabe 10-10

Machen Sie sich nach der Theorie mit der Praxis vertraut.

Es wird anspruchsvoller. C++ besitzt eine Lokalisierungsbibliothek local (locale). Machen Sie sich damit vertraut, wenn Sie Ihr C++-Programm lokalisieren wollen.

C++ kennt viele Literale:

Neu hingegen ist in C++11, dass der Anwender Literale selbst definieren kann. Häufig gewünschte Literale, die sich mit der neuen Syntax umsetzen lassen, sind:

Details

Ein benutzerdefiniertes Literal besteht aus einem C++98-Built-in-Literal ohne Suffix, das mit einem Unterstrich und einem Bezeichner verbunden ist. Es ist nur zulässig, den Bezeichner als Suffix anzuhängen. Literale besitzen keine Längeneinschränkung. Bestehende C++-Literale dürfen nicht neu definiert werden. Diese Literale werden dann von der C++11-Laufzeit auf den Literal-Operator abgebildet, der den Wert aus dem Literal extrahiert und, wie in Listing 10.14 exemplarisch dargestellt, als Distanzobjekt zur Verfügung stellt.

inline und constexpr

Dabei können die Literal-Operatoren inline oder als konstanter Ausdruck mit constexpr deklariert werden, damit sie zur Übersetzungszeit evaluiert werden.

Aufgabe des Anwenders

Die Aufgabe des Anwenders ist es, die speziellen Konvertierungsoperatoren, Literal-Operatoren genannt, zu implementieren. Dabei kann der Wert in raw- oder in cooked-Form von den Literal-Operatoren angenommen werden. In der raw-Form nimmt das Literal sein Argument als const char* entgegen, in der cooked-Form als Zahl. Ein paar Beispiele sollen die Begrifflichkeiten entflechten.

raw- und cooked-Form

Im Gegensatz zu Listing 10.14, in dem das Literal 123.45km in cooked-Form angenommen wird, nimmt der Literal-Operator in Listing 10.15 die Distanzangabe in raw-Form entgegen. Damit steht das Literal im Funktionskörper als const char* zur Verfügung.

C++11 unterstützt Literale für natürliche Zahlen, Fließkommazahlen, Strings und Zeichen. Während die natürlichen Zahlen und Fließkommazahlen in raw- und cooked-Form angenommen werden können, ist das für Strings und Zeichen nur in raw-Form möglich. In der folgenden Tabelle sind die Typen der Argumente abhängig vom Datentyp dargestellt. Dies bildet die Grundlage für den Compiler, die Literal-Operatoren implizit aufzurufen.

Wird sowohl die raw- als auch die cooked-Form für ein Literal definiert, besitzt die raw-Form die höhere Präzedenz. Die raw-Form wird in der Literatur auch als uncooked-Form bezeichnet.

Eine Besonderheit stellen die String-Literale dar. In Listing 10.16 wird die Funktion func (Zeile 1) mit dem String-Literal "myString"_str (Zeile 7) aufgerufen. Dies führt dazu, dass der Literal-Operator (Zeile 3) verwendet wird. Da die Länge des String-Literals 8 ist, wird len implizit auf 8 gesetzt und kann im Konstruktor von std::string (Zeile 4) angewandt werden. Das Ergebnis der Konvertierung ist, dass die Funktion mit einem String-Objekt aufgerufen wird.

Das zugegeben etwas konstruierte Beispiel für die Berechnung von Abständen in Listing 10.18 soll die Vorteile der benutzerdefinierten Literale auf den Punkt bringen. Werden die Literal-Operatoren mit Operatorüberladung geschickt kombiniert, entsteht eine Domain-Specific-Embedded-Language, kurz DSEL.

Im Namensraum Distance in Listing 10.18 wird eine Subsprache in C++ definiert, auf der die Addition und Subtraktion von verschiedenen Längenangaben implementiert sind. Erreicht wird dies durch das Überladen des +- und des -Operators in den Zeilen 8 und 12 und die Literal-Operatoren in den Zeilen 17, 20, 23 und 26, die den numerischen Wert aus dem Literal extrahieren und damit ein Distance-Objekt normiert instanziieren.

Domain-Specific-Embedded-Language

userDefinedLiterale.cpp

Aufgabe 10-11

Implementieren Sie Listing 10.18.

In Listing 10.18 wird die Idee einer Domain-Specific-Language für die Berechnung von Abständen skizziert. Ein Punkt fehlt noch: Die Klasse MyDistance benötigt einen Konstruktor und einen Ausgabeoperator.

nullptr

0 und NULL

Das neue Nullzeiger-Literal nullptr räumt mit der Mehrdeutigkeit der Zahl 0 und dem Makro NULL in C++ auf. Das Problem mit dem Literal 0 ist, dass es abhängig vom Kontext den Nullzeiger ((void*)0) oder die natürliche Zahl 0 bezeichnet. Das Problem an dem C-Makro NULL ist, dass NULL sich in der Regel nach int konvertieren lässt. Dieses Verhalten hängt von der Definition des Makros NULL ab.

Das C++11-Schlüsselwort nullptr besitzt ein eindeutiges Verhalten. Es lässt sich als Zeiger, als Zeiger auf ein Klassenmitglied oder als bool-Wert verwenden. Es kann aber nicht nach int konvertiert werden.

nullptr.cpp

In Listing 10.19 (Zeilen 16 und 18) wird der nullptr nach int* und bool konvertiert. Der in Zeile 20 ausgegebene Wert der booleschen Variablen b ergibt false. Interessanter sind die Ausgaben der Funktion overloadTest (Zeilen 23, 26 und 29). (char*)0 und nullptr werden als char* interpretiert, hingegen 0 als Integer.

Werden die Zeilen 17 und 32 in Listing 10.19 auskommentiert, bricht die Übersetzung des Programms mit dem aktuellen GCC ab, denn einerseits lässt sich der nullptr nicht nach int konvertieren, und andererseits lässt sich NULL sowohl nach char* als auch nach int konvertieren.

nullptrPerfectForwarding.cpp

Aufgabe 10-12

Überprüfen Sie Ihren Sourcecode auf Nullzeiger.

Durchsuchen Sie Ihren Sourcecode auf die Verwendung der Zahl 0 als Nullzeiger und das Makro NULL.

Entscheiden Sie, welche Konsequenzen der Einsatz des neuen nullptr gegenüber dem Einsatz der Zahl 0 oder des Makros NULL als Nullzeiger mit sich bringt.

Aufgabe 10-13

Lassen Sie nullptr mit Perfect Forwarding zusammenarbeiten.

Rufen Sie die die zwei Funktionen overloadTest(char*) und overloadTest(int) aus Listing 4.19 indirekt über Perfect Forwarding auf. Dies ist am einfachsten, wenn Sie createT aus Listing 2.12 als Grundlage nehmen. Prüfen Sie zum Abschluss, ob der direkte Aufruf der overloadTest-Funktionen zum gleichen Ergebnis (Abbildung 10.8 und Abbildung 10.9) führt wie der indirekte Aufruf.