Du bist nicht angemeldet.

Stilllegung des Forums
Das Forum wurde am 05.06.2023 nach über 20 Jahren stillgelegt (weitere Informationen und ein kleiner Rückblick).
Registrierungen, Anmeldungen und Postings sind nicht mehr möglich. Öffentliche Inhalte sind weiterhin zugänglich.
Das Team von spieleprogrammierer.de bedankt sich bei der Community für die vielen schönen Jahre.
Wenn du eine deutschsprachige Spieleentwickler-Community suchst, schau doch mal im Discord und auf ZFX vorbei!

Werbeanzeige

1

04.04.2017, 20:49

[C++]Duale Hierarchie

Guten Abend,
ich habe in meinem Projekt bisher alles relativ straight forward implementiert. Es gibt hier die ein oder andere Hierarchie wie z.B. Minion -> Unit -> Entity
Bisher auch kein großes Ding, allerdings möchte ich gerne vermehrt Interfaces mit Dualer Hierarchie benutzen, um eine Schnittstelle für etwaige Nebenschauplätze wie z.B. AI zu bieten, die keine Implementierungsdetails wissen müssen/sollen. Die AI z.B. ist ein separates Projekt und wird von einer anderen Person geschrieben, deswegen möchte ich hier unter anderem die Dependencies so gering wie möglich halten.

Ich habe dann durch diesen Artikel die Idee bekommen, das folgendermaßen zu lösen.
So, und damit folgt das Problem, der Compiler kommt scheinbar nicht auf die Multi Inheritance, bzw. virtuelle Methoden mit der gleichen Signatur, nicht klar (im online compiler bekomme ich nen abstract class fehler, in msvc nen mehrdeutigen Zugriff).

Was mache ich falsch, oder besser - Was ist denn prinzipiell der übliche Weg um mein Ziel zu erreichen?

BlueCobold

Community-Fossil

Beiträge: 10 738

Beruf: Teamleiter Mobile Applikationen & Senior Software Engineer

  • Private Nachricht senden

2

04.04.2017, 21:20

Du hast da ein Diamond-Pattern. Es ist für UnitImpl unklar welche der Methoden genau überschrieben werden soll, denn es gibt folgende zur Auswahl:
- EntityImpl::doSomething (not abstract!)
- Entity/Unit::doSomething (abstract)
Falls du alle zu einer zusammen mergen willst, bleibt dir nur virtual inheritence: class UnitImpl : public virtual Unit, public virtual EntityImpl. Das muss sich auch bei den anderen Klassen durchziehen. Allerdings wirst du dafür eine Warning vom Compiler bekommen. Siehe Stichwort Diamond-Pattern.
Von anderen Sprachen kennt man, dass abstract base-classes 'nicht zählen', weil Interfaces und Classes getrennte Konstrukte sind. für C++ zählt es aber sehr wohl woher es etwas erbt, denn es gibt da für C++ keinen Unterschied zwischen den base-classes (letztlich dürfen ja beide die Methode implementieren, wenn sie wollen, selbst wenn das hier eine der beiden nicht tut). Damit ist es für den Compiler auch wichtig zu wissen, welche der beiden Methoden du genau meinst bei 'override'.
Teamleiter von Rickety Racquet (ehemals das "Foren-Projekt") und von Marble Theory

Willkommen auf SPPRO, auch dir wird man zu Unity oder zur Unreal-Engine raten, ganz bestimmt.[/Sarkasmus]

Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von »BlueCobold« (04.04.2017, 21:25)


3

04.04.2017, 21:37

Danke für die Erklärung mit der virtual inheritance, kannte ich bisher gar nicht. Das ich hier ein Diamond gebaut habe, hatte ich allerdings erkannt ;)

Würde es nicht aber reichen nur die Interfaces, sprich "Entity" und "Unit" virtual zu deklarieren? Wenn ich das richtig verstehe, würde das ja auch das Diamond aufbrechen. Wenn ich das so richtig sehe, wird der Constructor ja nicht direkt aufgerufen, aber das wäre mir bei den Interfaces ja recht herzlich egal, da die keine Custom Constructor besitzen.

BlueCobold

Community-Fossil

Beiträge: 10 738

Beruf: Teamleiter Mobile Applikationen & Senior Software Engineer

  • Private Nachricht senden

4

04.04.2017, 21:49

Joar
Teamleiter von Rickety Racquet (ehemals das "Foren-Projekt") und von Marble Theory

Willkommen auf SPPRO, auch dir wird man zu Unity oder zur Unreal-Engine raten, ganz bestimmt.[/Sarkasmus]

dot

Supermoderator

Beiträge: 9 757

Wohnort: Graz

  • Private Nachricht senden

5

05.04.2017, 10:06

Würde es nicht aber reichen nur die Interfaces, sprich "Entity" und "Unit" virtual zu deklarieren? Wenn ich das richtig verstehe, würde das ja auch das Diamond aufbrechen. Wenn ich das so richtig sehe, wird der Constructor ja nicht direkt aufgerufen, aber das wäre mir bei den Interfaces ja recht herzlich egal, da die keine Custom Constructor besitzen.

Wenn du in C++ sowas wie Interfaces in Sprachen wie C# oder Java haben willst, solltest du generell virtual von den Interface Klassen erben, um das gewünschte Verhalten bestmöglich zu approximieren. Ich persönlich würde auch empfehlen, Copy Constructor und Copy Assignment Operator sowie Destructor der Interface Klassen in der Regel protected zu machen.

