Liebe Leserinnen, liebe Leser,

heute teile ich einen Trick für die Erstellung einer abstrakten Basisklasse, wenn es keine Möglichkeit gibt, eine benutzerdefinierte Funktion als rein virtuell zu deklarieren. Solche Situation könnte entstehen, wenn man nur eine Sammlung von Structs mit gemeinsamen Klassenvariablen haben will. Leider hat C++ kein Stichwort wie “abstract” oder “interface” oder etwas Ähnliches, um einem in solchen Fällen zu helfen, also was könnte gemacht werden? Hier gibt’s einen ersten Versuch:

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
struct IBase
{
    explicit IBase(const std::string& baseVal)
        : mBaseVal(baseVal) {}
    
    virtual ~IBase() {}
    
    std::string mBaseVal;
};

struct Derived : public IBase
{
    Derived()
        : IBase("MyObjStuff") {}
        
    std::string mDerivedVal;
};

int main() {

    IBase base("Base value");  // This shouldn't build
    
    return 0;
}

Das sieht gut aus, aber das wird leider kompilieren, weil es keine rein virtuellen Funktionen gibt. Als Behebung könnte man mBaseVal als privat deklarieren und eine rein virtuelle Getter-Funktion definieren, aber das würde zu eine zusätzliche unnötige Komplexität führen. Weil die einzige Funktion, die wir jetzt in virtual umwandeln können, der Destructor ist, könnte man das in rein virtuell umwandeln:

C++

1
2
3
4
5
6
7
8
9
struct IBase
{
    explicit IBase(const std::string& baseVal)
        : mBaseVal(baseVal) {}
    
    virtual ~IBase() = 0;
    
    std::string mBaseVal;
};

Das ist besser, aber jetzt gibt es keine Implementation, die die abgeleitete Klassen aufrufen können (das ist immer gefährlich). Weiterhin, dieser Code wird nicht mehr kompilieren, aber leider nicht wegen der gewünschten Gründe:

Shell

... in function `Derived::~Derived()':
... undefined reference to `IBase::~IBase()'
collect2: error: ld returned 1 exit status

Was ist zu tun? Zu meiner Überraschung, die Sprache verhindert die Erstellung einer Implementation für rein virtuelle Funktionen nicht. Darüber hinaus ist solche Sache erlaubt, solange man vorsichtig genug ist, die Implementation nicht innen der Klasse zu schreiben.

C++

1
2
3
4
5
6
7
8
9
10
11
struct IBase
{
    explicit IBase(const std::string& baseVal)
        : mBaseVal(baseVal) {}
    
    virtual ~IBase() = 0;
    
    std::string mBaseVal;
};

IBase::~IBase() {}

Jetzt soll alles wie erwünscht funktionieren, weil IBase nicht mehr instanziiert werden kann. Meine einzige Sorge ist zu überprüfen, ob der Destructor von mBaseVal mit diesem Setup wirklich aufgerufen wird (meines Wissens nach soll das aufgerufen werden, aber Vorsorgen ist besser als Heilen), also ich ersetze std::string durch etwas Eigenes:

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
38
39
40
41
42
43
44
struct MyObj
{
    explicit MyObj(const std::string& objVal)
        : mObjVal(objVal) {}
    
    ~MyObj() {std::cout << "MyObj destroyed\n";}
    
    std::string mObjVal;
};

std::ostream& operator<<(std::ostream& os, const MyObj& obj)
{
    os << obj.mObjVal;
    return os;
}

struct IBase
{
    explicit IBase(const std::string& baseVal)
        : mBaseVal(baseVal) {}
    
    virtual ~IBase() = 0;
    
    MyObj mBaseVal;
};

IBase::~IBase() {std::cout << "IBase destroyed\n";}

struct Derived : public IBase
{
    Derived()
        : IBase("base val") {}
        
    std::string mDerivedVal;
};

int main() {

    Derived derived;
    derived.mDerivedVal = "derived val";
    std::cout << derived.mDerivedVal << " " << derived.mBaseVal << std::endl;

    return 0;
}

Der Code führt endlich zu das gewünschte Ergebnis:

Shell

derived val base val
IBase destroyed
MyObj destroyed

Die Lektion des Tages ist:

Im Falle man eine abstrakte Klasse braucht, aber eine allgemeine rein virtuelle Funktion kann nicht definiert werden, sollte der Destructor in rein virtuell umgewandelt werden und die Definition außen der Klasse angeboten werden.