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

Quellcode

#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).

Im Moment (das C++11 ja doch noch recht neu ist) ist noch die C++98-Variante zu empfehlen. Für neuen Code, in dem man sowieso C++11-Features nutzen möchte, ist natürlich die neue Variante etwas klarer.