Effektives modernes C++
Von Scott Meyers
()
Über dieses E-Book
Scott Meyers
For more than 20 years, Scott Meyers’ Effective C++ books (Effective C++, More Effective C++, and Effective STL) have set the bar for C++ programming guidance. His clear, engaging explanations of complex technical material have earned him a worldwide following, and they keep him in demand as a trainer, consultant, and conference presenter. Winner of the 2009 Dr. Dobb’s Excellence in Programming Award, he has a Ph.D. in Computer Science from Brown University. His web site is aristeia.com.
Ähnlich wie Effektives modernes C++
Ähnliche E-Books
C++-Standardbibliothek - kurz & gut Bewertung: 0 von 5 Sternen0 BewertungenC++ – kurz & gut: Aktuell zu C++17 Bewertung: 4 von 5 Sternen4/5C# 10 – kurz & gut Bewertung: 0 von 5 Sternen0 BewertungenC++11 für Programmierer: Den neuen Standard effektiv einsetzen Bewertung: 0 von 5 Sternen0 BewertungenEffective Java: Best Practices für die Java-Plattform Bewertung: 0 von 5 Sternen0 BewertungenAlgorithmen: Grundlagen und Implementierung Bewertung: 0 von 5 Sternen0 BewertungenDSL mit Xtext/Xtend. Luecken(x)text Bewertung: 0 von 5 Sternen0 BewertungenProgrammieren in TypeScript: Skalierbare JavaScript-Applikationen entwickeln Bewertung: 0 von 5 Sternen0 BewertungenRuby Pakete 100 Stöße: Eine Stunde Meisterklasse, Ausgabe 2024 Bewertung: 0 von 5 Sternen0 BewertungenJavaScript und TypeScript für C#-Entwickler Bewertung: 0 von 5 Sternen0 BewertungenDocker: Software entwickeln und deployen mit Containern Bewertung: 0 von 5 Sternen0 BewertungenDSL mit Xtext/Xtend. 4GL mit externem Quellcode Bewertung: 0 von 5 Sternen0 BewertungenJavaScript für Anfänger: Die Top 100 Essentials Bewertung: 0 von 5 Sternen0 BewertungenREST und HATEOAS Bewertung: 0 von 5 Sternen0 BewertungenDynamic Proxies: Effizient programmieren Bewertung: 0 von 5 Sternen0 BewertungenAufsetzen, Testen und Betrieb einer Android-App Bewertung: 0 von 5 Sternen0 BewertungenClojure: Funktionale Programmierung für die JVM Bewertung: 0 von 5 Sternen0 BewertungenPatterns kompakt: Entwurfsmuster für effektive Softwareentwicklung Bewertung: 0 von 5 Sternen0 BewertungenSprachenkompendium: Vala, Go und Rust Bewertung: 0 von 5 Sternen0 BewertungenJava 9 Streams Bewertung: 0 von 5 Sternen0 BewertungenProgrammieren lernen mit A++: Funktional programmieren in Python und Java Bewertung: 0 von 5 Sternen0 BewertungenKompaktkurs C# 7 Bewertung: 0 von 5 Sternen0 BewertungenJava - Der umfassende Programmierkurs Bewertung: 0 von 5 Sternen0 BewertungenPrinzipien des Softwaredesigns: Entwurfsstrategien für komplexe Systeme Bewertung: 0 von 5 Sternen0 BewertungenLearnPasLin: Lernen der UR Programmiersprache aller Geeks.. Bewertung: 0 von 5 Sternen0 BewertungenOpenLaszlo: schnell + kompakt Bewertung: 0 von 5 Sternen0 BewertungenPatterns kompakt: Entwurfsmuster für effektive Software-Entwicklung Bewertung: 3 von 5 Sternen3/5MQL: Eine hierarchische Abfragesprache mit TypeScript erstellen Bewertung: 0 von 5 Sternen0 BewertungenA++ Die kleinste Programmiersprache der Welt: Eine Programmiersprache zum Erlernen der Programmierung Bewertung: 0 von 5 Sternen0 BewertungenSoftware Development Trends: Wegweisende Beiträge für eine neue IT: Wegweisende Beiträge für eine neue IT Bewertung: 0 von 5 Sternen0 Bewertungen
Programmieren für Sie
Das große Python3 Workbook: Mit vielen Beispielen und Übungen - Programmieren leicht gemacht! Bewertung: 4 von 5 Sternen4/5JavaScript kurz & gut Bewertung: 3 von 5 Sternen3/5Algorithmen: Grundlagen und Implementierung Bewertung: 0 von 5 Sternen0 BewertungenDie ultimative QNAP NAS Bibel - Das Praxisbuch - mit vielen Insider Tipps und Tricks - komplett in Farbe Bewertung: 0 von 5 Sternen0 BewertungenLinux Grundlagen - Ein Einstieg in das Linux-Betriebssystem Bewertung: 0 von 5 Sternen0 BewertungenPython | Schritt für Schritt Programmieren lernen: Der ultimative Anfänger Guide für einen einfachen & schnellen Einstieg Bewertung: 0 von 5 Sternen0 BewertungenRichtig einsteigen: Excel VBA-Programmierung: Für Microsoft Excel 2007 bis 2016 Bewertung: 0 von 5 Sternen0 BewertungenProgrammieren für Einsteiger: Teil 1 Bewertung: 0 von 5 Sternen0 BewertungenPython kurz & gut: Für Python 3.x und 2.7 Bewertung: 3 von 5 Sternen3/5PowerShell: Anwendung und effektive Nutzung Bewertung: 5 von 5 Sternen5/5GitHub – Eine praktische Einführung: Von den ersten Schritten bis zu eigenen GitHub Actions Bewertung: 0 von 5 Sternen0 BewertungenVBA-Programmierung für Word, Excel und Access: Das Praxisbuch für Microsoft-Office-Entwickler Bewertung: 0 von 5 Sternen0 BewertungenRaspberry Pi: Mach's einfach: Die kompakteste Gebrauchsanweisung mit 222 Anleitungen. Geeignet für Raspberry Pi 3 Modell B / B+ Bewertung: 0 von 5 Sternen0 BewertungenEigene Spiele programmieren – Python lernen: Der spielerische Weg zur Programmiersprache Bewertung: 0 von 5 Sternen0 BewertungenEinstieg in Google Go Bewertung: 0 von 5 Sternen0 BewertungenC++: Kurzportträt einer zeitlosen Sprache Bewertung: 0 von 5 Sternen0 BewertungenProgrammieren lernen mit Python 3: Schnelleinstieg für Beginner Bewertung: 0 von 5 Sternen0 BewertungenPython-Tricks: Praktische Tipps für Fortgeschrittene Bewertung: 3 von 5 Sternen3/5Der Weg zum Python-Profi: Ein Best-Practice-Buch für sauberes Programmieren Bewertung: 0 von 5 Sternen0 BewertungenHacken mit Python und Kali-Linux: Entwicklung eigener Hackingtools mit Python unter Kali-Linux Bewertung: 0 von 5 Sternen0 BewertungenPython Crashkurs: Eine praktische, projektbasierte Programmiereinführung Bewertung: 0 von 5 Sternen0 BewertungenSoftwareentwicklungsprozess: Von der ersten Idee bis zur Installation Bewertung: 0 von 5 Sternen0 BewertungenDas Excel SOS-Handbuch: Wie sie Excel (2010-2019 & 365) schnell & einfach meistern. Die All-in-One Anleitung für ihren privaten & beruflichen Excel-Erfolg! Bewertung: 0 von 5 Sternen0 BewertungenHTML5 für Mobile Web Bewertung: 0 von 5 Sternen0 BewertungenAndroid-Entwicklung für Einsteiger - 20.000 Zeilen unter dem Meer: 2. erweiterte Auflage Bewertung: 0 von 5 Sternen0 BewertungenSQL – kurz & gut Bewertung: 0 von 5 Sternen0 BewertungenEinstieg in TypeScript: Grundlagen für Entwickler Bewertung: 0 von 5 Sternen0 BewertungenHacking mit Python: Fehlersuche, Programmanalyse, Reverse Engineering Bewertung: 0 von 5 Sternen0 BewertungenLinux Befehlsreferenz: Schnelleinstieg in die Arbeit mit der Konsole, regulären Ausdrücken und Shellscripting Bewertung: 0 von 5 Sternen0 BewertungenDie Serverwelt von Node.js Bewertung: 0 von 5 Sternen0 Bewertungen
Rezensionen für Effektives modernes C++
0 Bewertungen0 Rezensionen
Buchvorschau
Effektives modernes C++ - Scott Meyers
Kapitel 1. Typen ableiten
In C++98 gab es genau einen Regelsatz für die Typableitung: den für Funktions-Templates. C++11 passt diesen Regelsatz ein wenig an und fügt zwei weitere hinzu – einen für auto und einen für decltype. C++14 erweitert dann die Anwendungsbereiche für auto und decltype. Die immer weiter gehende automatische Typableitung befreit Sie von der Tyrannei, Typen hinschreiben zu müssen, die offensichtlich oder redundant sind. C++-Software lässt sich dadurch besser anpassen, da das Ändern eines Typs an einer Stelle im Quellcode durch die Typableitung automatisch dafür sorgt, dass dies auch an anderen Stellen wirksam wird. Allerdings kann es auch schwieriger werden, Code zu analysieren, da die von den Compilern ermittelten Typen nicht immer so offensichtlich sind, wie Sie es sich vielleicht erhoffen.
Ohne ein solides Verständnis der Typableitung ist effektives Programmieren in modernem C++ so gut wie unmöglich. Es gibt einfach zu viele Gelegenheiten, bei denen Typableitung geschieht: in Aufrufen von Funktions-Templates, in den meisten Situationen mit auto, in decltype-Ausdrücken und – mit C++14 – beim Einsatz des mysteriösen decltype(auto).
In diesem Kapitel finden Sie die Informationen zur Typableitung, die jeder C++-Entwickler kennen muss. Es beschreibt, wie die Typableitung bei Templates funktioniert, wie auto darauf aufbaut und wie decltype seinen eigenen Weg geht. Zudem wird sogar erklärt, wie Sie Ihren Compiler dazu zwingen, die Ergebnisse seiner Typableitungen anzuzeigen, sodass Sie prüfen können, ob er so vorgeht, wie Sie es sich vorgestellt haben.
Technik 1: Typableitung beim Template
Wenn die Anwender eines komplexen Systems sich nicht darum scheren, wie es funktioniert – solange sie mit dem Ergebnis zufrieden sind – sagt das viel über das Design des Systems aus. Daran gemessen ist die Template-Typableitung in C++ ausgesprochen erfolgreich. Millionen von Programmierern haben Argumente erfolgreich an Template-Funktionen übergeben, obwohl die meisten von Ihnen höchstens in sehr groben Zügen erklären könnten, wie die von diesen Funktionen genutzten Typen ermittelt werden.
Wenn Sie sich auch zu dieser Gruppe zählen, habe ich eine gute und eine schlechte Nachricht für Sie. Die gute Nachricht ist, dass die Typableitung für Templates die Grundlage für eines der überzeugendsten Features in modernem C++ ist: auto. Waren Sie bisher zufrieden damit, wie in C++ Typen für Templates ermittelt wurden, werden Sie auch mit der Typableitung via auto in C++11 glücklich werden. Die schlechte Nachricht ist, dass beim Anwenden der Regeln zur Template-Typableitung auf auto manchmal weniger intuitive Ergebnisse herauskommen. Aus diesem Grund ist es wichtig, die Aspekte der Template-Typableitung wirklich zu verstehen, auf die auto baut. In dieser Technik werden Ihnen die notwendigen Informationen dazu vermittelt.
Wenn Sie nichts gegen ein bisschen Pseudocode haben, können wir uns ein Funktions-Template wie folgt vorstellen:
templateT> void f(ParamType param);
Ein Aufruf kann dann so aussehen:
f(expr); // f mit einem Ausdruck aufrufen
Während des Kompilierens nutzt der Compiler expr, um zwei Typen abzuleiten: einen für T und einen für ParamType. Diese Typen sind meist unterschiedlich, weil ParamType häufig noch Ausschmückungen wie const oder Referenz-Qualifier enthält. Ist das Template zum Beispiel so deklariert:
template
und haben wir diesen Aufruf:
int x = 0;
f(x); // Aufruf mit int
dann wird T als int ermittelt, während ParamType ein const int& ist.
Es ist nur natürlich, davon auszugehen, dass der für T ermittelte Typ der gleiche ist wie der Typ des an die Funktion übergebenen Arguments – also dass T dem Typ von expr entspricht. Im obigen Beispiel ist das auch der Fall: x ist ein int, und T wird als int abgeleitet. Aber das funktioniert nicht immer so. Der Typ, der für T ermittelt wird, hängt nicht nur vom Typ von expr ab, sondern auch noch von der Form von ParamType. Es gibt drei Fälle:
ParamType ist ein Zeiger- oder Referenztyp, aber keine universelle Referenz. (Universelle Referenzen werden in „Technik 24: Unterscheiden Sie zwischen universellen Referenzen und Rvalue-Referenzen" beschrieben. Hier müssen Sie nur wissen, dass es sie gibt und dass sie nicht dasselbe sind wie Lvalue- oder Rvalue-Referenzen.)
ParamType ist eine universelle Referenz.
ParamType ist weder ein Zeiger noch eine Referenz.
Wir haben also drei Szenarien bei der Typableitung, die wir uns anschauen wollen. Jedes Szenario wird dabei auf unserer allgemeinen Form für Templates aufbauen:
templateT> void f(ParamType param);
f(expr); // T und ParamType aus expr ableiten
Fall 1: ParamType ist eine Referenz oder ein Zeiger, aber keine universelle Referenz
In der einfachsten Situation ist ParamType ein Referenz- oder Zeigertyp, aber keine universelle Referenz. In diesem Fall funktioniert die Typableitung so:
Ist der Typ von expr eine Referenz, ignoriere den Referenz-Teil.
Dann vergleiche den Typ von expr per Mustererkennung mit ParamType, um T zu ermitteln.
Schauen wir uns zum Beispiel dieses Template an:
template
Dazu diese Variablendeklarationen:
int x = 27; // x ist ein int. const int cx = x; // cx ist ein const int. const int& rx = x; // rx ist eine Referenz auf x als const int.
Die abgeleiteten Typen für param und T sind dann wie folgt:
f(x); // T ist int, param ist int&.
f(cx); // T ist const int,
// param ist const int&.
f(rx); // T ist const int,
// param ist const int&.
Beachten Sie beim zweiten und dritten Aufruf, dass cx und rx const-Werte sind, daher wird T als const int abgeleitet und der Parametertyp wird zu const int&. Das ist für Aufrufer wichtig. Übergeben sie ein const-Objekt an einen Referenzparameter, erwarten sie, dass das Objekt unverändert bleibt, der Parameter also eine Referenz auf const ist. Darum ist es sicher, ein const-Objekt an ein Template zu übergeben, dass einen Parameter T& erwartet: Die »constheit« des Objekts wird Teil des Typs, der für T abgeleitet wird.
Im dritten Beispiel ist beachtenswert, dass der Typ von rx zwar eine Referenz ist, T aber trotzdem als Nicht-Referenz abgeleitet wird. Das liegt daran, dass die »Referenzheit« von rx beim Bestimmen des Typs ignoriert wird.
Diese Beispiele enthalten alle Lvalue-Referenzparameter, aber die Typableitung funktioniert genauso bei Rvalue-Referenzparametern. Natürlich können nur Rvalue-Argumente an Rvalue-Referenzparameter übergeben werden, aber diese Einschränkung hat nichts mit der Typableitung zu tun.
Ändern wir den Typ des Parameters von f von T& in const T&, ändert sich das Ergebnis ein bisschen – allerdings ohne große Überraschungen. Die constheit von cx und rx wird weiterhin beachtet, aber weil wir jetzt davon ausgehen, dass param eine Referenz auf ein const ist, muss const nicht länger als Teil von T abgeleitet werden:
template
int x = 27; // wie zuvor const int cx = x; // wie zuvor const int& rx = x; // wie zuvor
f(x); // T ist int, param ist const int&.
f(cx); // T ist int, param ist const int&.
f(rx); // T ist int, param ist const int&.
Wie vorher wird die Referenzheit von rx während der Typableitung ignoriert.
Wäre param statt einer Referenz ein Zeiger (oder ein Zeiger auf const), würde das Ganze gleich ablaufen:
template
int x = 27; // wie zuvor const int *px = &x; // px ist ein Zeiger auf x als const int.
f(&x); // T ist int, param ist int*.
f(px); // T ist const int,
// param ist const int*.
Vermutlich haben Sie zum Schluss nicht mehr genau gelesen, denn die Typableitungsregeln von C++ funktionieren für Referenz- und Zeigerparameter so selbstverständlich, dass sie in niedergeschriebener Form wirklich langweilig sind. Alles ist so offensichtlich! Aber das ist ja auch genau das, was Sie bei einer automatischen Typableitung haben wollen.
Fall 2: ParamType ist eine universelle Referenz
Bei Templates mit universellen Referenzparametern ist es nicht mehr ganz so offensichtlich. Solche Parameter werden wie Rvalue-Referenzen deklariert (das heißt, in einem Funktions-Template mit dem Typ-Parameter T wird ein Typ für eine universelle Referenz als T&& deklariert), aber das Verhalten ist anders, wenn Lvalue-Werte übergeben werden. Die ganze Geschichte erzähle ich in „Technik 24: Unterscheiden Sie zwischen universellen Referenzen und Rvalue-Referenzen", aber hier sind schon einmal die wichtigsten Punkte:
Ist expr ein Lvalue, werden sowohl T als auch ParamType als Lvalue-Referenzen abgeleitet. Das ist doppelt unerwartet. Zum einen ist es die einzige Situation bei der Template-Typableitung, in der T als Referenz abgeleitet wird. Zum anderen ist Param-Type zwar mit der Syntax für eine Rvalue-Referenz deklariert, der Typ wird aber trotzdem als Lvalue-Referenz ermittelt.
Ist expr ein Rvalue, gelten die »normalen« Regeln (aus Fall 1).
Zum Beispiel:
template
int x = 27; // wie zuvor const int cx = x; // wie zuvor const int& rx = x; // wie zuvor
f(x); // x ist ein Lvalue, daher ist T int&,
// param ist auch int&.
f(cx); // cx ist ein Lvalue, daher ist T const int&,
// param ist auch const int&.
f(rx); // rx ist ein Lvalue, daher ist T const int&,
// param ist auch const int&.
f(27); // 27 ist ein Rvalue, daher ist T int,
// param ist daher int&&.
In „Technik 24: Unterscheiden Sie zwischen universellen Referenzen und Rvalue-Referenzen" wird ausführlich erklärt, warum diese Beispiele die beschriebenen Ergebnisse liefern. Entscheidend ist hier, dass sich die Regeln zur automatischen Typableitung für universelle Referenzparameter von denen für Lvalue- oder Rvalue-Referenzparameter unterscheiden. Insbesondere unterscheidet die Typableitung bei universellen Referenzen zwischen Lvalue- und Rvalue-Argumenten. Das passiert niemals bei nichtuniversellen Referenzen.
Fall 3: ParamType ist weder ein Zeiger noch eine Referenz
Ist ParamType weder ein Zeiger noch eine Referenz, arbeiten wir per Wertübergabe (Pass-by-Value):
template
param enthält dann eine Kopie des übergebenen Werts – ein ganz neues Objekt. Dadurch wird auch die Regel beeinflusst, wie T aus expr abgeleitet wird:
Wie zuvor gilt: Ist der Typ von expr eine Referenz, wird der Referenz-Teil ignoriert.
Ist expr nach dem Ignorieren der Referenzheit noch const, wird auch das ignoriert. Ebenso, falls exprvolatile ist. (volatile-Objekte kommen selten vor. Sie werden meist nur beim Implementieren von Gerätetreibern eingesetzt. Details dazu finden Sie in „Technik 40: Verwenden Sie std::atomic in Concurrency-Situationen und volatile für spezielle Speicherbereiche".)
Daher:
int x = 27; // wie zuvor const int cx = x; // wie zuvor const int& rx = x; // wie zuvor
f(x); // T und param sind beide vom Typ int.
f(cx); // T und param sind beide vom Typ int.
f(rx); // T und param sind immer noch vom Typ int.
Beachten Sie: Obwohl cx und rx const-Werte repräsentieren, ist param nicht const. Das ist durchaus sinnvoll. param ist ein Objekt, des vollständig unabhängig von cx und rx ist – eine Kopie von cx oder rx. Die Tatsache, dass cx und rx nicht verändert werden können, sagt nichts darüber aus, ob dies bei param auch der Fall ist. Darum wird bei expr eine constheit (und auch eine volatileheit) ignoriert, wenn ein Typ für param abgeleitet wird: Nur weil expr nicht verändert werden kann, heißt das nicht, dass eine Kopie davon ebenso konstant bleiben muss.
Es ist wichtig, sich zu merken, dass const (und volatile) nur bei Wertübergaben ignoriert wird. Wie wir gesehen haben, wird bei Referenz- oder Zeiger-const-Parametern die constheit von expr bei der Typableitung bewahrt. Aber stellen Sie sich jetzt den Fall vor, in dem expr ein const-Zeiger auf ein const-Objekt ist und expr an einen By-Value-param übergeben wird:
template
const char* const ptr = // ptr ist ein const-Zeiger auf ein const-Objekt.
Spaß mit Zeigern
;
f(ptr); // Argument vom Typ const char * const übergeben
Hier deklariert das const auf der rechten Seite des Sterns ptr als const: ptr kann nicht auf einen anderen Ort zeigen oder auf null gesetzt werden. (Das const links vom Stern sagt, dass das, worauf das ptr zeigt – der String – const ist und daher nicht verändert werden kann.) Wird ptr an f übergeben, werden die Bits des Zeigers nach param kopiert. Der Zeiger selbst (ptr) wird also als Wert übergeben. Entsprechend der Regeln der Typableitung für Werteparameter wird die constheit von ptr ignoriert, und der für param ermittelte Typ wird const char* sein – also ein veränderbarer Zeiger auf einen const-String. Die constheit dessen, worauf ptr zeigt, wird bei der automatischen Typableitung bewahrt, aber die constheit von ptr selbst wird beim Kopieren in den neuen Zeiger param verworfen.
Array-Argumente
Damit sind die meisten Fälle behandelt, die bei der Typableitung vorkommen können. Es gibt aber einen Spezialfall, den Sie kennen sollten. Array-Typen unterscheiden sich nämlich von Zeigertypen, auch wenn sie manchmal austauschbar scheinen. Das liegt vor allem daran, dass sich in vielen Situationen ein Array in einen Zeiger auf sein erstes Element per Decay umwandeln lässt. Damit wird Code wie der im folgenden Beispiel kompilierbar:
const char name[] = J. P. Briggs
; // Typ von name ist
// const char[13].
const char * ptrToName = name; // Das Array wird zu einem Zeiger.
Hier wird der const char*-Zeiger ptrToName mit name initialisiert, einem const char[13]. Diese Typen (const char* und const char[13]) sind nicht gleich, aber aufgrund der Array-nach-Zeiger-Decay-Regel lässt sich der Code kompilieren.
Was geschieht aber, wenn ein Array an ein Template als Werteparameter übergeben wird? Was passiert dann?
template
f(name); // Welche Typen werden für T und param abgeleitet?
Wir beginnen mit der Beobachtung, dass es kein Array als Funktionsparameter gibt. Ja, ja, die Syntax ist erlaubt:
void myFunc(int param[]);
Aber die Array-Deklaration wird als Zeigerdeklaration behandelt. myFunc könnte auch so deklariert werden:
void myFunc(int* param); // gleiche Funktion wie oben
Diese Äquivalenz von Array- und Zeigerparametern resultiert aus den C-Wurzeln von C++. Wegen ihr glauben viele, Array- und Zeigertypen seien das Gleiche.
Da Deklarationen von Array-Parametern so behandelt werden, als handele es sich um Zeigerparameter, wird der Typ eines an eine Template-Funktion als By-Value übergebenen Parameters als Zeigertyp ermittelt. Bei einem Aufruf des Templates f wird dessen Typparameter T daher als const char* abgeleitet:
f(name); // name ist Array, aber T wird zu const char*.
Aber jetzt kommt die Überraschung: Funktionen können zwar keine Parameter als echte Arrays deklarieren, aber es ist ihnen möglich, Parameter zu deklarieren, die Referenzen auf Arrays sind! Wenn wir also das Template f so anpassen, dass es sein Argument als Referenz übernimmt,
template
und wir dann ein Array übergeben,
f(name); // Array an f übergeben
ist der für T abgeleitete Typ tatsächlich der Typ des Arrays! Dazu gehört auch dessen Größe. Daher wird T in diesem Beispiel zu const char [13], und der Typ des Parameters von f (eine Referenz auf dieses Array) ist const char (&)[13]. Ja, die Syntax sieht verboten aus, aber das Wissen darüber schindet Eindruck (falls sich Ihr Gegenüber dafür interessieren sollte).
Interessanterweise können Sie durch die Fähigkeit, Referenzen auf Arrays zu deklarieren, ein Template erstellen, das die Anzahl der Elemente in einem Array ermittelt:
// Größe eines Arrays als beim Kompilieren konstante Größe. (Der // Array-Parameter hat keinen Namen, weil wir uns nur für die // Anzahl der Elemente interessieren.)
template
return N; // und } // noexcept
Wie in „Technik 15: Verwenden Sie nach Möglichkeit immer constexpr" beschrieben ist, sorgt das Deklarieren dieser Funktion als constexpr dafür, dass das Ergebnis schon zum Zeitpunkt des Kompilierens zur Verfügung steht. Damit lässt sich zum Beispiel ein Array mit der gleichen Anzahl an Elementen wie bei einem gegebenen Array deklarieren, dessen Größe aus einer Initialisierungsliste mit geschweiften Klammern berechnet wird:
int keyVals[] = { 1, 3, 7, 9, 11, 22, 35 }; // keyVals hat
// 7 Elemente
. int mappedVals[arraySize(keyVals)]; // genauso wie
// mappedVals
Natürlich bevorzugen Sie als moderner C++-Entwickler ein std::array gegenüber einem eingebauten Array:
std::arrayarraySize(keyVals)> mappedVals; // mappedVals
// mit Größe 7
Die Deklaration von arraySize als noexcept hilft dem Compiler, besseren Code zu erzeugen. Details dazu finden Sie in „Technik 14: Deklarieren Sie Funktionen als noexcept, wenn sie keine Exceptions auslösen werden".
Funktionsargumente
Arrays sind nicht die einzigen Elemente in C++, die sich in Zeiger verwandeln können. Funktionstypen können per Decay zu Funktionszeigern werden, und alles, was wir zur Typableitung für Arrays geschrieben haben, gilt auch für die Typableitung von Funktionen und ihre Umwandlung in Funktionszeiger. Als Ergebnis:
void someFunc(int, double); // someFunc ist eine Funktion,
// Typ ist void(int, double).
template
template
f1(someFunc); // param als ptr-to-func bestimmt,
// Typ ist void (*)(int, double). f2(someFunc); // param als ref-to-func bestimmt,
// Typ ist void (&)(int, double).
Das macht in der Praxis nur sehr selten einen Unterschied, aber wenn Sie schon etwas über die Array-nach-Zeiger-Umwandlung wissen, sollten Sie auch über die Funktionnach-Zeiger-Umwandlung informiert sein.
Das sind sie also – die mit auto in Zusammenhang stehenden Regeln für die Template-Typableitung. Ich habe am Anfang geschrieben, dass sie ziemlich einfach sind, und für den größten Teil stimmt das auch. Die Sonderbehandlung bei Lvalues beim Ermitteln von Typen für universelle Referenzen stört dieses schöne Bild ein wenig, und die Regeln für Arrays und Funktionen zum Umwandeln in Zeiger sorgen ebenfalls für Unruhe. Manchmal wollen Sie dann doch vermutlich einfach Ihren Compiler schütteln und ihn anschreien: »Sag mir, was für einen Typ du abgeleitet hast!« Wenn das geschiet, schauen Sie sich „Technik 4: Zeigen Sie abgeleitete Typen an" an, denn dort geht es darum, dem Compiler genau diese Information zu entlocken.
Was Sie sich merken sollten
Während der Template-Typableitung werden Referenzargumente als Nicht-Referenzen behandelt – die Referenzheit wird also ignoriert.
Beim Ableiten von Typen für universelle Referenzparameter werden Lvalue-Argumente besonders behandelt.
Beim Ableiten von Typen für By-Value-Parameter werden const- und/oder volatile-Argumente so behandelt, als ob sie nicht mit const oder volatile versehen wären.
Während der Template-Typableitung werden Arrays oder Funktionsnamen als Argumente in Zeiger umgewandelt, sofern sie nicht zum Initialisieren von Referenzen genutzt werden.
Technik 2: Die auto-Typableitung verstehen
Wenn Sie „Technik 1: Typableitung beim Template" zur Template-Typableitung gelesen haben, wissen Sie schon alles, was für die Typableitung bei auto notwendig ist. Denn abgesehen von einer kuriosen Ausnahme sind die auto- und die Template-Typableitung identisch. Aber wie kann das sein? Zur Template-Typableitung gehören Templates und Funktionen und Parameter, aber bei auto kommt nichts davon vor.
Das stimmt, macht aber nichts. Es gibt eine direkte Abbildung zwischen der Template-Typableitung und der auto-Typableitung – eine algorithmische Umwandlung vom einen in das andere.
In „Technik 1: Typableitung beim Template" wird die Template-Typableitung mit diesem Funktions-Template erklärt:
templateT> void f(ParamType param);
Dazu gehört dieser Aufruf:
f(expr); // f mit einem Ausdruck aufrufen
Im Aufruf von f nutzt der Compiler expr, um die Typen für T und ParamType zu ermitteln.
Wird eine Variable mithilfe von auto deklariert, nimmt auto die Rolle von T im Template ein und die Typ-Spezifikation für die Variable fungiert als ParamType. Das lässt sich einfacher zeigen als erklären. Schauen Sie sich daher bitte dieses Beispiel an:
auto x = 27;
Hier ist die Typ-Spezifikation für x einfach auto. In der Deklaration:
const auto cx = x;
ist die Typ-Spezifikation jedoch const auto. Und bei:
const auto& rx = x;
ist es const auto&. Um Typen für x, cx und rx zu ermitteln, verhält sich der Compiler so, als ob es ein Template für jede Deklaration und einen Aufruf dieses Templates mit dem entsprechenden Initialisierungsausdruck gäbe:
template
func_for_x(27); // konzeptioneller Aufruf: Ermittelter
// Typ von param ist Typ von x.
template
func_for_cx(x); // konzeptioneller Aufruf: Ermittelter
// Typ von param ist Typ von cx.
template
func_for_rx(x); // konzeptioneller Aufruf: Ermittelter
// Typ von param ist Typ von rx.
Wie schon gesagt ist das Ableiten von Typen mit auto identisch mit dem für Templates – mit einer Ausnahme, auf die ich gleich eingehen werde.
„Technik 1: Typableitung beim Template" unterteilt die Template-Typableitung in drei Fälle – basierend auf dem Charakter von ParamType, der Typ-Spezifikation für param im allgemeinen Funktions-Template. In einer Variablendeklaration mit auto nimmt die Typ-Spezifikation den Platz von ParamType ein, sodass es auch auch hier drei Fälle gibt:
Fall 1: Die Typ-Spezifikation ist ein Zeiger oder eine Referenz, aber keine universelle Referenz.
Fall 2: Die Typ-Spezifikation ist eine universelle Referenz.
Fall 3: Die Typ-Spezifikation ist weder ein Zeiger noch eine Referenz.
Wir haben schon Beispiele für die Fälle 1 und 3 gesehen:
auto x = 27; // Fall 3 (x ist weder Zeiger noch Referenz)
const auto cx = x; // Fall 3 (cx auch nicht)
const auto& rx = x; // Fall 1 (rx ist nicht-univers. Ref.)
Fall 2 arbeitet so, wie Sie es erwarten würden:
auto&& uref1 = x; // x ist int und Lvalue,
// daher ist Typ von uref1 int&.
auto&& uref2 = cx; // cx ist const int und Lvalue,
// daher ist Typ von uref2 const int&.
auto&& uref3 = 27; // 27 ist int und Rvalue,
// daher ist Typ von uref3 int&&.
„Technik 1: Typableitung beim Template" schließt mit einem Abschnitt, wie Array- und Funktionsnamen für nichtreferenzielle Typangaben zu Zeigern werden. Das passiert auch bei der auto-Typableitung:
const char name[] = // Typ von name ist const char[13].
R. N. Briggs
;
auto arr1 = name; // Typ von arr1 ist const char*.
auto& arr2 = name; // Typ von arr2 ist
// const char (&)[13].
void someFunc(int, double); // someFunc ist Funktion,
// Typ ist void(int, double).
auto func1 = someFunc; // Typ von func1 ist
// void (*)(int, double).
auto& func2 = someFunc; // Typ von func2 ist
// void (&)(int, double).
Wie Sie sehen, geht die auto-Typableitung wie die Template-Typableitung vor. Es handelt sich letztendlich um zwei Seiten einer Medaille.
Mit einer Ausnahme. Beginnen wir mit der Beobachtung, dass C++98 beim Deklarieren eines int mit einem initialen Wert von 27 zwei Syntax-Varianten bietet:
int x1 = 27; int x2(27);
C++11 ermöglicht aufgrund seiner Unterstützung für die vereinheitlichte Initialisierung noch diese:
int x3 = { 27 }; int x4{ 27 };
Insgesamt gibt es also vier Syntax-Varianten, aber nur ein Ergebnis: ein int mit dem Wert 27.
Wie aber in „Technik 5: Ziehen Sie auto einer expliziten Typdeklaration vor" beschrieben wird, hat es Vorteile, Variablen über auto statt über explizite Typen zu deklarieren. Daher wäre es gut, int in den obigen Variablendeklarationen durch auto zu ersetzen. Ein einfaches Suchen und Ersetzen liefert diesen Code:
auto x1 = 27; auto x2(27); auto x3 = { 27 }; auto x4{ 27 };
Diese Deklarationen lassen sich alle kompilieren, aber sie haben nicht die gleiche Bedeutung wie vor dem Ersetzen. Die ersten beiden Anweisungen deklarieren tatsächlich eine Variable vom Typ int mit dem Wert 27. Die zweiten beiden deklarieren hingegen eine Variable vom Typ std::initializer_list
auto x1 = 27; // Typ ist int, Wert ist 27.
auto x2(27); // ebenso
auto x3 = { 27 }; // Typ ist std::initializer_list
// Wert ist { 27 }.
auto x4{ 27 }; // ebenso
Dies liegt an einer speziellen Typableitungsregel für auto. Ist der Initializer für eine per auto deklarierte Variable in geschweiften Klammern angegeben, ist der abgeleitete Typ eine std::initializer_list. Kann solch ein Typ nicht ermittelt werden (weil zum Beispiel die Werte im Braced Initializer unterschiedliche Typen haben), wird der Code nicht kompiliert:
auto x5 = { 1, 2, 3.0 }; // Fehler! T kann nicht für
// std::initializer_list
// ermittelt werden.
Wie der Kommentar schon beschreibt, wird die Typableitung in diesem Fall nicht funktionieren, aber es ist wichtig, zu erkennen, dass hier tatsächlich zwei Arten von Typableitung beteiligt sind. Die eine resultiert aus dem Einsatz von auto: Der Typ von x5 muss abgeleitet werden. Da der Initializer von x5 in geschweiften Klammern angegeben ist, wird als Typ von x5 eine std::initializer_list ermittelt. std::initializer_list ist aber ein Template. Instanziierungen sind std::initializer_list
Die Behandlung von Braced Initializers ist der einzige Punkt, in dem sich die Typableitung zwischen auto und Templates unterscheidet. Wird eine per auto deklarierte Variable mit einem Braced Initializer initialisiert, ist der abgeleitete Typ eine Instanziierung von std::initializer_list. Wird aber das entsprechende Template an den gleichen Initializer übergeben, schlägt die Ableitung fehl und der Code lässt sich nicht kompilieren:
auto x = { 11, 23, 9 }; // Typ von x ist
// std::initializer_list
template
// Deklaration von x
f({ 11, 23, 9 }); // Fehler! Typ für T nicht ermittelbar.
Geben Sie allerdings im Template an, dass param für ein unbekanntes T vom Typ std::initializer_list
template
f({ 11, 23, 9 }); // T als int abgeleitet, Typ von initList
// ist std::initializer_list
Der einzige echte Unterschied zwischen der auto- und der Template-Typableitung ist also, dass auto davon ausgeht, dass ein Braced Initializer eine std::initializer_list repräsentiert, während das bei der Template-Typableitung nicht der Fall ist.
Sie fragen sich vielleicht, warum es für die Typableitung bei auto eine spezielle Regel für Braced Initializers gibt, nicht aber bei der Template-Typableitung. Das frage ich mich selbst. Ich habe bisher keine vernünftige Erklärung dafür gefunden. Aber die Regel ist nun einmal da, und Sie müssen daran denken, dass der abgeleitete Typ bei der Kombination aus auto und einem Braced Initializer immer std::initializer_list sein wird. Das ist besonders dann entscheidend, wenn Sie sich vorgenommen haben, die vereinheitlichte Initialisierung umzusetzen – wozu natürlich die Braced Initializers gehören. Ein klassischer Fehler in der C++11-Programmierung ist das unabsichtliche Deklarieren einer Variable vom Typ std::initializer_list, wenn Sie eigentlich einen anderen Typ haben wollen. Dieser Fallstrick ist einer der Gründe, warum manche Entwickler nur dann geschweifte Klammern um ihre Initialisierungsausdrücke legen, wenn sie es müssen. (Wann das der Fall ist, wird in „Technik 7: Der Unterschied zwischen () und {} beim Erstellen von Objekten" erklärt.)
Bei C++11 ist das alles, aber bei C++14 geht noch mehr. C++14 ermöglicht es, mit auto den Rückgabetyp einer Funktion ableiten zu lassen (siehe „Technik 3: Verstehen Sie decltype"), und Lambdas in C++14 können auto bei der Parameterdeklaration verwenden. Allerdings wird bei diesen auto-Einsätzen die Template-Typableitung verwendet, nicht die auto-Typableitung. Eine Funktion mit einem auto-Rückgabetyp, die einen Braced Initializer zurückgibt, würde sich also nicht kompilieren lassen:
auto createInitList() {
return { 1, 2, 3 }; // Fehler: Typ nicht ableitbar } // für { 1, 2, 3 }
Das Gleiche gilt, wenn auto in einer Parameter-Typ-Spezifikation bei einem C++14-Lambda eingesetzt wird:
std::vector
auto resetV =
[&v](const auto& newValue) { v = newValue; }; // C++14
...
resetV({ 1, 2, 3 }); // Fehler! Typ nicht ableitbar
// für { 1, 2, 3 }
Was Sie sich merken sollten
Die auto-Typableitung entspricht meist der Template-Typableitung, allerdings geht die auto-Typableitung davon aus, dass ein Braced Initializer eine std::initializer_list repräsentiert, während das bei der Template-Typableitung nicht der Fall ist.
auto in einem Rückgabetyp einer Funktion oder bei einem Lambda-Parameter nutzt die Template-Typableitung, nicht die auto-Typableitung.
Technik 3: Verstehen Sie decltype
decltype ist ein seltsames Ding. Geben Sie ihm einen Namen oder einen Ausdruck, gibt decltype Ihnen den Typ des Namens oder Ausdrucks aus. Dabei erhalten Sie im Allgemeinen genau das, was Sie erwarten würden. Gelegentlich aber erhalten Sie ein Ergebnis, bei dem Sie sich nur am Kopf kratzen können und erst einmal nachschlagen müssen, was das zu bedeuten hat.
Wir werden mit den normalen Fällen beginnen – denen, die keine Überraschungen liefern. Im Gegensatz zu dem, was während der Typableitung für Templates und auto passiert (siehe die „Technik 1: Typableitung beim Template und „Technik 2: Die auto-Typableitung verstehen
), plappert decltype normalerweise genau den Typ des Namens oder Ausdrucks aus, den Sie ihm mitgeben:
const int i = 0; // decltype(i) ist const int.
bool f(const Widget& w); // decltype(w) ist const Widget&.
// decltype(f) ist bool(const Widget&).
struct Point {
int x, y; // decltype(Point::x) ist int. }; // decltype(Point::y) ist int.
Widget w; // decltype(w) ist Widget.
if (f(w)) ... // decltype(f(w)) ist bool.
template
\u
T& operator[](std::size_t index);
\u };
vector
Sehen Sie? Keine Überraschungen.
In C++11 ist der wichtigste Einsatzbereich für decltype vermutlich das Deklarieren von Funktions-Templates, bei denen der Rückgabetyp der Funktion von den Parametertypen abhängt. Stellen Sie sich zum Beispiel vor, dass wir eine Funktion schreiben wollen, die einen Container übernimmt, der das Indexieren über eckige Klammern ermöglicht (also »[]«), dabei den Benutzer authentifiziert und das Ergebnis der Index-Operation zurückliefert. Der Rückgabetyp der Funktion sollte der gleiche sein wie der Typ der Index-Operation.
operator[] auf einem Container mit Objekten vom Typ T liefert im Allgemeinen ein T& zurück. Das gilt zum Beispiel für std::deque und ist so gut wie immer der Fall für std::vector. Für std::vector
decltype macht das sehr einfach. Hier sehen Sie eine erste Version für das Template, das wir schreiben wollen. decltype berechnet dabei den Rückgabetyp. Das Template muss noch etwas verfeinert werden, aber das machen wir später:
template
-> decltype(c[i]) // Verbesserungen {
authenticateUser();
return c[i]; }
Der Einsatz von auto vor dem Funktionsnamen hat nichts mit Typableitung zu tun, sondern mit der C++11-Syntax des nachlaufenden Rückgabetyps, bei dem der Rückgabetyp der Funktion nach der Parameterliste angegeben wird (nach dem »->«). Ein nachlaufender Rückgabetyp hat den Vorteil, dass die Parameter der Funktion in der Spezifikation des Rückgabetyps genutzt werden können. In authAndAccess spezifizieren wir zum Beispiel den Rückgabetyp anhand von