P
Überladung von Operatoren in C++ (Teil 4) - new, delete & Co.
Inhalt
1 operator new und der new-Operator
2 Überladung von operator new und operator delete
2.1 Standardüberladungen
2.2 Benutzerdefinierte Überladungen
3 Zu jedem Topf ein Deckel
4 Exceptionsicherheit
1 operator new und der new-Operator
Zum Grundlagenwissen in C++ gehört die Möglichkeit, mit new Speicher anzufordern und Objekte darin zu erzeugen sowie sie mit delete wieder zu zerstöen un den vorher angeforderten Speicher wieder freizugeben. Das sind jeweils zwei Aktionen als Folge von einem Token im Quelltext. Darin unterscheiden sich die Speicherverwaltungsoperatoren von den restlichen Operatoren in C++.
Im Folgenden sei der XY -Operator jeweils das Token im Quelltext, während operator xy die zugehörige Funktion ist, die aufgerufen wird. Im Falle arithmetischen Operatoren wie z.B. +, - usw. korrespondieren beide direkt miteinander, d.h. der + -Operator im Quelltext wird vom Compiler direkt in einen Aufruf von operator+ übersetzt. Bei den Speicherverwaltungsoperatoren ist das etwas anders:
Betrachten wir folgenden Quelltext:
std::string* p1 = new std::string("gaga");
std::string* pN = new std::string[5] ("fivetimeswtf");
delete p1;
delete[] pN;
Gehen wir es Zeile für Zeile durch: in Zeile 1 steht der new -Operator. Dies führt dazu, dass der Compiler zuerst die Funktion operator new aufruft, die den nötigen Speicher beschafft, in dem Fall Speicher der Größe sizeof(std::string) . Danach ist er anders als bei den anderen Operatoren aber noch nicht fertig sondern ruft noch den Konstruktor für ein std::string -Objekt auf, dass in dem von operator new beschafften Speicher entstehen soll. Dabei werden dem Konstruktor die übergebenen Argumente mitgegeben. Nachdem der Konstruktor abgearbeitet wurde wird der Zeiger auf den Speicherbereich und damit auf das neue Objekt zurückgegeben.
In Zeile 2 passiert quasi das selbe, allerdings für mehrere Objekte gleichzeitig. Zuerst wird operator new[] aufgerufen, um den nötigen Speicher für 5 strings zu beschaffen. Danach werden einer nach dem anderen die 5 strings im angeforderten Speicher initialisiert, und zwar alle mit dem selben übergebenen Argument. Nachdem das letzte Objekt initialisiert (d.h. bei Klassen konstruiert) wurde, wird ein Zeiger auf den beschafften Speicher, also auf das erste Objekt darin, zurückgegeben.
Der delete -Operator in Zeile 3 hat wie der new -Operator zwei Aktionen zur Folge: Zuerst wird für das Objekt, auf das p1 zeigt, der Destruktor aufgerufen. Danach wird der Speicher, der vorher von new angefordert wurde, wieder freigegeben. Beim delete[] -Operator in Zeile 4 ist das Verfahren ähnlich, nur dass hier der Destruktor fünfmal aufgerufen wird.
2 Überladung von operator new und operator delete
Die Überladung von Operatoren in C++ bezieht sich einzig auf die zugrundeliegenden operator -Funktionen, nicht jedoch auf das Verhalten, das der Compiler aus dem Auftauchen eines Operators im Quellcode macht. Im Falle von new und delete heißt das, dass das Verhalten von operator new und operator delete verändert werden kann, nicht jedoch die Tatsache, dass zum entsprechenden Zeitpunkt die Konstruktoren und Destruktoren aufgerufen werden. Das Selbe gilt natürlich für operator new[] und operator delete[] . Die beiden werden im Folgenden nicht weiter explizit erwähnt, es sei denn es gibt Besonderheiten im Vergleich zu operator new und operator delete .
Für das Überladen von operator new und operator delete gelten bestimmte Regeln und Richtlinien:
Ort der Überladung und gruppenweise Überladung
operator new und operator delete dürfen nur als Methode einer Klasse oder im globalen Namespace überladen werden. Eine Überladung im globalen Namespace versteckt allerdings die bereits bekannten Standardimplementierungen, so dass darauf dann nicht mehr zugegriffen werden kann. In den meisten Fällen ist daher von der Überladung der globalen Funktionen abzuraten. Wird operator new als Klassenmethode überladen, dann wird die Überladung für alle Speicheranforderungen der Klasse sowie der abgeleiteten Klassen benutzt, d.h. operator new und operator delete werden vererbt. operator new und operator delete sind implizit statische Methoden, d.h. der Compiler macht sie auch dann static , wenn das Schlüsselwort nicht mit angegeben wird.
Wenn man operator new überlädt, dann tut man dies um ein bestimmtes Verhalten bei der Speicherbeschaffung zu erzielen. Im Normalfall bedeutet das, dass man operator delete auch überladen muss, um diesem Verhalten auch bei der Freigabe des Speichers gerecht zu werden. Zusätzlich wird in den meisten Fällen auch eine Überladung von operator new[] und operator delete[] nötig sein, da das Verhalten bei der Allokation von Arrays nicht vom Verhalten bei der Allokation von Einzelobjekten abweichen sollte.
Argumente und Rückgabewerte
Der Rückgabewert von operator new ist immer void* und zeigt auf den allokierten Speicher oder ist NULL . Das erste Argument ist vom Typ std::size_t und enthält die Größe des angeforderten Speicherbereichs. Beim Aufruf des new -Operators wird das Argument nicht übergeben, es wird vom Compiler ermittelt, bei new T; ist es gleich sizeof(T) . Rückgabewert und Argumente von operator new[] sind die selben, bei new T[N]; berechnet der Compiler das erste Argument mit N * sizeof(T) . Alle zusätzlichen, über das size_t hinausgehenden Argumente werden beim Aufruf im Quellcode nach dem new -Operator angegeben:
MyType* p = new (zweitesArgument, drittesArgument) MyType(konstruktorArgument);
Der Rückgabewert von operator delete ist void , das erste Argument ist vom Typ void* und zeigt auf den freizugebenden Speicherbereich. Gleiches gilt für operator delete[] .
2.1 Standardüberladungen
Im Standard vorgegeben sind drei Überladungen von operator new : Das "normale" new, nothrow -new und das sogenannte Placement- new . Für operator delete gibt es nur zwei Standard-Versionen, weil es kein korrespondierendes delete zum Palcement- new gibt. Die Syntax für die Überladung und Aufrufe sind wie folgt:
class X
{
public:
static void* operator new(std::size_t size); // (a)
static void* operator new(std::size_t size, std::nothrow_t); // (b)
static void* operator new(std::size_t size, void* ptr); // (c)
static void operator delete(void* ptr); // (d)
static void operator delete(void* ptr, std::nothrow_t); // (e)
};
int main()
{
X* xptr1 = new X; // (1)
X* xptr2 = new(std::nothrow) X; // (2)
X* xptr3 = X::operator new(sizeof(X)); // (3)
/*xptr3 =*/ new(xptr3) X; // (4)
delete xptr1; // (5)
delete(std::nothrow) xptr2; // (6)
xptr3->~X(); // (7)
operator delete(xptr3); // (8)
}
In (a) wird das "normale" new deklariert. Der Aufruf mittels new -Operator ist in (1) zu sehen, wie in Abschnitt 1 erläutert wurde, wird hier direkt nach dem Aufruf des operator new ein Objekt vom Typ X konstruiert. Schlägt die Speicherbeschaffung endgültig fehl, so verlangt der Standard, dass operator new eine Exception vom Typ std::bad_alloc wirft. Was in diesem Kontext "endgültig" heißt, wird später erläutert.
In (b) wird das nothrow -new deklariert. Es akzeptiert ein Argument vom Typ std::nothrow_t . Der Standard definiert ein globales Objekt dieses Typs mit Namen std::nothrow . Man braucht also kein eigenes Objekt des Typs zu schaffen sondern kann den Operator wie in (2) gezeigt mit dem globalen Objekt aufrufen. Der Parameter ist nur ein formaler Parameter und wird nicht ausgewertet ( std::nothrow_t ist eine leere Klasse). Im Gegensatz zum "normalen" new wirft das nothrow -new keine Exception sondern liefert einen Nullpointer bei Nichterfolg. Das restliche Verhalten ist exakt das selbe wie beim "normalen" operator new . Dieses Verhalten existiert hauptsächlich aus Kompatibilitätsgründen mit Vorstandard-Versionen von C++, wo das "normale" new bei Nichterfolg einen Nullpointer zurücklieferte.
In (c) wird das Standard-Placement-new deklariert. In vielen Fällen werden auch alle benutzerdefinierten Überladungen (d.h. mit anderen Parametern als nothrow und void* ) als Placement-new bezeichnet, ich werde der Klarheit wegen aber zwischen Placement-new und benutzerdefinierten Überladungen unterscheiden. Placement-new erwartet als zweiten Parameter einen Pointer, der auf einen bereits allokierten Speicherbereich zeigt, der groß genug ist um das zu erstellende Objekt darin zu konstruieren. Die Standard-Implementierung übernimmt keine weiteren Checks des Speicherblocks und ruft lediglich den Konstruktor für das neue Objekt auf. In (3) und (4) geschieht folgendes: In (3) wird die Funktion operator new für das "normale new" direkt aufgerufen, d.h. es wird nur der Speicher beschafft ohne die nachfolgende Konstruktion eines Objekts. In (4) wird danach mittels Placement-new in dem soeben beschafften rohen Speicher ein neues Objekt konstruiert.
In (d) wird operator delete deklariert, in (e) die nothrow -Version. Die jeweiligen Aufrufe sind in (5) und (6) zu sehen. Da delete sowieso keine Exception werfen sollte sind die zwei unterschiedlichen Versionen des operator delete eigentlich redundant. Tatsächlich ist im Standard auch festgelegt, dass beide delete -Versionen Pointer entgegennehmen müssen, die entweder vom normalen oder vom nothrow-new erzeugt worden sind. Dies ist auch insofern konsequent, weil die beiden new -Versionen sich im Erfolgsfall nicht unterschiedlich verhalten sollten.
2.2 Benutzerdefinierte Überladungen
Unter Berücksichtigung der oben genannten Anforderungen an die Rückgabetypen und den ersten Parameter sind alle möglichen Überladungen der Operatorfunktionen denkbar. Ein häufig genanntes Beispiel ist die Nutzung eines Memorymanagers. Der Memorymanager ist eine eigens definierte Klasse, die die Allokation von Speicher in großen Blöcken übernimmt und bei Bedarf kleinere Blöcke daraus für die Objekte zur Verfügung stellt. Zur Laufzeit wird eine oder mehrere Instanzen des Memorymanagers erstellt und bei den Operatoraufrufen jeweils mit übergeben. Beispiel:
class MemoryManager
{
public:
/* Konstruktoren etc. */
void* get(size_t size); //besorgt Speicher der Größe N*size
void release(void* there); //gibt den Speicher an der Stelle *there wieder frei.
};
template <class T>
struct Managed
{
static void* operator new(std::size_t size, MemoryManager& mm)
{
void* ptr = mm.get(size);
if (ptr == NULL) throw std::bad_alloc(); //normales new wirft exception bei Fehlschlag
return ptr;
}
static void* operator new(std::size_t size, std::nothrow_t&, MemoryManager& mm)
{
return mm.get(size);
}
static void operator delete(void* there, MemoryManager& mm)
{
mm.release(there);
}
static void operator delete(void* there, std::nothrow_t&, MemoryManager& mm)
{
mm.release(there);
}
static void operator new[](std::size_t size, MemoryManager& mm)
{
void* ptr = mm.get(size);
if (ptr == NULL) throw std::bad_alloc(); //normales new wirft exception bei Fehlschlag
return ptr;
}
// usw.
};
3 Zu jedem Topf ein Deckel
4 Exceptionsicherheit
Quellen/Links
http://www.informit.com/guides/content.aspx?g=cplusplus&seqNum=379