Nehmen wir einmal an, wir möchten eine Arrayklasse für Objekte dieses Typen bauen:
struct object
{
object& operator=(const object& other)
{
if(std::rand() == 12389)
{
throw std::exception();
}
}
};
Eine erste Implementation könnte so aussehen:
class object_array
{
public:
object_array():
array(0),
size(0)
{}
explicit object_array(std::size_t size):
array(new object[size]),
size(size)
{}
object_array(const object_array& other):
array(0),
size(other.size)
{
if(other.array && size)
{
try
{
array = new object[size];
std::copy(other.array, other.array + size, array);
}
catch(...)
{
delete [] array; // Im Fehlerfall speicher freigeben, um kein Speicherleck zu hinterlassen.
throw;
}
}
}
~object_array()
{
delete [] array;
}
object_array& operator=(const object_array& other)
{
delete [] array;
array = 0;
size = other.size;
if(other.array && size)
{
array = new object[size];
std::copy(other.array, other.array + size, array);
}
return *this;
}
object& operator[](std::size_t index)
{
assert(index < size);
return array[index];
}
object operator[](std::size_t index) const
{
assert(index < size);
return array[index];
}
private:
object* array;
std::size_t size;
};
Diese Implementation weist einige Probleme auf. operator= beinhaltet praktisch den selben Code wie der Kopierkonstruktor. Nun könnte man eine Funktion copy schreiben, die von operator= und vom Kopierkonstruktor aufgerufen wird.
Doch was passiert bei einer Selbstzuweisung?
object_array array(5); array = array;
Verursacht undefiniertes Verhalten. Das eigene Array wird im Zuweisungsoperator gelöscht und vom anderen Objekt in das eigene Array hineinkopiert. Wenn das andere Array das selbe ist wie das eigene wird beim Kopieren auf Speicher zugegriffen, der gerade erst freigegeben wurde. Das lässt sich aber auch regeln. Über eine Simple abfrage ob this == &other.
Ein Problem besteht aber immer noch: Eine Kopieroperation von object kann eine Exception werfen. Was passiert nun, wenn bei operator= das Kopieren eines Elements mittendrin fehlschlägt? operator= bricht ab und lässt das Array in einem ungültigen Zustand. Alle Elemente nach dem objekt, das die Exception warf, bleiben unkopiert.
Die Lösung: Das Copy & Swap Idiom. Alle eben genannten Probleme lassen sich darüber lösen, ohne irgendwelchen copy-Funktionen oder Abfragen ob this == &other. Wir ergänzen eine swap-Funktion, die 2 object_arrays miteinander tauscht:
void swap(object_array& other)
{
std::swap(array, other.array);
std::swap(size, other.size);
}
Unseren operator= implementieren wir nun so:
object_array& operator=(object_array other)
{
swap(other);
return *this;
}
Er arbeitet folgendermaßen: Das auf der rechten Seite stehende Objekt der Zuweisung wird über den Kopierkonstruktor kopiert. Dieses Objekt wird über swap getauscht. Wenn beim Kopieren etwas fehlschlägt, bleibt das Objekt, dem zugewiesen wurde, in seinem ursprünglichen, gültigen Zustand. Beim swap kann nichts fehlschlagen, da lediglich Zeiger und Integer getauscht werden müssen. Das other-objekt übernimmt den Speicher des alten Objekts und zerstört diesen beim Verlassen der Funktion. Hier noch einmal die vollständige Klasse:
class object_array
{
public:
object_array():
array(0),
size(0)
{}
explicit object_array(std::size_t size):
array(new object[size]),
size(size)
{}
object_array(const object_array& other):
array(0),
size(other.size)
{
if(other.array && size)
{
try
{
array = new object[size];
std::copy(other.array, other.array + size, array);
}
catch(...)
{
delete [] array; // Im Fehlerfall speicher freigeben, um kein Speicherleck zu hinterlassen.
throw;
}
}
}
~object_array()
{
delete [] array;
}
object_array& operator=(object_array other)
{
swap(other);
return *this;
}
void swap(object_array& other)
{
std::swap(array, other.array);
std::swap(size, other.size);
}
object& operator[](std::size_t index)
{
assert(index < size);
return array[index];
}
object operator[](std::size_t index) const
{
assert(index < size);
return array[index];
}
private:
object* array;
std::size_t size;
};