• Hallo Gast, du kaufst gerne günstig ein und erfährst oft vor deinen Freunden von interessanten Angeboten? Dann kannst du dein Talent als Schnäppchenjäger jetzt zu Geld machen und anderen PCGH-Lesern beim Sparen helfen! Schau einfach mal rein - beim Test der Community Deals!

Diamond-Vererbung schlechter Programmierstil? Lösung für Interface-Problem

Crysis nerd

Freizeitschrauber(in)
Diamond-Vererbung schlechter Programmierstil? Lösung für Interface-Problem

Guten Tag ihr Lieben,

ich bin schon öfters auf ein Problem gestoßen, welches im Zusammenhang mit Interfaces auftritt. Mit Interface meine ich eine Klasse, die pure virtual ist (also alle Funktionen müssen überschrieben werden) und die sozusagen nur der Leitfaden für andere Klassen ist. Das wird, wenn ich das richtig verstanden habe, oft bei Spiele-Engines verwendet, um dem Programmierer vom Spiel die Funktionen bereitzustellen aber ohne den Funktionsrumpf (den Engine-Source) zu offenbaren.

Wer schon durch die Überschrift weiß worauf das hinausläuft, braucht meine Erklärung der Situation nicht lesen.

Mal ein Beispiel dazu:
Bei einem UI-System habe ich die Klasse "IButton" (I -> Interface), die rein virtuell ist. Außerdem habe ich die Klasse "CButton", welche von IButton erbt.
Code:
class IButton
{
	public:
	
	virtual void SetLabel(std::string text) = 0;
}

class CButton : public IButton
{
	public:
	
	void SetLabel(std::string text) { /*do something*/ }
	
	private:
	std::string m_Label;
}

Soweit so klar. Nun habe ich aber mehrere UI Elemente, die auch alle ein paar gleiche Funktionen haben, wie zb. die Measure Funktion, die später die Position der Elemente anhand von Margin, Alignment und ähnlichen Werten errechnen soll.
Also erweitern wir um die Klasse IUIElement und lassen IButton davon erben:
Code:
class IUIElement
{
	public:
	virtual void Measure() = 0;
}

class IButton : public IUIElement
{
	public:
	
	virtual void SetLabel(std::string text) = 0;
}

class CButton : public IButton
{
	public:
	
	void SetLabel(std::string text) { /*do something*/ }
	
	private:
	std::string m_Label;
}

Aber jetzt habe ich ein Problem:
Wo implementier ich den Funktionsrumpf von Measure() ?
Das könnte man jetzt in IUIElement direkt machen, aber dadurch wäre das Interface verletzt, weil im Interface keine Methoden definiert werden.
Zweite Möglichkeit ist, in jedem UI-Element die Funktion einzubauen, hier zb. in CButton. Würde sich mit dem Interface vertragen ABER man müsste auch in CDropDownBox und CSlider und und und die selbe Funktion definieren (viel doppelter Code, angenommen die Funktion ist überall gleich).

Die Lösung ist Diamond Vererbung: Ich baue eine Klasse CUIElement, die von IUIElement erbt. Und CButton erbt von CUIElement.
Dann hätten wir das:
Code:
class IUIElement
{
	public:
	virtual void Measure() = 0;
}

class IButton : virtual public IUIElement
{
	public:
	
	virtual void SetLabel(std::string text) = 0;
}

class CUIElement : virtual public IUIElement
{
	public:
	void Measure() { /* do something*/ }
}


class CButton : public IButton, public CUIElement
{
	public:
	
	void SetLabel(std::string text) { /*do something*/ }
	
	private:
	std::string m_Label;
}


TL;DR HIER WEITERLESEN
Und die Welt ist wieder korrekt. Fast.
Ich lese hin und wieder, dass diese Diamond Vererbung Nachteile mit sich bringt. Zb. werden vom Compiler und während der Laufzeit spezielle Zeiger-Listen oder so angefertigt, um dann auf die richtige Klasse zu zeigen. Das merkt man beim Debuggen, dass man Werte von Visual Studio (in meinem Fall) nicht angezeigt werden können, sondern nur so eine komische Zeigerliste gezeigt wird. Außerdem höre ich, dass es schlechter stil ist und so weiter.

