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

02.02.2013, 21:06

[C++] Smart Pointer, Raw Pointer oder Handles?

Hallo zusammen,

ich möchte einen simplen Ressourcenmanager für Texturen, Sounds usw. schreiben. Im Groben habe ich mir das folgendermaßen vorgestellt:

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
template<class T>
class ResourceManager
{
public:
    T *getResource(const std::string& name)
    {
        ResourceMap::iterator iter = this->resources.find(name);
        if (iter == this->resources.end())
            return nullptr;
        return &(iter->second);
    }

protected:
    typedef std::unordered_map<std::string, T> ResourceMap;
    
private:
    ResourceMap resources;
};


Mein Sorgenkind ist hier allerdings getResource: Andere Klassen sollen mit Hilfe dieser Methode Ressourcen des Ressourcenmanagers anfordern können. Die Tiles einer Tilemap rufen damit bspw. die Texturen ab, die sie zeichnen sollen.

In meinem kleinen, obigen Beispiel gibt getResource einen nackten Pointer auf eine Ressource innerhalb der ResourceMap zurück - vermutlich kein besonders gutes Design, oder? Ich habe deswegen über mehrere Alternativen nachgedacht:

  1. Anstatt roher Pointer könnte ich shared_ptr verwenden. Dadurch würde die Absicht des Codes deutlicher werden, da die Ressourcen von mehreren Objekten geteilt werden. Aber ich müsste dann auch die einzelnen Ressourcen innerhalb der ResourceMap jeweils in einen shared_ptr packen, oder?

  2. getResource gibt keine Pointer oder Referenzen zurück, sondern Handles. Dadurch könnte ich genau festlegen, was mit den Ressourcen außerhalb des Ressourcenmanagers angestellt werden darf. Ich müsste der ResourceManager-Klasse (bzw. den davon abgeleiteten, spezialisierten Klassen) noch Methoden hinzufügen, die bestimmte "Ressourcen-Queries" beantworten können, z.B. getTextureAnimation(handle).

  3. getResource könnte auch eine Ressourcen-Proxy-Klasse zurückgeben, deren Interface an den externen Gebrauch angepasst ist. Möglicherweise "cooler", moderner und praktischer als ein Handle.
Und jetzt meine Frage an euch: Welche dieser Möglichkeiten sind eurer Meinung nach empfehlenswert und welche nicht? Vielleicht sind meine Ideen auch allesamt Mist und ihr habt bessere Vorschläge?

Übrigens habe ich auch den Thread Gemeinsamer Ressourcenzugriff über Smart-Pointer gefunden, in dem ein ähnliches Thema behandelt wird. Da für mich aber noch Nachfragebedarf bestand, habe ich diesen neuen Thread eröffnet.

Ich freue mich auf eure Antworten.

idontknow

unregistriert

2

02.02.2013, 21:26

Kannst auch unique_ptr speichern, aber den nackten Pointer zurückgeben.

3

02.02.2013, 22:05

Zum speichern ansich benutze ich eh nur noch und für alles unique_ptr. Alleine schon, weil man dadurch schon am Typ sieht, ob eine Klasse ein Objekt verwaltet, oder nur eine Referenz darauf.

Der shared_ptr bringt dir glaube ich nicht viel. Du musst dich halt fragen, ob zu dem Zeitpunkt, an dem der Manager zerstört wird, irgendein Objekt noch eine Ressource gespeichert hat - wenn dein Level vor dem Ressourcenmanager zerstört wird (wieso solltest du den Manager zerstören, während es noch ein Level gibt, dass Ressourcen von ihm benutzt?) wird das eh immer der Fall sein. Du hast also einfach eine Menge Verwaltungsaufwand, der dir am Ende nichts bringt.
Was die Idee mit den Handles bringen soll, versteh ich ehrlich gesagt nicht, willst du dir anhand eine Handels ständig die Ressource neu holen?
Und eine Proxyklasse, nun, welchen Mehrwert hätte die genau? Es macht keinen Sinn, dein Programm durch weitere Klassen komplexer zu machen, nur weil es "cool aussieht". Entweder du hast eine klare Notwendigkeit und wirst dieser durch eine Klasse gerecht, oder du hältst es so einfach wie möglich (was immer der bessere weg ist).

Ich würde intern unique_ptr benutzen (alleine weil die so super mit stl-Containern zusammenarbeiten) und nach außen ganze normale Pointer rausgeben (von mir aus auch Ressourcen), bis du einen wirklichen Grund hast, warum du irgendetwas anderes brauchen solltest.
Lieber dumm fragen, als dumm bleiben!

BlueCobold

Community-Fossil

Beiträge: 10 738

Beruf: Teamleiter Mobile Applikationen & Senior Software Engineer

  • Private Nachricht senden

4

02.02.2013, 22:42

