Liebe Leserinnen, liebe Leser,

in diesem Beitrag werde ich über die Besonderheiten der Überladung der Operators von abgeleiteten Klassen (oder anderen ähnlichen Operators) diskutieren. Das kann im Falle von normalen Klassen unkompliziert scheinen, aber bei Klassenstrukturen ist das kinffliger. Lasst uns die warums mithilfe der folgenden Klassenstruktur untersuchen:

C++

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// myclasses.h
...

class Base {
public:
    virtual ~Base() {}
    // other stuff
};

class Derived : public Base {
public:
    // other stuff
};

class Derived2 : public Derived {
public:
    // other stuff
};

C++

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// myclasses.cpp

#include "myclasses.h"
...

std::ostream& operator<<(std::ostream& os, const Base& b)
{
//     os << b.print();
    os << "I'm a base class";
    return os;
}

std::ostream& operator<<(std::ostream& os, const Derived& b)
{
//     os << b.print();
    os << "I'm a Derived class";
    return os;
}

std::ostream& operator<<(std::ostream& os, const Derived2& b)
{
//     os << b.print();
    os << "I'm a Derived2 class";
    return os;
}

C++

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// main.cpp

#include "myclasses.h"
...

int main()
{
    std::unique_ptr<Base> der = std::make_unique<Derived>();
    std::cout << *der << std::endl;
    
    std::unique_ptr<Base> der2 = std::make_unique<Derived2>();
    std::cout << *der2 << std::endl;
    
    std::unique_ptr<Derived> der3 = std::make_unique<Derived2>();
    std::cout << *der3 << std::endl;
}

Dieser Ansatz ist natürlich naiv, und das wird zu der gewünschten Ergebnis nicht führen, weil es hier keine Art von Vererbung gibt. Die Ausgabe ist selbstverständlich:

Bash

I'm a base class
I'm a base class
I'm a Derived class

Als Behebung könnte man könnte viele Sachen ausprobieren, um die Operatorüberladungen in die Klasse umzuziehen und Vererbung irgendwie einzuschalten…

C++

1
2
3
4
5
6
7
// compiler rejects: can't be friend and virtual simultaneously
friend virtual std::ostream& operator<<(std::ostream& os, const Base& b);

// compiler rejects: must take exactly one argument
virtual std::ostream& operator<<(std::ostream& os, const Base& b);

// and so on...

…aber das Endresultat ist, dass alle diese Versuche mit verschiedenen Fehlermeldungen scheitern werden. Weil es keine einfache Möglichkeit für die direkte Nutzung solcher Operators mit Vererbung gibt, könnte man die gewünschten Ergebnisse durch einn indirekten Ansatz zu erreichen versuchen:

C++

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Base {
public:
    virtual ~Base() {}
    
    virtual std::string print() const {
        std::string message {"I'm a base class"};
        return message;
    }
    // other stuff
};

class Derived : public Base {
public:
    std::string print() const override {
        std::string message {"I'm a Derived class"};
        return message;
    }
    // other stuff
};

class Derived2 : public Derived {
public:
    std::string print() const override {
        std::string message {"I'm a Derived2 class"};
        return message;
    }
    // other stuff
};

C++

1
2
3
4
5
6
7
8
9
10
// myclasses.cpp

#include "myclasses.h"
...

std::ostream& operator<<(std::ostream& os, Base& b)
{
    os << b.print();
    return os;
}

Einerseits wurde eine print() Funktion in die Klasse eingeführt, die alle nötige Vererbungsregeln folgt. Andererseits brauchte die Operatorüberladung nur für die Basisklasse definiert werden, die nur die entsprechende print() Funktion der Klassenstruktur aufruft. Jetzt haben wir die ursprünglich gewünschten Ergebnisse:

Bash

I'm a Derived class
I'm a Derived2 class
I'm a Derived2 class

Das ist gut, aber einem könnte eine weitere öffnentliche Funktion nicht gefallen, besonders wenn man bedenkt, dass die Vorhandensein dieser Funktion ist rein technisch. Anders ausgedrückt, eine sichtbare Implementationseinzelheit ist nicht erwünscht, und man könnte dieses Problem durch kleine Änderungen in der Header-Datei beheben:

C++

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// myclasses.h

class Base {
public:
    virtual ~Base() {}
    
    friend std::ostream& operator<<(std::ostream& os, Base& b);
    
private:
    virtual std::string print() const {
        std::string message {"I'm a base class"};
        return message;
    }
    // other stuff
};

class Derived : public Base {
public:
    // other stuff

private:
    std::string print() const override {
        std::string message {"I'm a Derived class"};
        return message;
    }
};

class Derived2 : public Derived {
public:
    // other stuff

private:
    std::string print() const override {
        std::string message {"I'm a Derived2 class"};
        return message;
    }
};

Durch die Umziehung aller print() Funktionen in den privaten Bereich der Klasse, und durch die Markierung der Operatorüberladung als Freund kann man eine optimale Lösung erstellen, also die Lektion des Tages ist:

Verwende ‘Delegation’ für die Überladung des Ausgabeoperators (und ähnlichen Operators)