Idiome in der Softwareentwicklung: Covariant Return Type

Für die Implementierung des Prototyp-Musters in C++ ist der Covariant Return Type essenziell.

In Pocket speichern vorlesen Druckansicht 11 Kommentare lesen

(Bild: canbedone / Shutterstock.com)

Lesezeit: 5 Min.
Von
  • Rainer Grimm
Inhaltsverzeichnis

Der Covariant Return Type einer Memberfunktion ermöglicht es einer überschreibenden Memberfunktion, einen engeren Typ zurückzugeben. Das spielt insbesondere beim Implementieren des Prototyp-Musters in C++ eine wichtige Rolle.

Den Covariant Return Type habe ich bereits in meinen vorherigen Artikeln verwendet, ohne ihn jedoch näher zu erklären. Das hole ich heute nach.

Zum Auftakt starte ich mit einer naiven Implementierung.

Das folgende Programm covariantReturnType.cpp wendet das Prototyp-Muster an.

// covariantReturnType.cpp

#include <iostream>
#include <string>

class Window{                                  // (1)
 public:                
    virtual Window* clone() { 
        return new Window(*this);
    }
    virtual std::string getName() const {
        return "Window";
    }                       
    virtual ~Window() {};
};

class DefaultWindow: public Window {          // (2)
     DefaultWindow* clone() override { 
        return new DefaultWindow(*this);
    } 
    std::string getName() const override {
        return "DefaultWindow";
    }   
};

class FancyWindow: public Window {           // (3)
    FancyWindow* clone() override { 
        return new FancyWindow(*this);
    } 
    std::string getName() const override {
        return "FancyWindow";
    }   
};
                
Window* cloneWindow(Window& oldWindow) {    // (4)                
    return oldWindow.clone();
}
  
int main() {

    std::cout << '\n';

    Window window;
    DefaultWindow defaultWindow;
    FancyWindow fancyWindow;

    const Window* window1 = cloneWindow(window);
    std::cout << "window1->getName(): " << window1->getName() << '\n';

    const Window* defaultWindow1 = cloneWindow(defaultWindow);
    std::cout << "defaultWindow1->getName(): " << defaultWindow1->getName() << '\n';

    const Window* fancyWindow1 = cloneWindow(fancyWindow);
    std::cout << "fancywindow1->getName(): " << fancyWindow1->getName() << '\n';
  
    delete window1;
    delete defaultWindow1;
    delete fancyWindow1;

    std::cout << '\n';
  
}

Die Interface-Klasse Window (Zeile 1) hat eine virtuelle clone-Funktion. Die clone-Funktion gibt eine Kopie von sich selbst zurück. Die abgeleiteten Klassen wie DefaultWindow (Zeile 2) und FancyWindow (Zeile 3) geben ebenfalls eine Kopie von sich selbst zurück. Die Funktion cloneWindow (Zeile 4) verwendet die virtuelle Mitgliedsfunktion und erstellt Klone des verwendeten Window. Darüber hinaus habe ich eine virtuelle getName-Funktion implementiert, um den virtuellen Dispatch zu visualisieren.

Die Ausgabe des Programms birgt noch keine Überraschung:

Ist die Verwendung des Covariant Return Type in diesem Beispiel offensichtlich zu erkennen? Die virtuelle clone-Funktion von Window gibt einen Window-Zeiger zurück, aber die virtuelle clone-Funktion von DefaultWindow einen DefaultWindow-Zeiger und die virtuelle clone-Funktion von FancyWindow einen FancyWindow-Zeiger. Das bedeutet, dass der Datentyp kovariant ist: Wenn eine abgeleitete Klassenfunktion einen weiter abgeleiteten Typ zurückgibt als ihre überschriebene Basisklassenfunktion, wird der Rückgabetyp der abgeleiteten Klasse als kovariant bezeichnet.

Außerdem gibt es dabei eine kleine Besonderheit, auf die ich hinweisen möchte: Obwohl die virtuellen Mitgliedsfunktionen clone von DefaultWindow und FancyWindows privat sind, kann die Funktion cloneWindow (Zeile 4) sie aufrufen. Der Grund dafür ist einfach: Die Mitgliedsfunktion cloneWindow verwendet die öffentliche Schnittstelle der Interface-Klasse Window.

Warum aber habe ich diese Form der Implementierung als naiv bezeichnet?

Im Allgemeinen ist die Implementierung der clone-Funktion nicht bekannt. clone gibt einen Zeiger auf ein Window zurück. Zeiger haben von Natur aus einen Makel; sie modellieren zwei völlig unterschiedliche Semantiken: Besitz und Ausleihe.

  • Besitz: Der Aufrufer ist für das Window verantwortlich und muss es zerstören; das ist das Verhalten, das das Programm covariantReturnType.cpp modelliert.
  • Leihen: Der Aufrufer ist für das Window verantwortlich und leiht es sich vom Aufrufer aus.

Lass mich das noch einmal betonen:

  • Besitzer: Du bist der Besitzer des Window. Du musst dich um es kümmern und es zerstören. Wenn nicht, verursachst du ein Speicherleck.
  • Leiher: Du bist nicht der Eigentümer des Window. Du kannst es nicht zerstören. Wenn du es zerstörst, verursachst du ein doppeltes Löschen.