Würde ich auch so machen. Ganz klare Ownership mit unique_ptr und alle anderen erhalten nur einen Raw-Pointer als Referenz. Den können sie natürlich mutwillig löschen, aber mehr als einen Fehler wird das nicht bewirken. Programme absichtlich zum Absturz bringen kann ein Entwickler immer. Also lautet die Regel ganz klar: Was dir nicht gehört, darfst Du nicht löschen. Im Foren-Spiel wird das ganz restriktiv so gehandhabt. Raw-Pointer sind nur Referenzen, ein Delete gibt es nirgends, das machen alles unique_ptr.
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]

5

03.02.2013, 03:27

Danke für eure Anworten!

Gut, dann war das mit dem Raw Pointer ja doch keine so schlechte Idee. Aber wenn die Ressourcenobjekte, so wie in dem Codebeispiel, direkt in der Map gespeichert werden (anstatt Pointer auf Ressourcenobjekte), kann ich auf die unique_ptr auch verzichten, woll? Der Ownership ist dadurch ja eindeutig festgelegt und im Vergleich zu unique_ptr fallen mir auch sonst keine großen Nachteile ein.

Zitat von »Jonathan_Klein«

Was die Idee mit den Handles bringen soll, versteh ich ehrlich gesagt nicht, willst du dir anhand eine Handels ständig die Ressource neu holen?

Die Grundidee war, einen opaken Datentypen bereitzustellen, um den Umgang mit den Ressourcen zu abstrahieren und in bestimmten Bereichen einzuschränken. Der Ressourcenmanager hätte die Schnittstelle zwischen seinen eigenen Ressourcen und der "Außenwelt" dargestellt. Ein (schon älterer) Artikel aus Game Programming Gems hat mich auf diese Idee gebracht: A Generic Handle-Based Resource Manager

Zitat

Und eine Proxyklasse, nun, welchen Mehrwert hätte die genau? Es macht keinen Sinn, dein Programm durch weitere Klassen komplexer zu machen, nur weil es "cool aussieht". Entweder du hast eine klare Notwendigkeit und wirst dieser durch eine Klasse gerecht, oder du hältst es so einfach wie möglich (was immer der bessere weg ist).

Hinter den Proxyklassen steckte die gleiche Idee wie bei den Handles (deswegen hatte ich sie auch nicht noch mal explizit im Titel des Threads erwähnt). Ich befand sie für "cooler", weil ich das Herumreichen von Handles nicht ganz so elegant finde. Aber du hast schon recht: Dadurch würden viele Klassen hinzukommen (eine pro Ressourcentyp), die das Programm womöglich unnötig komplex machen würden. Abgesehen von einer zusätzlichen (und fragwürdigen) Abstraktionsebene wäre dadurch nichts gewonnen.

6

03.02.2013, 14:30

Ok, was du zumindest tun könntest, wäre ein typedef für den Zeiger zu benutzen. Willst du später auf Proxy-Objekte umsteigen, brauchst du bei denen nur den -> Operator überladen und solltest den meisten Code ganz normal weiter benutzen können.

Durch Reference-Counting kannst du zum Beispiel feststellen, ob Ressourcen noch benutzt werden, oder aber freigegeben werden können. Allerdings musst du dich fragen, ob sich das für dich lohnt:
- Eventuell hast du gar nicht so viele Objekte, dass du zur Laufzeit welche freigeben musst, um platz für neue zu schaffen
- Es ist relativ wahrscheinlich, dass Dinge die einmal benötigt werden auch noch ein weiteres mal benötigt werden
- Wenn Wenn du ein neues Level lädst, und das vorherige vorher löschst, sind eh alle Ressourcen weg, dann kannst du auch einfach beim Laden einen neuen Manager anlegen und hast damit auch aufgeräumt. Um Objekte wirklich wiederverwenden zu können, musst du dir etwas einfallen lassen, Beispielsweise das neue Level laden, bevor du das alte freigibst, oder ein verzögertes freigeben, dass die Objekte noch am Leben lässt, auch wenn der Referenzcounter auf 0 ist.

Es kann also durchaus Sinn machen, trivial ist die Angelegenheit aber nicht.
Aber was auch Klasse ist: Du kannst Proxyobjekte haben, die zur Laufzeit ihre Ressource wechseln, ohne dass der Benutzer etwas merkt. So kannst du beispielsweise sehr niedrig aufgelöste Texturen zum Start laden und dann in einem zweiten Thread während das Spiel schon läuft, bessere Versionen nachladen. In Bioshock sieht man das zuweilen sehr deutlich. Der Vorteil ist, dass wenn diese Ressourcen nicht direkt am Levelstart sichtbar sind, es dem Benutzer gar nicht auffällt, er hat nur kürzere Ladezeiten. Und sollte er sie doch mal sehen, sieht er halt ein paar Sekunden eine schlechtere Version, was noch verhältnismäßig harmlos ist.

Wie gesagt, für den Anfang ist ein normaler Zeiger eigentlich ok, aber mit einem typedef kannst du dir etwas Arbeit ersparen, solltest du irgendwann mal etwas von dem Erwähnten umsetzen wollen.
Lieber dumm fragen, als dumm bleiben!

7

03.02.2013, 16:58

