ubuntuusers.de

C++ und dessen komisches OOP-Verständnis (oder meins?)

Status: Ungelöst | Ubuntu-Version: Nicht spezifiziert
Antworten |

Marc_BlackJack_Rintsch Team-Icon

Ehemalige
Avatar von Marc_BlackJack_Rintsch

Anmeldungsdatum:
16. Juni 2006

Beiträge: 4673

Wohnort: Berlin

Ich verzweifel hier gerade wieder an C++ und dessen eigenartige Vorstellung von OOP. Ich habe eine Basisklasse und mehrere davon abgeleitete Unterklassen. Nun möchte ich gerne eine Funktion, die einen Container mit Exemplaren der Basisklasse bekommt, mit Containern mit Exemplaren der abgeleiteten Klasse aufrufen. Dat jeht aber nich. Minimalbeispiel:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <vector>

using namespace std;

class A
{
public:
    void foo() {}
};

class B : public A
{
};

void bar(vector<A> va)
{
}

int main()
{
    vector<B> vb;
    bar(vb);
}

Compiler beschwert sich, dass die Umwandlung vom Typ von vb in den von va nicht geht.

Welche Verrenkungen muss ich da machen, dass das geht?

BadBoy

Avatar von BadBoy

Anmeldungsdatum:
25. Oktober 2007

Beiträge: 479

ich hatte mal ein ähnliches Problem, als ich ein Menü erstellen wollte, und jeder Menüpunkt eine eigene Klasse darstellte. Ich hab's folgendermaßen gelöst:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class MenuBase
{
  public:
     virtual std::string getName();
};

class PlaylistMenu : public MenuBase
{
  public:
     std::string getName() { return "Playlist"; }
};

int main()
{
  MenuBase *playlist = new PlaylistMenu;
return 0;
}

und irgendwo wird dann halt noch std::vector<MenuBase*> verwendet.

Hilft dir das weiter?

Marc_BlackJack_Rintsch Team-Icon

Ehemalige
(Themenstarter)
Avatar von Marc_BlackJack_Rintsch

Anmeldungsdatum:
16. Juni 2006

Beiträge: 4673

Wohnort: Berlin

@BadBoy: So wirklich glücklich bin ich damit nicht, weil ich schon einen vector von Bs bräuchte, sonst muss ich "casten" wenn ich da Objekte heraus hole. Was ich unschön fände. Ausserdem geht mir da die Typsicherheit verloren:

 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
#include <vector>

using namespace std;

class A
{
public:
    void foo() {}
};

class B : public A
{
};

class C : public A
{
};

void bar (vector<A*> va)
{
    va[0]->foo();
}

int main()
{
    B b;
    C c;
    vector<A*> va;
    va.push_back(&c);
    va.push_back(&b);
    bar(va);
    B x = *static_cast<B*>(va[0]);
}

Als Vergleich dazu mal D:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
class A
{
    public void foo() {}
}

class B : A
{
}

void bar(A[] as)
{
    as[0].foo();
}

void main()
{
    B[] bs = [new B()];
    bar(bs);
    B b = bs[0];
}

claus007

Avatar von claus007

Anmeldungsdatum:
15. Oktober 2008

Beiträge: 90

Hi,

das geht aus zwei Gründen nicht. Und dein Compiler hat recht ☺

1: Zum einen erkennt er nicht das man Vector<A> in Vector<B> oder umgekehrt umwandel kann. Is einfach so hat was mit Templates zu tun

2: Zum anderen hast du einen Vector, d.h. die Elemente liegen hintereinander in einem großen Speicherbereich mit einem festen Bereich pro Element, eine abgeleitete Klasse wäre Größer als die Basisklasse und würde das nächste Element überschreiben.

Die Lösung ist ganz einfach: nimm' Zeiger und arbeite nur mit der Basisklasse.

Marc_BlackJack_Rintsch Team-Icon

Ehemalige
(Themenstarter)
Avatar von Marc_BlackJack_Rintsch

Anmeldungsdatum:
16. Juni 2006

Beiträge: 4673

Wohnort: Berlin

@claus007: Zeiger sind fehleranfälliger und nur mit der Basisklasse kann ich nicht arbeiten, denn ich brauche an der Stelle wo ich den vector erzeuge schon die Unterklassen und deren Funktionalität.