Mit C++11 lässt sich dieses Zeigerproblem einfach umgehen: Verwende entweder einen std::unique_ptr oder einen std::shared_ptr.

Die Rückgabe eines std::unique_ptr bedeutet, dass der Aufrufer der Eigentümer ist. Der std::unique_ptr verhält sich wie eine lokale Variable. Wenn diese ihren Gültigkeitsbereich verlässt, wird sie automatisch zerstört. Außerdem lässt sich der Covariant Return Type simulieren.

// covariantReturnTypeUniquePtr.cpp

#include <iostream>
#include <memory>
#include <string>

class Window{ 
 public:                
    virtual std::unique_ptr<Window> clone() { 
        return std::make_unique<Window>(*this);          // (1)
    }
    virtual std::string getName() const {
        return "Window";
    }                       
    virtual ~Window() {};
};

class DefaultWindow: public Window { 
     std::unique_ptr<Window> clone() override { 
        return std::make_unique<DefaultWindow>(*this);  // (2)
    } 
    std::string getName() const override {
        return "DefaultWindow";
    }   
};

class FancyWindow: public Window { 
    std::unique_ptr<Window> clone() override { 
        return std::make_unique<FancyWindow>(*this);   // (3)
    } 
    std::string getName() const override {
        return "FancyWindow";
    }   
};
                
auto cloneWindow(std::unique_ptr<Window>& oldWindow) {                    
    return oldWindow->clone();
}
  
int main() {

    std::cout << '\n';

    std::unique_ptr<Window> window = std::make_unique<Window>();
    std::unique_ptr<Window> defaultWindow = std::make_unique<DefaultWindow>();
    std::unique_ptr<Window> fancyWindow = std::make_unique<FancyWindow>();

    const auto window1 = cloneWindow(window);
    std::cout << "window1->getName(): " << window1->getName() << '\n';

    const auto defaultWindow1 = cloneWindow(defaultWindow);
    std::cout << "defaultWindow1->getName(): " << defaultWindow1->getName() << '\n';

    const auto fancyWindow1 = cloneWindow(fancyWindow);
    std::cout << "fancyWindow1->getName(): " << fancyWindow1->getName() << '\n';

    std::cout << '\n';
  
}

Der Rückgabetyp der virtuellen clone-Funktion ist nun std::unique_ptr<Window>, und das zurückgegebene Objekt ist ein std::make_unique<Window>(*this) (Zeile 1), ein std::make_unique<DefaultWindow>(*this) (Zeile 2) beziehungsweise ein std::make_unique<FancyWindow>(*this) (Zeile 3).

Die Ausgabe des Programms bleibt identisch mit der des vorherigen:

Der Vollständigkeit halber möchte ich dieses Beispiel noch mit einem std::shared_ptr implementieren.

Die Rückgabe eines std::shared_ptr bedeutet, dass der Aufrufer und der Aufgerufene sich das Eigentum teilen. Wenn weder der Aufrufer noch der Aufgerufene den std::shared_ptr mehr benötigen, wird er automatisch zerstört.

// covariantReturnTypeSharedPtr.cpp

#include <iostream>
#include <memory>
#include <string>

class Window{ 
 public:                
    virtual std::shared_ptr<Window> clone() { 
        return std::make_shared<Window>(*this);
    }
    virtual std::string getName() const {
        return "Window";
    }                       
    virtual ~Window() {};
};

class DefaultWindow: public Window { 
     std::shared_ptr<Window> clone() override { 
        return std::make_shared<DefaultWindow>(*this);
    } 
    std::string getName() const override {
        return "DefaultWindow";
    }   
};

class FancyWindow: public Window { 
    std::shared_ptr<Window> clone() override { 
        return std::make_shared<FancyWindow>(*this);
    } 
    std::string getName() const override {
        return "FancyWindow";
    }   
};
                
auto cloneWindow(std::shared_ptr<Window>& oldWindow) {                    
    return oldWindow->clone();
}
  
int main() {

    std::cout << '\n';

    std::shared_ptr<Window> window = std::make_shared<Window>();
    std::shared_ptr<Window> defaultWindow = std::make_shared<DefaultWindow>();
    std::shared_ptr<Window> fancyWindow = std::make_shared<FancyWindow>();

    const auto window1 = cloneWindow(window);
    std::cout << "window1->getName(): " << window1->getName() << '\n';

    const auto defaultWindow1 = cloneWindow(defaultWindow);
    std::cout << "defaultWindow1->getName(): " << defaultWindow1->getName() << '\n';

    const auto fancyWindow1 = cloneWindow(fancyWindow);
    std::cout << "fancyWindow1->getName(): " << fancyWindow1->getName() << '\n';

    std::cout << '\n';
  
}

Die Portierung des Beispiels covariantReturnTypeUniquePtr.cpp in covariantReturnTypeSharedPtr.cpp ist ein Kinderspiel: Ich ersetze einfach unique durch shared. Damit ergibt sich erwartungsgemäß folgende Ausgabe des Programms:

Mein nächster Artikel fällt ein wenig aus der Reihe, denn ich werde darin nochmals einen Überblick über alles bereits zu Idiomen rund um Polymorphismus und Templates Geschriebene geben.

Alle vier Schulungen finden im Schulungshotel Aramis (Herrenberg) statt. (map)