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

31

09.12.2009, 23:13

Laguna, das versprochene Beispiel:

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
// Neuer std::vector soll gefüllt werden.

std::vector<MyClass*> Vec;
Vec.push_back(new MyClass());       // OK

Vec.push_back(new MyClass());       // Memory Leak, falls MyClass-Konstruktor

Vec.push_back(new MyClass());       // Exception wirft oder falls new fehlschlägt


// Temporäre Kopie anlegen, wir brauchen die jetzt. Neues Element hinzufügen.

std::vector<MyClass*> Vec2 = Vec;   // OK

Vec2.push_back(new MyClass());      // OK


// Nachträglich Element in alten Vector einfügen, das nicht in Vec2 kopiert werden soll.

Vec.push_back(new MyClass());       // OK


// Schlussendlich (nach irgendwelchem Code) soll der alte Vector wieder 3 Elemente enthalten.

Vec2.resize(3);                     // garantiertes Memory Leak


// Vec soll sortiert werden

std::sort(Vec.begin(), Vec.end());  // OK


// Wir möchten alle Elemente aus Vec2 löschen.

for (int i = 0; i < Vec2.size(); ++i)
    delete Vec[i];                  // OK, alle Elemente in Vec2 korrekt freigegeben.

Vec2.clear();                        // OK, vector mit ungültigen Zeigern geleert.


// Wir möchten Vec1 korrekt freigeben.

// Wir merken, dass wir nicht jedes Element deleten dürfen, da ein Teil des Speichers bereits

// in Vec2 freigegeben wurde. Wir sehen auch sogleich, dass keine Möglichkeit besteht, heraus-

// zufinden, welche Zeiger bereits freigegeben wurden. Nett, wir haben die Wahl zwischen

// doppeltem Delete und Memory Leaks.

Tut mir leid, wenn es ein wenig gekünstelt wirkt, aber ich habe versucht, einige Gefahren in einer Funktion zu vereinen. Stell dir vor, dass diese noch viel fieser werden, wenn implizite Annahmen getroffen werden. Ein Beispiel dafür sind generische Funktionen wie die STL-Algorithmen, die Voraussetzungen an die Typen stellen (z.B. kann std::remove() Elemente überschreiben oder duplizieren). Generell verliert man sehr schnell die Übersicht, sobald ein Container von mehreren Funktionen oder sogar Klassen genutzt wird. Aber selbst bei einfachen Konstrukten ist unverhältnismässig viel Aufmerksamkeit erforderlich. Ausserdem kann Debugging durch undefiniertes Verhalten recht schnell zur Hölle werden.

Zusammengefasst müssen wir die ganze Zeit extrem wachsam sein und können gewisse Fehler dennoch nicht verhindern (einfach weil die STL-Container nicht auf rohe, besitzende Zeiger ausgelegt sind). Das ist ein Rückschritt zu Zeiten, in denen manuelle Speicherverwaltung den Alltag bestimmt hat. Es führt uns weg vom Prinzip der Containerklassen und modernen C++-Errungenschaften wie RAII, welche einem unter anderem gerade diese Aufgabe abnehmen sollen.

dv

Frischling

Beiträge: 30

Wohnort: Südamerikanischer Dschungel

  • Private Nachricht senden

32

10.12.2009, 02:19

Zitat von »"Nexus"«

Laguna, das versprochene Beispiel:

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
// Neuer std::vector soll gefüllt werden.

std::vector<MyClass*> Vec;
Vec.push_back(new MyClass());       // OK

Vec.push_back(new MyClass());       // Memory Leak, falls MyClass-Konstruktor

Vec.push_back(new MyClass());       // Exception wirft oder falls new fehlschlägt


// Temporäre Kopie anlegen, wir brauchen die jetzt. Neues Element hinzufügen.

std::vector<MyClass*> Vec2 = Vec;   // OK

Vec2.push_back(new MyClass());      // OK


// Nachträglich Element in alten Vector einfügen, das nicht in Vec2 kopiert werden soll.

Vec.push_back(new MyClass());       // OK


// Schlussendlich (nach irgendwelchem Code) soll der alte Vector wieder 3 Elemente enthalten.

Vec2.resize(3);                     // garantiertes Memory Leak


// Vec soll sortiert werden

std::sort(Vec.begin(), Vec.end());  // OK


// Wir möchten alle Elemente aus Vec2 löschen.

for (int i = 0; i < Vec2.size(); ++i)
    delete Vec[i];                  // OK, alle Elemente in Vec2 korrekt freigegeben.

Vec2.clear();                        // OK, vector mit ungültigen Zeigern geleert.


// Wir möchten Vec1 korrekt freigeben.

// Wir merken, dass wir nicht jedes Element deleten dürfen, da ein Teil des Speichers bereits