Die Lösung war aber in der Tat ganz einfach: ein Template für die generische Funktion:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
template<class T>
T choose(vector<T> items, const string &prompt)
{
    cout << prompt << endl;
    while (true) {
        int nr = 1;
        foreach (const T &item, items) {
            cout << " (" << nr++ << ") " << item.getName() << endl;
        }
        unsigned int result;
        cin >> result;
        if (result < items.size()) return items[result - 1];
        cout << "Falsche Antwort!" << endl;
    }
}

claus007

Avatar von claus007

Anmeldungsdatum:
15. Oktober 2008

Beiträge: 90

Marc 'BlackJack' Rintsch schrieb:

@claus007: Zeiger sind fehleranfälliger

jeepp, wirst aber auf dauer nicht drum rum kommen...

und nur mit der Basisklasse kann ich nicht arbeiten, denn ich brauche an der Stelle wo ich den vector erzeuge schon die Unterklassen und deren Funktionalität.

Also aus meiner Erfahrung raus kann ich dir sagen, wenn man ständig wirklich mit der abgeleiteten Klasse arbeitet/arbeiten muss, ist die Schnittstelle in der Bassiklasse meist nicht ordentlich definiert ist...

Die Lösung war aber in der Tat ganz einfach: ein Template für die generische Funktion

Aber generell kannst Du nicht immer templates verwenden... der richtige Weg wären hier Zeiger. Kleiner Tip, verwende Refcounted-Autopointer die sind erheblich sicherer...

Marc_BlackJack_Rintsch Team-Icon

Ehemalige
(Themenstarter)
Avatar von Marc_BlackJack_Rintsch

Anmeldungsdatum:
16. Juni 2006

Beiträge: 4673

Wohnort: Berlin

@claus007: Die Basisklasse kann in diesem Fall fast nichts, also kann man mit der auch wenig anfangen. Die hat nur ein Feld name und eine entsprechende getName()-Methode. Die abgeleiteten Klassen machen grundverschiedene Dinge, mit der einzigen Gemeinsamkeit, dass sie "benannte" Objekte sind, die man in die generische Funktion stecken kann, die wiederum dem Benutzer eine Auswahl präsentiert. Wie macht man das ordentlicher/sauberer in C++?

Hello_World

Anmeldungsdatum:
13. Juni 2006

Beiträge: 3620

Wenn man Polymorphismus in C++ verwenden will, dann muss man Objekte auf dem Heap allokieren und Zeiger auf diese Objekte verwalten - genau wie in so ziemlich jeder anderen Sprache auch, nur dass Zeiger in Java oder C# Referenzen heißen.

Allerdings kann man auch einen vector<B*> nicht an eine Funktion übergeben, die einen vector<A*> haben will. By Reference geht nicht, weil die Funktion möglicherweise irgendein A* in den B*-Container speichern will. By Value geht ebenfalls nicht, weil der Template-Mechanismus nichts von Vererbung weiß und er sich ja nicht irgendwelche Kopierkonstruktoren ausdenken kann. Wenn Du also wirklich einen vector<A*> hast, und ihn an eine Funktion übergeben willst, die einen vector<B*> nimmt, musst Du ihn kopieren (oder unsauber programmieren).

Was die "Fehleranfälligkeit" von Zeigern angeht: Wenn man immer brav std::tr1::shared_ptr<T> statt T* verwendet, sollte es keine Probleme geben, den Speicher wieder freizugeben. Und Pointerarithmetik muss man ja nicht benutzen.

