C++ Core Guidelines: Regeln fürs Allokieren und Deallokieren

Modernes C++  –  3 Kommentare

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:

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.

  1. Anfordern des Speichers für das Objekt
  2. 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.

  1. Speicheranforderung für Widget(1)
  2. Aufruf des Konstruktors für Widget(1)
  3. Speicheranforderung für Widget(2)
  4. 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.

  1. Speicheranforderung für Widget(1)
  2. Speicheranforderung für Widget(2)
  3. Aufruf des Konstruktors für Widget(1)
  4. 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.

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.