Kopieren verboten!
Beim Programmieren taucht öfter mal die Anforderung auf, Kopien von bestimmten Objekten zu verhindern. Das können bspw. große Objekte sein, die man aus Effizienzgründen nicht kopieren will oder Betriebssystemressourcen, die man nicht kopieren kann/sollte (Mutexe oder Filehandles sind da immer gute Kandidaten). Dummerweise hilft einem C++ beim Kopieren von Objekten im Normalfall, indem es die dafür notwendigen Memberfunktionen automatisch anlegt, was natürlich dann nicht mehr gewollt ist. Das kann man aber auch verhindern.
Video
Code
#include <iostream>
#include <stdexcept>
class IntBuffer
{
unsigned int *mBuffer;
unsigned int mSize;
// C++98-Variante mit private-Copykonstruktor
/*
IntBuffer(IntBuffer const &) {}
*/
public:
explicit IntBuffer(unsigned int size)
: mBuffer(new unsigned int[size]), mSize(size)
{
}
// C++11 Variante mit explizit gelöschtem Konstruktor
IntBuffer(IntBuffer const &) = delete;
unsigned int &operator[](unsigned int position)
{
if (position < mSize) {
return mBuffer[position];
}
throw std::runtime_error("Out of bounds");
}
unsigned int size()
{
return mSize;
}
};
void printBuffer(IntBuffer buffer)
{
for (unsigned int i = 0; i < buffer.size(); ++i) {
std::cout << buffer[i] << " ";
}
std::cout << std::endl;
}
int main()
{
IntBuffer b(10);
b[5] = 15;
printBuffer(b);
}
Erklärung
C++ generiert einige Memberfunktionen, die man typischerweise gebrauchen kann, automatisch. Dazu gehören:
- Standardkonstruktor – Falls kein anderer Konstruktor definiert ist
- Copy-Konstruktor – Falls kein eigener Copykonstruktor (oder Movekonstruktor bei C++11) definiert wurde. Sonderregel hier noch: wenn ein eigener Destruktor definiert wurde, dann ist bei C++11 das Fehlen eines eigenen Copykonstruktors als veraltet deklariert. Sprich: das könnte dann mit der nächsten Standardvariante ein Fehler sein.
- Destruktor – Falls kein eigener erzeugt wurde
- Zuweisungsoperator (Copy, bzw. Copy und Move bei C++11) – Falls kein eigener definiert wurde
Im Standardfall macht jedes dieser Memberfunktionen mit den Feldern der Klasse
genau das, was sie selbst tut: sprich: der Copy-Konstruktor ruft die
Copy-Konstruktoren aller Member auf, der Destruktor die Destruktoren etc. Genau
dieses Verhalten ist hier unser Problem: wird der Copykonstruktor von unserer
Klasse aufgerufen (bspw. durch den call-by-value-Parameter der Funktion
printBuffer), dann ruft er standardmäßig die Copykonstruktoren der Felder auf.
Das ist im Falle des mSize-Feldes kein Problem: das kopiert einfach den Wert.
Im Fall des mBuffer-Pointers allerdings sieht die Sache anders aus: statt den
Speicherbereich zu kopieren, auf den der Pointer zeigt, wird nur der Wert des
Pointers kopiert. Damit zeigen dann natürlich zwei Pointer auf den gleichen
Speicherbereich. Das ist an sich erstmal noch kein Problem, kann sogar ja
gewollt sein. Problematisch wird es dann, wenn eines der Objekte vernichtet
wird: der Destruktor gibt den Speicherbereich frei. Damit zeigen alle Kopien
plötzlich ins Nirvana. Werden diese nun vernichtet, versucht das Programm einen
bereits freigegebenen Speicherbereich nochmals freizugeben. Das ist laut
Standard undefiniertes Verhalten, kann also im Prinzip beliebiges Verhalten
zeigen (von einfachem Funktionieren bis hin zum Programmabsturz).
Dem Problem kann man mit zwei Mitteln begegnen: entweder man definiert einen
eigenen Copykonstruktor, der den Speicherbereich tatsächlich kopiert oder (wie
in unserem Fall hier) man entscheidet, dass eine Kopie verboten ist und
definiert seine Klasse entsprechend. Diesen Weg haben wir hier gewählt, weil so
ein IntBuffer ja beliebig groß werden kann und wir den daher nicht kopieren
wollen. Hier gibt es nun zwei Möglichkeiten: entweder man definiert den
Copykonstruktor als private (wie oben im Beispiel auskommentiert in Zeile 11)
oder man verbietet dem Compiler explizit mit = delete die automatische
Implementierung des Konstruktors (geht nur in C++11, siehe Zeile 21 im
Beispiel). Versucht man nun eine Kopie eines IntBuffer-Objektes anzulegen,
dann wird sich der Compiler beschweren, dass er den Copy-Konstruktor nicht
aufrufen darf (bzw. dass dieser gelöscht wurde bei der C++11-Variante).