Ein Hinweis noch: Übergib keine Container by value, das macht die Sache nur unnötig langsam. Verwende stattdessen const std::vector<T> & (& damit der Parameter by reference übergeben wird, const um anzuzeigen, dass Du den Container nicht modifizieren wirst). Außerdem willst Du Dir vielleicht die [Pointer Container Library http://www.boost.org/doc/libs/1_38_0/libs/ptr_container/doc/ptr_container.html] des boost-Projekts ansehen. Boost ist sowieso etwas das jeder C++-Programmierer kennen sollte.

claus007

Avatar von claus007

Anmeldungsdatum:
15. Oktober 2008

Beiträge: 90

Marc 'BlackJack' Rintsch schrieb:

Wie macht man das ordentlicher/sauberer in C++?

Also meiner Erfahrung nach gibt's kein richtig und kein falsch - es gibt lediglich design's die sich später positiv oder negativ auf das vorankommen deiner arbeit auswirken ☺

Ich versuche mich in solchen Sachen an Java zu orientieren....

Was dieses konkrete Beispiel anbelangt, hab ich ein Bild mitgeschickt, ABC ist z.Bsp. nachher eine konkrete Klasse. Die Execute Klasse muss nicht sein...

Hello World schrieb:

Und Pointerarithmetik muss man ja nicht benutzen.

jop - zustimmung - muß und solltest du nicht ☺

Bilder

Marc_BlackJack_Rintsch Team-Icon

Ehemalige
(Themenstarter)
Avatar von Marc_BlackJack_Rintsch

Anmeldungsdatum:
16. Juni 2006

Beiträge: 4673

Wohnort: Berlin

@claus007: Interfaces gibt's doch in C++ nicht!? Da müsste man dann nach dem Bild auch Mehrfachvererbung verwenden!? In Deinem Entwurf haben die ABC und XYZ auch "zuviel" miteinander zu tun.

In Java ist das ja auch alles kein Problem, da kann man ja auch einfach eine List<B> übergeben, wenn eine List<A> erwartet wird und B von A abgeleitet ist. Mal etwas konkreter meins in Java:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public abstract class Named {

    private String name;

    public Named(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    @Override
    public String toString() {
        return name;
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class Attack extends Named {

    public Attack(String name) {
        super(name);
    }

    public void apply(Creature enemy) {
        // Give da bastard a good beating here.
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import java.util.List;

public class Creature extends Named {

    private List<Attack> attacks;

    public Creature(String name, List<Attack> attacks) {
        super(name);
        this.attacks = attacks;
    }

    public List<Attack> getAttacks() {
        return attacks;
    }
}
 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
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import org.apache.commons.io.IOUtils;
import org.apache.commons.io.LineIterator;

public class Main {

    private static <T extends Named> T chooseNamed(List<T> namedItems,
            String prompt) throws IOException {

        System.out.println(prompt);
        LineIterator lines = IOUtils.lineIterator(System.in, "utf-8");
        while (true) {
            int nr = 1;
            for (T item : namedItems) {
                System.out.println(String.format(" (%d) %s", nr++, item
                        .getName()));
            }
            try {
                return namedItems.get(Integer.parseInt(lines.nextLine()) - 1);
            } catch (NumberFormatException e) {
                System.out.println("No valid number!");
            } catch (IndexOutOfBoundsException e) {
                System.out.println(String.format(
                        "Please enter a number between 1 and %d!", namedItems
                                .size()));
            }
        }
    }

    public static void main(String[] args) throws Exception {
        List<Creature> creatures = new ArrayList<Creature>();
        creatures.add(new Creature("Godzilla", null));
        Creature attacker = chooseNamed(creatures, "Who should attack?");

        ArrayList<Attack> attacks = new ArrayList<Attack>();
        attacks.add(new Attack("roundhouse kick"));
        Attack attack = chooseNamed(attacks, "Choose move:");

        attack.apply(attacker); // Godzilla kicks itself. :-)
    }
}

Attack wäre eigentlich nochmal abstract und es gäbe dann verschiedene Unterklassen, deren Angriffe sich verschieden berechnen.

Bei der Umsetzung in C++ hänge ich jetzt gerade bei der gegenseitigen Abhängigkeit von Attack und Creature. Wie ist denn da die übliche Lösung?

claus007

Avatar von claus007

Anmeldungsdatum:
15. Oktober 2008

Beiträge: 90

Marc 'BlackJack' Rintsch schrieb:

@claus007: Interfaces gibt's doch in C++ nicht!? Da müsste man dann nach dem Bild auch Mehrfachvererbung verwenden!?

hmmja, ☺ par definition gibt's keine Interfaces → aber nichts hindert dich daran klassen zu erstellen, die nur pure-virtual methoden haben.

Mehrfachvererbung ist nur dann schlimm wenn du wirklich zwei implementierte Basis-Klassen hast, weil du dann nämlich immer Probleme haben wirst festzustellen welche Methode er aufruft, wenn du dich an die Interface-Strategie hälst hast diese Probleme nicht....

In Deinem Entwurf haben die ABC und XYZ auch "zuviel" miteinander zu tun.

Aber du kannst/brauchst dann nur mit der Basis-Klasse "MenuItem" arbeiten (!) weil die eigentliche Implementierung in den abgeleiteten Klassen stattfindet.

Zu deinem Beispiel:

  • ja ok technisch in Java kein Problem, allerdings mit einem guten Klassen Modell hättest du auch in C++ kein Problem

  • Übringens die Klasse Named wird dir so auch immer Probleme machen - in Java wird eben genau sowas über ein Interface erschlagen

Marc_BlackJack_Rintsch Team-Icon

Ehemalige
(Themenstarter)
Avatar von Marc_BlackJack_Rintsch

Anmeldungsdatum:
16. Juni 2006

Beiträge: 4673

Wohnort: Berlin

@claus007: Welche Probleme wird Named machen, die durch ein Interface erschlagen werden? Was ist an dem Klassenmodell schlecht?

Ich will keine generische UI bauen, sprich nichts davon soll eine Bibliothek für Unbekannte sein, die Klassen gehören alle zu *meiner* Anwendung. Also sowas wie "man soll grundsätzlich nur gegen Interfaces programmieren" ist für mich kein Argument und Code der auf Teufel komm raus überall jedes nur erdenkliche Entwurfsmuster verwendet um "flexibel" und "erweiterbar" zu sein, ist in meinen Augen kein guter Entwurf (für "Nicht-Bibliotheken). Ich stehe mehr auf das KISS-Prinzip – Keep It Simple and Stupid. Solange ich nicht ein Named brauche, dass nicht von der abstrakten Klasse abgleitet wird, sehe ich keinen Grund für ein Interface. *Sollte* ich so etwas brauchen, ist es zumindest in Java, ein leichtes der IDE zu sagen, dass sie doch bitte aus der abstrakten Klasse ein Interface heraus ziehen soll.

claus007

Avatar von claus007

Anmeldungsdatum:
15. Oktober 2008

Beiträge: 90

äähhh ja, erst mal sorry wenn es so rüberkommt ich will nichts schlecht machen...

Und natürlich kannst Du programmieren wie Du willst! ☺ und ich werde keine Grundsatz Diskussion anfangen eie weit es sinn macht sich an die Regeln der objekt orientierung zu halten.

☺ Machen wir's doch ganz einfach - das ist jetzt echt nicht böse gemeint aber- schreib weiter an deinem Programm und wenn Du merkst dass Du doch mal mit oder wegen dieser Klasse Probleme hast, denk' an mich ok ? ☺ ( und wenn du cool bist schreibst du mir was )

In diesem Sinne ein schönen ersten Mai & evtl. ein schönes langes Wochenende!

Hello_World

Anmeldungsdatum:
13. Juni 2006

Beiträge: 3620

Marc 'BlackJack' Rintsch schrieb:

@claus007: Interfaces gibt's doch in C++ nicht!?

Interfaces sind in C++ unnötig, da man das alles per Mehrfachvererbung lösen kann.

In Java ist das ja auch alles kein Problem, da kann man ja auch einfach eine List<B> übergeben, wenn eine List<A> erwartet wird und B von A abgeleitet ist.

Unsinn, das geht auch in Java nicht und das ist völlig richtig so. Folgendes ist nicht compilierbar:

1
2
3
4
5
6
7
8
class B {}
class D extends B {}
class Main {
    void f(LinkedList<B> l) {}
    public static void main(String[] args) {
        f(new LinkedList<D>());
    }
}

f könnte versuchen, die Anweisung f.add(new B()); durchzuführen. Das kann aber nicht gelingen, da in einer LinkedList<D> eben nur Ds sein dürfen, daher verhindert der Compiler das. Was in Java geht ist folgendes: void f(LinkedList<? extends B> l);. Diese Methode könnte dann wirklich eine LinkedList<D> nehmen, aber dafür keine neuen Elemente einfügen. Eine derartige Funktionalität gibt es in C++ nicht.

Hello_World

Anmeldungsdatum:
13. Juni 2006

Beiträge: 3620

War das Problem damit jetzt eigentlich gelöst?

Antworten |