Idiome in der Softwareentwicklung: Das Copy-and-Swap-Idiom

Das Copy-and-Swap Idiom erlaubt es in C++, Operation mit Rollback Semantik zu implementieren.

In Pocket speichern vorlesen Druckansicht 41 Kommentare lesen
Lesezeit: 6 Min.
Von
  • Rainer Grimm
Inhaltsverzeichnis

Ein Idiom ist eine Architektur- oder Entwurfsmusterimplementierung in einer konkreten Programmiersprache. Ihre Anwendung ist idiomatisch für eine Programmiersprache. Heute stelle ich das Copy-and-Swap-Idiom in C++ vor, das die Strong-Exception-Safety-Garantie anbietet.

Modernes C++ – Rainer Grimm

Rainer Grimm ist seit vielen Jahren als Softwarearchitekt, Team- und Schulungsleiter tätig. Er schreibt gerne Artikel zu den Programmiersprachen C++, Python und Haskell, spricht aber auch gerne und häufig auf Fachkonferenzen. Auf seinem Blog Modernes C++ beschäftigt er sich intensiv mit seiner Leidenschaft C++.

Bevor ich über das Copy-and-Swap Idiom schreibe, sollte ich zuerst die Garantie rund um Exceptions klären. Hier sind ein paar allgemeine Gedanken zur Fehlerbehandlung:

Fehlerbehandlung ist ein wichtiger Bestandteil guter Software. Daher bieten die C++ Core Guidelines rund 30 Regeln für die Fehlerbehandlung an.Welche Aspekte gehören laut der Guidelines zur Fehlerbehandlung?

  • Detecting an error
  • Transmitting information about an error to some handler code
  • Preserve the state of a program in a valid state
  • Avoid resource leaks

Man sollte Exceptions für die Fehlerbehandlung verwenden. David Abrahams, einer der Gründer der Boost-Library und ehemaliges Mitglied des ISO-C++-Standardisierungskomitees, formalisiert in seinem Dokument "Exception-Safety in Generic Components", was Exception-Safety bedeutet.

Abrahams Guarantees

Die Abrahams Guarantees beschreiben einen Vertrag, der grundlegend ist für Exception-Safety. Hier sind die vier Abstufungen des Vertrags:

  1. No-throw guarantee, also known as failure transparency: Operations are guaranteed to succeed and satisfy all requirements even in exceptional situations. If an exception occurs, it will be handled internally and not observed by clients.
  2. Strong exception safety, also known as commit or rollback semantics: Operations can fail, but failed operations are guaranteed to have no side effects, so all data retains their original values.
  3. Basic exception safety, also known as a no-leak guarantee: Partial execution of failed operations can cause side effects, but all invariants are preserved and there are no resource leaks (including memory leaks). Any stored data will contain valid values, even if they differ from what they were before the exception.
  4. No exception safety: No guarantees are made.

Im Allgemeinen sollte man zumindest die grundlegende Ausnahmesicherheitsgarantie (3) implementieren. Das bedeutet, dass im Falle eines Fehlers keine Ressourcen verloren gehen und sich das Programm immer in einem wohldefinierten Zustand befindet. Falls sich das Programm nach einem Fehler nicht in einem wohldefinierten Zustand befindet, gibt es nur noch eine Möglichkeit: Das Programm beenden.

Das Copy-and-Swap-Idiom bietet Strong-Exception-Safety-Garantie (2). Dies ist eine stärkere Garantie als die Basic-Exception-Safety-Garantie (3).

Die swap-Funktion

Damit ein Typ ein regulärer Typ ist, muss er eine swap-Funktion unterstützen. Eine informellere Definition eines regulären Typs ist ein Typ, der sich wie ein int verhält. Über reguläre Typen werde ich in einem der nächsten Beiträge schreiben. Gemäß den Regeln "C.83: For value-like types, consider providing a noexcept swap function" und "C.85: Make swap noexcept" der C++ Core Guidelines sollte eine swap-Funktion nicht fehlschlagen und noexcept sein. Der folgende Datentyp Foo hat eine swap-Funktion.

class Foo {
 public:
    void swap(Foo& rhs) noexcept {
        m1.swap(rhs.m1);
        std::swap(m2, rhs.m2);
    }
 private:
    Bar m1;
    int m2;
};

Der Einfachheit halber sollte man in Erwägung ziehen, eine nicht-member swap-Funktion zu unterstützen, die auf der bereits implementierten swap-Member-Funktion basiert.

void swap(Foo& a, Foo& b) noexcept {
    a.swap(b);
}