Kennt ihr euch damit aus und könnte ihr mir sagen, ob man das wirklich Vermeiden sollte?
Oder könnte ihr mir andere Lösungvorschläge sagen, die für ein solches Problem angebracht wären?
Oder seid ihr, wie mein Kollege sebi707, der Meinung, dass man Interfaces schon mal für Funktionen nutzen darf?

Hoffe ich konnte es einigermaßen erklären :)
Liebe Grüße,

Crysis nerd
 
Zuletzt bearbeitet:

fadade

BIOS-Overclocker(in)
AW: Diamond-Vererbung schlechter Programmierstil? Lösung für Interface-Problem

Kennt ihr euch damit aus und könnte ihr mir sagen, ob man das wirklich Vermeiden sollte?
Oder könnte ihr mir andere Lösungvorschläge sagen, die für ein solches Problem angebracht wären?

Oder seid ihr, wie mein Kollege sebi707, der Meinung, dass man Interfaces schon mal für Funktionen nutzen darf?

Jup, um es mal kurz zu machen :D
Was auch eine gute Lösung ist, dass du im Interface die am häufigsten benötige Implementierung für Measure einbaust und sie dann in den Klassen nochmal überschreibst, wo sie dann anders sind.

Also hier hättest du dann im Interface irgendnen Methoden-Rumpf für Measure und die Button-Klasse überschreibt den kram dann nochmal.
Ansonsten bin ich in Stilfragen wohl eher der falsche Ansprechpartner :fresse: :ugly:
 

bingo88

PCGH-Community-Veteran(in)
AW: Diamond-Vererbung schlechter Programmierstil? Lösung für Interface-Problem

Mit dem Offenlegen des Codes hat das nichts zu tun (es sei denn, man hat nur inline-Methoden), sondern da geht es um das Trennen der konkreten Implementierung von der Schnittstelle. So kann man z. B. Plattformunabhängigkeit erreichen oder kann die Implementierung tauschen, ohne den Client-Code anpassen zu müssen (sieh dir mal das PIMPL/Pointer-to-Implementation-Idiom für mehr Infos an).

Interfaces mit implementierten Methoden gibt es übrigens auch, das nennt sich dann abstrakte Basisklasse. Das ist bei deinem Beispiel vielleicht sogar besser, da man die gemeinsamen Methoden in die (nun abstrakte) Oberklasse auslagert. Den Code hast du ja trotzdem in der Regel in separaten Dateien (Header + Implementierung) und bei ner Lib liefert man ja nur Header + Lib-Datei aus, keinen Code (darum kümmert sich dann der Linker). Der Weg über abstrakte Basisklassen ist auch meistens der Weg, den du bei Sprachen einschlagen musst, die keine Merhfachvererbung ermöglichen (Java, C# z. B.). Bei solchen Sprachen hat man das Problem auch gar nicht erst ;-)

Grundsätzlich ist virtuelle Vererbung nichts "böses", ähnlich wie bei goto (vllt. was übertrieben ^^) muss man den Einsatz aber wohl überlegen, da es unter anderem zu Verständnisproblemen und unerklärlichen Fehlern führen kann. Ich versuche das bei meinen Projekten so gut es geht zu vermeiden und eigentlich klappt das auch immer.
 
TE
Crysis nerd

Crysis nerd

Freizeitschrauber(in)
AW: Diamond-Vererbung schlechter Programmierstil? Lösung für Interface-Problem

Danke für eure beiden Antworten, es hat doch schon geholfen. Bingo88, du hast mir auch nochmal einige Sachen erklärt, damit ich sie jez besser verstanden habe :D

LG,
Lukas
 
Oben Unten