Mehr Mythen der Blog-Leser
Bei den weiteren Mythen geht es um Funktionsparameter, die Initialisierung von Klassenelementen und Zeiger versus Referenzen.
- Rainer Grimm
Heute schließe ich meine Geschichte zu den Mythen meiner Blog-Leser ab. Bei den aktuellen Mythen geht es um Funktionsparameter, die Initialisierung von Klassenelementen und Zeiger versus Referenzen.
Nimm die Parameter immer als konstante Referenz an (Gunter Königsmann)
Wenn eine Funktion ihre Parameter annimmt und diese nicht verändern soll, gibt es zwei Optionen:
- Sie nimmt die Parameter by Value (kopieren) an.
- Sie nimmt die Parameter als konstante Referenz an.
Dies war die Korrektheitsperspektive. Doch was lässt sich über die Performanz sagen? Die C++ Core Guidelines geben auf diese Frage eine Antwort. Das folgende Beispiel bringt die Schwierigkeit auf den Punkt:
void f1(const string& s); // OK: pass by reference to const; always cheap
void f2(string s); // bad: potentially expensive
void f3(int x); // OK: Unbeatable
void f4(const int& x); // bad: overhead on access in f4()
Wohl basierend auf Erfahrung stellt die Guidelines die folgende Daumenregel auf.
- Du sollst einen Parameter p per konstanter Referenz annehmen, wenn gilt:
sizeof(p) > 4 * sizeof(int)
- Du sollst einen Parameter p kopieren, wenn gilt:
sizeof(p) < 3 * sizeof(int)
Damit stellt sich nur noch die Frage, wie groß die Datentypen sind. Das Programm sizeofArithmeticTypes.cpp
gibt die Antwort für die arithmetischen Datentypen:
// sizeofArithmeticTypes.cpp
#include <iostream>
int main(){
std::cout << std::endl;
std::cout << "sizeof(void*): " << sizeof(void*) << std::endl;
std::cout << std::endl;
std::cout << "sizeof(5): " << sizeof(5) << std::endl;
std::cout << "sizeof(5l): " << sizeof(5l) << std::endl;
std::cout << "sizeof(5ll): " << sizeof(5ll) << std::endl;
std::cout << std::endl;
std::cout << "sizeof(5.5f): " << sizeof(5.5f) << std::endl;
std::cout << "sizeof(5.5): " << sizeof(5.5) << std::endl;
std::cout << "sizeof(5.5l): " << sizeof(5.5l) << std::endl;
std::cout << std::endl;
}
sizeof(void*)
gibt zurück, ob es sich um ein 32-Bit- oder 64-Bit-System handelt. Dank des Online-Compilers rextester lässt sich das Programm auf GCC, Clang und cl.exe (Windows) ausführen. Hier sind die Zahlen für die 64-Bit-Systeme:
GCC
Clang
cl.exe (Windows)
Die Zahlen von cl.exe unterscheiden sich deutlich von den von GCC und Clang. Ein long int besitzt 4 Bytes und ein long double 8 Bytes auf Windows. Im Gegensatz dazu sind beide Datentypen auf dem GCC und Clang doppelt so groß.
Initialisierung und Zuweisungen in einem Konstruktor sind äquivalent (Gunter Königsmann)
Zuerst muss ich klären, für was die Initialisierung und die Zuweisung in einem Konstruktor steht:
class Good{
int i;
public:
Good(int i_): i{i_ }{}
};
class Bad{
int i;
public:
Bad(int i_): { i = i_ ; }
};
Die Klasse Good
verwendet Initialisierung, aber die Klasse Bad
Zuweisung. Dies sind die Konsequenzen:
- Die Variable
i
wird in der KlasseGood
direkt initialisiert. - Die Variable
i
wird in der KlasseBad
zuerst Default-konstruiert und danach nochmals überschrieben.
Die Konstruktorinitialisierung ist einerseits schneller, aber auch andererseits bei Konstanten, Referenzen, und Mitglieder einer Klasse, die sich nicht Default-konstruieren lassen, möglich:
// constructorAssignment.cpp
struct NoDefault{
NoDefault(int){};
};
class Bad{
const int constInt;
int& refToInt;
NoDefault noDefault;
public:
Bad(int i, int& iRef){
constInt = i;
refToInt = iRef;
}
// Bad(int i, int& iRef): constInt(i), refToInt(iRef), noDefault{i} {}
};
int main(){
int i = 10;
int& j = i;
Bad bad(i, j);
}
Wenn ich versuche, das Programm zu übersetzen, erhalte ich drei Fehlermeldungen:
constInt
ist nicht initialisiert und kann nicht im Konstruktor überschrieben werden.refToInt
ist nicht initialisiert.- Die Klasse
NoDefault
besitzt keinen Default-Konstruktor, da ich einen Konstruktor fürint
bereits implementiert habe. Wenn du einen Konstruktor für eine Klasse implementierst, erzeugt der Compiler nicht automatisch einen Default-Konstruktor.
In der zweiten, erfolgreichen Übersetzung verwende ich den auskommentierten Konstruktor, der die Initialisierung der Zuweisung vorzieht.
In dem Beispiel kamen aus gutem Grunde Referenz statt nackten Zeiger zum Einsatz.
Du benötigst nackte Zeiger in deinem Code (Thargon110)
Motiviert durch den Kommentar von Thargon110 bin ich jetzt dogmatisch: NNN. Was? Natürlich meinte ich No Naked New. Aus der Perspektive des Applikationsentwicklers betrachtet gibt es keinen Grund, nackte Zeiger einzusetzen. Wenn du zeigerartige Semantik benötigst, stecke deinen nackten Zeiger in einen Smart Pointer – dafür genau steht NNN – und das Problem ist gelöst.
Im Wesentlichen hat C++11 std::unique_ptr
für die exklusiven und std::shared_ptr
für die geteilten Besitzverhältnisse. Das heißt, wenn ein std::shared_ptr
kopierst wird, wird ein interner Referenzzähler inkrementiert und wenn ein std::shared_ptr
gelöscht wird, wird ein interner Referenzzähler dekrementiert. Besitzverhältnisse stehen dafür, dass Smart Pointer auf den zugrundeliegenden Speicher aufpassen und ihn freigeben, wenn er nicht mehr benötigt wird. Der Speicher wird im Falle des std::shared_ptr
nicht mehr benötigt, wenn der Referenzzähler den Wert 0 besitzt.
Damit gibt es mit modernem C++ keine Speicherleaks mehr. Ich höre schon deine Klagen und freue mich darauf, sie zu widerlegen:
- Zyklen von
std::shared_ptr
können Speicherleaks verursachen, da der Referenzzähler nie den Wert 0 erreicht. Stimmt, aber einstd:.weak_ptr
hilft, zyklische Referenzen vonstd::shared_ptr
zu brechen:std::weak_ptr
. - Der
std::shared_ptr
besitzt Verwaltungsaufwand und ist damit teurer als ein nackter Zeiger. Stimmt, darum solltestd::unique_ptr
die erste Wahl sein:std::unique_ptr
. - Ein
std::unique_ptr
ist nicht sehr praktisch, denn er kann nicht kopiert werden. Stimmt, einstd::unique_ptr
kann aber verschoben werden.
Gerade die letzte Klage ist sehr hartnäckig. Daher wird mir das kleine Beispiel wertvolle Dienste leisten:
// moveUniquePtr.cpp
#include <algorithm>
#include <iostream>
#include <memory>
#include <utility>
#include <vector>
void takeUniquePtr(std::unique_ptr<int> uniqPtr){ // (1)
std::cout << "*uniqPtr: " << *uniqPtr << std::endl;
}
int main(){
std::cout << std::endl;
auto uniqPtr1 = std::make_unique<int>(2014);
takeUniquePtr(std::move(uniqPtr1)); // (1)
auto uniqPtr2 = std::make_unique<int>(2017);
auto uniqPtr3 = std::make_unique<int>(2020);
auto uniqPtr4 = std::make_unique<int>(2023);
std::vector<std::unique_ptr<int>> vecUniqPtr;
vecUniqPtr.push_back(std::move(uniqPtr2)); // (2)
vecUniqPtr.push_back(std::move(uniqPtr3)); // (2)
vecUniqPtr.push_back(std::move(uniqPtr4)); // (2)
std::cout << std::endl;
std::for_each(vecUniqPtr.begin(), vecUniqPtr.end(), // (3)
[](std::unique_ptr<int>& uniqPtr){ std::cout << *uniqPtr << std::endl; } );
std::cout << std::endl;
}
Die Funktion takeUniquePtr
(Zeile 1) nimmt einen std::unique_ptr
per Value an. Die zentrale Beobachtung ist es, dass dazu der std::unique_ptr
verschoben werden muss. Diese zentrale Beobachtung gilt auch für die Argumente des std::vector<std::unique_ptr<int>>
(Zeile 2). std::vector
wie alle Container der Standard Template Library (STL) will seine Elemente besitzen. Ein Kopieren eines std::unique_ptr
ist aber nicht möglich. std::move
löst dieses Problem. Du kannst selbst einen Algorithmus der STL wie std::for_each
auf einen std::vector<std::unique_ptr<int>>
(Zeile 3) anwenden, wenn dieser keine Copy-Semantik verwendet.
Verwende Referenzen anstelle von Zeigern
Am Ende will ich noch auf den initialen Punkt von Thargon110 reagieren. Ich muss zugeben, dass diese Regel deutlich höhere Relevanz in klassischem C++ ohne Smart Pointer besitzt, denn Smart Pointer sind im Gegensatz zu nackten Zeigern Besitzer.
Verwende eine Referenz anstelle eines Zeigers, denn eine Referenz besitzt immer einen Wert. Ermüdende Prüfungen wie die folgende sind daher nicht mehr notwendig mit Referenzen:
if(!ptr){
std::cout << "Something went terrible wrong" << std::endl;
return;
}
std::cout << "All fine" << std::endl;
Zusätzlich kannst du die Prüfung nicht vergessen. Referenzen verhalten sich wie konstante Zeiger.
Wie geht's weiter?
Die C++ Core Guidelines definieren Profile. Profile sind Teilmengen der Regeln. Profile gibt es für Typ-Sicherheit, Bounds-Sicherheit (Prüfung der Containergrenzen) und Lebenszeit-Sicherheit.
Wahl des nächsten PDF-Päckchens:
Auf meinem deutschen und meinem englischen Blog findet gerade die Wahl zum nächsten PDF-Päckchen statt. Hier sind die Links zur Wahl:
- Deutscher Blog: Welches PDF-Päckchen soll ich zusammenstellen? Mache dein Kreuz!
- Englischer Blog: Which PDF bundle should I provide? Make your choice!