C++ Core Guidelines: Regeln fĂĽrs Allokieren und Deallokieren
Die Guidelines besitzen sechs Regeln fĂĽr das explizite Anfordern und Freigeben von Speicher. Das ist sehr ĂĽberraschend. Lautet eine einfache Regel in modernem C++ doch schlicht: "Verwende kein explizites new und delete." Offensichtlich ist die C++-Welt doch nicht so einfach.
- Rainer Grimm
Die Guidelines besitzen sechs Regeln fĂĽr das explizite Anfordern und Freigeben von Speicher. Das ist sehr ĂĽberraschend. Lautet eine einfache Regel in modernem C++ doch schlicht: "Verwende kein explizites new und delete." Offensichtlich ist die C++-Welt doch nicht so einfach.
Hier sind die sechs Regeln:
- R.10: Avoid malloc() and free()
- R.11: Avoid calling new and delete explicitly
- R.12: Immediately give the result of an explicit resource allocation to a manager object
- R.13: Perform at most one explicit resource allocation in a single expression statement
- R.14: ??? array vs. pointer parameter
- R.15: Always overload matched allocation/deallocation pairs
Ich werde kein Wort ĂĽber die letzten zwei Regeln verlieren. Zum einen ist die Regel R.14 nur ein Fragment, zum anderen ist mir die Regel R.15 zu speziell fĂĽr allgemeine Empfehlungen. Wenn du mehr zu dem Ăśberladen von new und delete wissen willst, kannst du meine Artikel zur Speicheranforderung und -freigabe durchlesen.
Bevor ich tiefer in die Regel eintauche, ist noch ein wenig Hintergrundwissen notwendig, um diese besser zu verstehen. Das Erzeugen eines Objekts mit new besteht in C++ aus zwei Schritten.
- Anfordern des Speichers fĂĽr das Objekt
- Initialisieren des Objekts in dem angeforderten Speicherbereich
operator new or operator new [] ĂĽbernehmen den ersten Schritt; der Konstruktor ĂĽbernimmt den zweiten. Dieselbe Strategie findet bei der Destruktion des Objekts in umgekehrter Reihenfolge statt. Zuerst wird der Destruktor (falls vorhanden) aufgerufen, und dann wird der Speicher mittels operator delete or operator delete [] freigegeben. Diese zweistufige Erzeugen und die Destruktion sind der Grund fĂĽr die vier Regeln. Jetzt geht es los mit den Regeln.
R.10: Avoid malloc() and free()
Was ist der Unterschied zwischen new und malloc bzw. delete und free? Die C-Funktionen malloc und free tun nur die Hälfte ihres Jobs. malloc fodert lediglich den Speicher an, den free wieder freigibt. Weder ruft malloc den Konstruktor auf noch free den Destruktor.
Das bedeutet, wenn du ein Objekt verwendest, das mit malloc erzeugt wurde, erhältst du undefiniertes Verhalten.
// mallocVersusNew.cpp
#include <iostream>
#include <string>
struct Record{
Record(std::string na = "Record"): name(na){} // (4)
std::string name;
};
int main(){
std::cout << std::endl;
Record* p1 = static_cast<Record*>(malloc(sizeof(Record))); // (1)
std::cout << p1->name << std::endl; // (3)
auto p2 = new Record; // (2)
std::cout << p2->name << std::endl;
std::cout << std::endl;
}
Ich fordere lediglich in (1) Speicher für mein Record-Objekt an. Das Ergebnis ist, dass die Ausgabe p1->name in (3) undefiniertes Verhalten darstellt. Im Gegensatz dazu, stößt der Ausdruck (2) den Konstruktor in Zeile (4) an. Undefiniertes Verhalten bedeutet schlicht, dass keine verbindlichen Annahmen zu dem Programm mehr möglich sind.
Abhängig von der verwendeten Plattform und dem verwendeten GCC Compiler verhält sich das Programm vollkommen unterschiedlich.
- GCC 4.8.5 produziert einen Core Dump auf meinem lokalen PC.
- GCC 4.9 (auf cppreference.com) produziert keine Ausgabe.
- GCC 7.1 (auf cppreference.com ) produziert die erwartete Ausgabe.
R.11: Avoid calling new and delete explicitly
Diese Regel solltest du im Kopf behalten. Die Betonung in dieser Regel liegt auf dem Wort explizit, denn Smart Pointer oder die Container der Standard Template Library verwenden implizit new und delete.
R.12: Immediately give the result of an explicit resource allocation to a manager object
R.12 ist der entscheidende Grund, um Smart Pointer wie std::unique_ptr<int> upInt(new int)) zu verwenden, und gilt nicht für das Gegenbeispiel aus den Guidelines. Falls die Speicheranforderung des Puffer buffer fehlschlägt, geht der Filehandle verloren.
void f(const std::string& name)
{
FILE* f = fopen(name, "r"); // open the file
std::vector<char> buf(1024);
fclose(f); // close the file
}
R.13: Perform at most one explicit resource allocation in a single expression statement
Die Regel ist ein wenig knifflig.
void func(std::shared_ptr<Widget> sp1, std::shared_ptr<Widget> sp2){
...
}
func(std::shared_ptr<Widget>(new Widget(1)),
std::shared_ptr<Widget>(new Widget(2)));
Der Aufruf der Funkion func ist nicht exception-safe und kann daher in einem Speicherleck enden. Warum? Der Grund ist, dass vier Operationen ausgefĂĽhrt werden mĂĽssen, um die zwei Shared Pointer zu initialisieren.
- Speicheranforderung fĂĽr Widget(1)
- Aufruf des Konstruktors fĂĽr Widget(1)
- Speicheranforderung fĂĽr Widget(2)
- Aufruf des Konstruktors fĂĽr Widget(2)
Der Compiler kann aber durchaus erst den Speicher fĂĽr Widget(1) und Widget(2) anfordern, bevor er die Konstruktoren aufruft.
- Speicheranforderung fĂĽr Widget(1)
- Speicheranforderung fĂĽr Widget(2)
- Aufruf des Konstruktors fĂĽr Widget(1)
- Aufruf des Konstruktors fĂĽr Widget(2)
Falls nun einer der Konstruktoren eine Ausnahme wirft, wird der Speicher des anderen Objekts nicht automatisch freigegeben, und wir erhalten ein Speicherleck.
Dies Problem lässt sich sehr einfach durch die Fabrikfunktion std::make_shared lösen, die einen std::shared_ptr erzeugt.
func(std::make_shared<Widget>(1), std::make_shared<Widget>(2));
std::make_shared sichert zu, dass die Funktion keinen Effekt besitzt, falls eine Ausnahme geworfen wird. FĂĽr das Pendant std::make_unique, um eine std::unique_ptr zu erzeugen, gibt dieselbe Garantie.
Wie geht's weiter?
Die nächsten Regeln zum Umgang mit Ressourcen werden der Regel R.11 folgen: avoid calling new and delete explicitly. Daher geht es im nächsten Artikel rund um die Smart Pointer std::unique_ptr, std::shared_ptr und std::weak_ptr.
Weitere Informationen:
- Die neuen PDF-Päckchen stehen zum Download bereit:
- Deutsch: Multithreading: The High-Level Schnittstelle
- Englisch: Embedded: Performance Matters
- Für meine drei offenen Seminare im ersten Halbjahr 2018 sind noch Plätze frei:
- Embedded-Programmierung mit modernem C++: 16. bis 18. Januar 2018
- C++11 und C++14: 13. bis 15. März 2018
- Multithreading mit modernem C++: 8. bis 9. Mai. 2018