Wenn man keine swap-Funktion zur Verfügung stellt, greifen die Algorithmen der Standardbibliothek, die ein swap erfordern (z. B. std::sort und std::rotate), auf das std::swap-Template zurück, das auf der Grundlage Move Semantik implementiert ist.

template<typename T>
void std::swap(T& a, T& b) noexcept {
    T tmp(std::move(a));
    a = std::move(b);
    b = std::move(tmp);
}

Der C++ Standard bietet mehr als 40 Überladungen von std::swap. an Man kann die swap-Funktion als Baustein für viele Idiome verwenden, z. B. für die Copy-Konstruktion oder die Move-Zuweisung. Das bringt mich zum Copy-and-Swap Idiom.

Copy-And-Swap

Wenn das Copy-and-Swap-Idiom zum Einsatz kommt, um den Copy- und Move-Konstruktor zu implementieren, muss die eigene swap-Funktion entweder als Member-Funktion oder Friend-Funktion definiert werden. Ich habe der Klasse Cont eine swap-Funktion hinzugefügt und verwende sie in der Copy- und Move-Zuweisung.

class Cont {
 public:
    // ...
    Cont& operator = (const Cont& rhs);
    Cont& operator = (Cont&& rhs) noexcept;
    friend void swap(Cont& lhs, Cont& rhs) noexcept {
        swap(lhs.size, rhs.size);
        swap(lhs.pdata, rhs.pdata);
}
 private:
    int* pData;
    std::size_t size;
};

Cont& Cont::operator = (const Cont& rhs) {
    Cont tmp(rhs);              // (1)
    swap(*this, tmp);           // (3)
    return *this;
}

Cont& Cont::operator = (Cont&& rhs) {
    Cont tmp(std::move(rhs));  // (2)
    swap(*this, tmp);          // (3)
    return *this;
}

Beide Zuweisungsoperatoren erstellen eine temporäre Kopie tmp des Quellobjekts (1 und 2) und wenden dann die swap-Funktion auf sie an (3 und 4). Wenn die verwendeten swap-Funktionen noexcept sind, unterstützen der Zuweisungsoperator copy und der Zuweisungsoperator move die Strong-Exception-Safety-Garantie. Das bedeutet, dass beide Zuweisungsoperatoren garantieren, dass die Effekte des Operationsaufrufs im Falle eines Fehlers vollständig zurückgenommen werden, so als wäre der Fehler nie aufgetreten.

Operationen, die das Copy-and-Swap-Idiom unterstützen, ermöglichen es, in einem transaktionsbasierten Stil zu programmieren. Man bereitet eine Operation vor (arbeitet auf der Kopie) und veröffentlicht die Operation (tauscht das Ergebnis aus), wenn sie in Ordnung ist. Das Copy-and-Swap-Idiom ist ein sehr mächtiges Idiom, das ziemlich gerne angewendet wird.

Concurrency: Die Änderung findet auf einem lokalen Objekt statt. Dies ist per Definition thread-sicher, weil die Daten nicht gemeinsam genutzt werden. Wer mit den Änderung fertig ist, überschreibt die gemeinsam genutzten Daten mit den lokalen Daten auf geschützte Weise.

Versionskontrollsystem: Zuerst checkt man die Daten aus und erhältt eine lokale Kopie. Wenn die Änderungen fertig sind, werden sie wieder veröffentlicht. Eventuell muss dazu ein merge-Konflikt gelöst werden. Wenn man den merge-Konflikt nicht lösen kann, wirft man die lokale Änderung weg und checkt noch einmal aus.

Wenn eine swap-Funktion auf der Copy-Semantik statt auf der Move-Semantik basiert, kann eine swap-Funktion aufgrund von Speichermangel fehlschlagen. Die folgende Implementierung widerspricht der C++ Core Guidelines Regel "C++94: A swap function must not fail": Dies ist die C++98-Implementierung von std::swap.

template<typename T>
void std::swap(T& a, T& b) {
    T tmp = a;
    a = b;
    b = tmp;
}

In diesem Fall kann das Speichermangement eine std::bad_alloc-Ausnahme verursachen.

Partial Function Application ist eine Technik, bei der eine Funktion einige ihrer Argumente bindet und eine Funktion mit weniger Argumenten zurückgibt. Diese Technik ist verwandt mit Currying, das in den funktionalen Sprachen gerne verwendet wird.

Meine kleine Weihnachtspause

In den nächsten zwei Wochen werde ich keinen Blogartikel veröffentlichen. Mein nächster Artikel erscheint daher am 9ten Januar. Eine ruhige und besinnliche Zeit. (rme)