Das typedef klingt in der Tat nach einer guten Idee. So oder so ähnlich würde das aussehen, wenn ich dich nicht missverstanden habe:

C-/C++-Quelltext

1
2
typedef T ResourceType;
ResourceType *getResource(const std::string& name);

Das Sternchen habe ich absichtlich nicht in das typedef aufgenommen: So ist mir das persönlich lieber, weil der Pointer dadurch direkt als solcher erkennbar ist (ohne den Namen des typdefs genauer durchzulesen).

Bei Bedarf könnte ich nun das T durch eine Proxyklasse ersetzen - das ist praktisch. Danke für den Tipp.

Eine Referenzzählung für die Ressourcen werde ich wahrscheinlich nicht benötigen. Es wird zwar mengenmäßig einiges zusammenkommen, aber die einzelnen Ressourcen sind nicht besonders groß. Von daher dürfte der rohe Zeiger auf die Ressourcen vorerst völlig genügen.

H5::

Treue Seele

Beiträge: 368

Wohnort: Kiel

  • Private Nachricht senden

8

03.02.2013, 18:30

Es ist natürlich Ansichtssache, aber ich persönlich mag es nicht wenn mir Pointer zurückgegeben werden. Man weiß nie ist es jetzt eine Referenz oder nicht. Am Ende wird etwas erzeugt und nie frei gegeben. Bei den shared_ptr kannst du z.B. einen Deleter übergeben. Soll die Ressource freu gegeben werden so kannst du den normalen benutzen, soll das nicht so sein kann dein Manager einen anbringen der die Resource dann selbst zerstört oder einfach nichts tut. Durch den weak_ptr hättest du dann sogar eine Möglichkeit festzustellen ob die Resource noch benutzt wird.

Diese Idee kommt aber bei mir aus einem anderen Bereich, ich habe sowas in der Form deiner Fragestellung noch nicht umgesetzt.
Für mich ist aber der Stern böse ;) fui bäh.


C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
// template<typename T>
// using Pointer = std::weak_ptr<T>;
typedef std::weak_ptr<ResourceType> ResourcePointer;

...
// auto ResourceManager::Resource<ResourceType>(Pointer<ResourceType>& pointer, const std::string& name) -> void;
auto ResourceManager::Resource(ResourcePointer& pointer, const std::string& name) -> void;
...

// Pointer<ResourceType> resource(nullptr);
ResourcePointer resource(nullptr);
manager.Resource(resource, "Bla");


Edit:
Hab noch mal hinzugefügt wie ich eine Resource erzeugen würde die der Manager nicht verwaltet. Auch wenn es ein größeres Object sein sollte… der Compiler macht das schon. Man braucht nur noch selten Zeiger.

C-/C++-Quelltext

1
auto ResourceManager::CreateResource(const std::string& name) -> ResourceType;
:love: := Go;

Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von »H5::« (03.02.2013, 18:43)


9

05.02.2013, 10:53

Danke auch für deine Antwort, H5.

Ich habe mich jetzt für den bösen Stern als Rückgabewert entschieden. Da ich noch keine große Erfahrung mit Smart Pointern habe, versuche ich mich erst mal an diese Richtlinien zu halten:
  • unique_ptr, wenn es einen einzigen Besitzer gibt.
  • shared_ptr, wenn es mehrere, gleichberechtigte Besitzer gibt.
  • weak_ptr, wenn es einen befristeten Besitzer gibt. Das referenzierte Objekt kann zu jeder Zeit von einem anderen Besitzer gelöscht werden.
  • Roher Pointer, wenn der Besitz bei dem Aufgerufenen verbleibt. Das referenzierte Objekt existiert mindestens so lange wie der Aufrufer.
In diesem Fall wäre also der Ressourcenmanager der Aufgerufene und garantiert allen Aufrufern den Fortbestand der Ressourcen. Ich hoffe, dass ich damit ganz gut fahren werde.

Übrigens kannte ich bis zum Lesen deines Beitrags den neuen Funktionsdeklarationssytax von C++11 noch gar nicht. Gestern habe ich aber schon gelesen, was man damit alles anstellen kann. Nicht schlecht, vor allem in Verbindung mit decltype.

Falls ich auch nichtverwaltete Ressourcen benötige, werde ich das wohl wie von dir vorgeschlagen umsetzen. Mit return value optimization scheint das ja gut zu klappen. Früher hätte ich mich das nicht getraut, aber heutzutage kann man auch mal was riskieren. ;)

Eine Frage zu deinem Code hätte ich aber doch noch: Hat es einen bestimmten Grund, dass ResourceManager::Resource keinen Rückgabewert liefert, sondern diesen in einer Referenz speichert?

10

05.02.2013, 11:24

Nein, der Stern muss schon in den Typedef mit rein. Sonst legst du dich ja wieder auf Pointer fest und wirst nie Proxyobjekte benutzen können, höchstens Pointer auf Proxyobjekte.
Lieber dumm fragen, als dumm bleiben!

Werbeanzeige