Bei mir sieht das in der Regel dann so aus:

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// interface.h

#ifndef INCLUDED_INTERFACE
#define INCLUDED_INTERFACE

#pragma once


#ifdef _MSC_VER
#define INTERFACE __declspec(novtable)
#else
#define INTERFACE
#endif

#endif  // INCLUDED_INTERFACE


C-/C++-Quelltext

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
// Navigator.h

#ifndef INCLUDED_NAVIGATOR
#define INCLUDED_NAVIGATOR

#pragma once

#include <interface.h>


class INTERFACE Navigator
{
protected:
    Navigator() = default;
    Navigator(const Navigator&) = default;
    Navigator& operator =(const Navigator&) = default;
    ~Navigator() = default;
public:
    virtual void reset() = 0;
    virtual void writeWorldToLocalTransform(math::affine_float4x4* M) const = 0;
    virtual void writeLocalToWorldTransform(math::affine_float4x4* M) const = 0;
    virtual void writePosition(math::float3* p) const = 0;
    virtual void buttonDown(GL::platform::Button button, int x, int y) = 0;
    virtual void buttonUp(GL::platform::Button button, int x, int y) = 0;
    virtual void mouseMove(int x, int y) = 0;
    virtual void mouseWheel(int delta) = 0;

};


#endif  // INCLUDED_NAVIGATOR


C-/C++-Quelltext

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

#ifndef INCLUDED_ORBITAL_NAVIGATOR
#define INCLUDED_ORBITAL_NAVIGATOR

#pragma once

#include "Navigator.h"


class OrbitalNavigator : public Navigator
{
    // ...
};

#endif  // INCLUDED_ORBITAL_NAVIGATOR

Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von »dot« (05.04.2017, 10:12)


6

07.04.2017, 16:46

Danke für die Antwort.
Ne andere Frage, weil es mir gerade auffällt. Warum benutzt du einerseits include guards und anderer seits auch noch pragma once? Ist das nicht ein bisschen redundant? Bzw. würden include guards nicht vollkommen ausreichen?

dot

Supermoderator

Beiträge: 9 757

Wohnort: Graz

  • Private Nachricht senden

7

08.04.2017, 00:26

Warum benutzt du einerseits include guards und anderer seits auch noch pragma once? Ist das nicht ein bisschen redundant? Bzw. würden include guards nicht vollkommen ausreichen?

Include Guards verhindern, dass der Compiler den Code mehrmals zu Gesicht bekommt, wenn ein Header mehrfach #included wird. Er muss aber dennoch, zumindest rein prinzipiell, die Datei jedes Mal erneut öffnen, einlesen und durch den Tokenizer und Präprocessor schicken, nur damit letzterer dann den ganzen Inhalt wegschmeißen kann. #pragma once dagegen, erlaubt dem Compiler, sich einfach zu merken, dass eine Datei schon #included wurde und jedes weitere #include dieser Datei direkt komplett zu skippen. #pragma once muss aber nicht von jedem Compiler unterstützt werden. #pragmas sind – per Definition – generell optionale, compilerspezifische Dinge; wenn ein Compiler ein #pragma nicht kennt/nicht unterstützt, so muss er es ignorieren und so weitermachen als ob es einfach nicht dagewesen wäre (was im Fall von #pragma once in der Regel fatal endet). Wenn du standardkonformen C++ Code haben willst, der von jedem standardkonformen C++ Compiler korrekt übersetzt wird, sind Include Guards also strenggenommen im Allgemeinen notwendig.

Viele Compiler, wie z.B. Clang und GCC, sind extra mit der Fähigkeit ausgestattet, Include Guards in ihrer üblichen Form zu erkennen und in diesen Fällen ein erneutes Öffnen und Lesen der Datei zu vermeiden, so wie #pragma once es tun würde. Alles in Allem ist die in meinen Beispielen oben verwendete Kombination von Include Guards und #pragma once die imo beste Lösung: Die Include Guards garantieren, dass der Code auf jeden Fall erst einmal korrekt ist (standardkonformer Weg zur Vermeidung von Mehrfachdefinitionen). Das #pragma once erlaubt Compilern, die #pragma once verstehen, die entsprechende Optimierung anzuwenden. Die Platzierung des #pragma once innerhalb der Include Guards stellt sicher, dass ein hypothetischer Compiler, der zwar Include Guards erkennen und optimieren kann, aber #pragma once nicht versteht, nicht an der Anwendung der Optimierung gehindert wird (wäre das #pragma once außerhalb der Include Guards, so wäre die Datei aus Sicht eines solchen Compilers bei Wiederinkludierung potentiell nicht völlig leer und die Anwendung der Optimierung daher nicht unbedingt sicher).

Dieser Beitrag wurde bereits 2 mal editiert, zuletzt von »dot« (08.04.2017, 00:33)


8

08.04.2017, 11:57

Wow, danke für die Erklärung ;)
Also unterstützt #pragma once prinzipiell den PCH?

dot

Supermoderator

Beiträge: 9 757

Wohnort: Graz

  • Private Nachricht senden

9

08.04.2017, 11:58

Also unterstützt #pragma once prinzipiell den PCH?

Bin mir nicht sicher, was genau du damit meinst. Mit Precompiled Headern (PCH) hat das eigentlich nix zu tun...

10

08.04.2017, 12:03

Ja, ich meine den precompiled Header. Nach meinem Wissen verhindert der PCH das mehrfache parsen und tokenizen ein und der selben include Datei. Oder bin ich hier auf dem falschen Dampfer?

Werbeanzeige