// in Vec2 freigegeben wurde. Wir sehen auch sogleich, dass keine Möglichkeit besteht, heraus-

// zufinden, welche Zeiger bereits freigegeben wurden. Nett, wir haben die Wahl zwischen

// doppeltem Delete und Memory Leaks.



std::vector<MyClass*> zu kopieren, das macht man nicht. Das ist nichts gefinkeltes o.ä., das ist Basiswissen - wenn du in C einen Array von Pointern herumkopierst, wirst du ähnliche Probleme kriegen.

Lösungen:
1. shared_ptr nehmen. Also: std::vector< boost::shared_ptr < MyClass > >
Alternativ dazu tr1::shared_ptr, falls kein Boost erwünscht ist.

2. vector nicht kopieren, sondern Referenzen/Pointer drauf übergeben.


Zu double dispatch:
Es skaliert schlecht, und ist intrusiv - bei neuen Objekttypen muss man bestehenden Code ändern. Ich ziehe hier RTTI vor, um sowas ähnliches wie Multimethoden in C++ zu haben.


Zum allgemeinen Thema Interface vs. Template:

Interfaces sind nichts anderes als "poor man's generic programming concepts". Concepts sind im Prinzip strukturierte Typen zur Compilezeit.

Das ist auch der wichtige Unterschied zwischen Interfaces und Concepts. Interfaces basieren auf nominative Typsysteme (d.h. Typ-Check = überprüfen auf Übereinstimmung des Typnamens), concepts auf strukturelle Typsysteme (Typ-Check = Typ erfüllt eine bestimmte Liste von Anforderungen).

Die STL baut auf Concepts, also zB CopyConstructible, StrictWeakOrdering .... Wenn man in C++ anfängt, concepts einzusetzen, verschwinden große Teile von Klassenhierarchien, weil sie eben nix anderes sind wie Concepts.

Problem: Concepts existieren zurzeit nur indirekt in C++ (das Templatesystem erlaubt es, sie umzusetzen), aber die Checks auf Erfüllung der Anforderungen passiert nicht explizit.

Empfehlenswert zu lesen: http://en.wikipedia.org/wiki/Concept_%28generic_programming%29
~dv();

drakon

Supermoderator

Beiträge: 6 513

Wohnort: Schweiz

Beruf: Entrepreneur

  • Private Nachricht senden

33

10.12.2009, 14:27

Zitat

shared_ptr nehmen. Also: std::vector< boost::shared_ptr < MyClass > >

Wenn man schon boost verwendet, kann man auch gerade pointer container benutzen:
http://www.boost.org/doc/libs/1_40_0/libs/ptr_container/doc/ptr_container.html

34

10.12.2009, 17:12

Container von Pointern zu kopieren hatte ich nicht vor.
Die Boost::ptr_container schauen aber auf jeden fall interessant aus.

Zur RTTI, ich hab das "Effective c++" grade nicht vor mir liegen, aber dort wurde ein selbstgeschriebener Double Dispatch der RTTI aus einigen Gründen vorgezogen.

Trotzdem will ich versuchen, diesen Problemen aus dem Weg zu gehen.

Vll werde ich spasseshalber auch mal beide Systeme implementieren und hier zur weiteren Diskussion Ausschnittsweise darstellen.


So Far...

Laguna

35

10.12.2009, 18:12

Zitat von »"dv"«

std::vector<MyClass*> zu kopieren, das macht man nicht. Das ist nichts gefinkeltes o.ä., das ist Basiswissen - wenn du in C einen Array von Pointern herumkopierst, wirst du ähnliche Probleme kriegen.
Es ist noch einiges Basiswissen, und trotzdem wissen es viele Leute nicht. Es ging übrigens nicht nur ums Kopieren. Und dein Vergleich mit C trifft es gut: Man verwendet unsichere, fehleranfällige und aufwändige Techniken in C++, obwohl man das Problem mit weniger Code besser lösen könnte. Nicht ohne Grund verwende ich keine rohen C-Arrays mehr, sofern ich nicht an irgendwelche Schnittstellen gebunden bin.

Ausserdem würde ich auch eher Pointer-Container als Smart-Pointer verwenden, es sei denn, man braucht deren Flexibilität tatsächlich. shared_ptr wird meines Erachtens überbewertet und noch schnell einmal eingesetzt, weil man sich keine Gedanken um Besitzverhältnisse macht.

dv

Frischling

Beiträge: 30

Wohnort: Südamerikanischer Dschungel

  • Private Nachricht senden

36

10.12.2009, 23:07

Wenn man über die Ownership-Frage nicht Bescheid weiss, ist man in C++ sowieso verloren (aber dieses Wissen ist eigentlich in jeder Sprache fundamental). shared_ptr bietet sich für Ressourcen aber geradezu an dank seines Referenzzählens.
~dv();

Werbeanzeige