Welche Muster bevorzugen Sie bei der Entwicklung von Spielen in C ++ in Bezug auf die Verwendung von Zeigern (seien es keine, unformatiert, beschränkt, gemeinsam genutzt oder auf andere Weise zwischen intelligent und stumm)?
Sie könnten darüber nachdenken
- Objektbesitz
- Benutzerfreundlichkeit
- Kopierrichtlinie
- Overhead
- zyklische Referenzen
- Zielplattform
- mit Behältern verwenden
quelle
Ich folge auch dem Gedankengang "starkes Eigentum". Ich möchte klar abgrenzen, dass "diese Klasse dieses Mitglied besitzt", wenn es angebracht ist.
Ich benutze selten
shared_ptr
. In diesem Fall verwende ich,weak_ptr
wann immer ich kann , eine großzügige Verwendung, damit ich das Objekt wie ein Handle behandeln kann, anstatt die Referenzanzahl zu erhöhen.Ich benutze
scoped_ptr
überall. Es zeigt offensichtliche Eigenverantwortung. Der einzige Grund, warum ich solche Objekte nicht einfach zu Mitgliedern mache, ist, dass Sie sie weiterleiten können, wenn sie sich in einem scoped_ptr befinden.Wenn ich eine Liste von Objekten brauche, benutze ich
ptr_vector
. Es ist effizienter und hat weniger Nebenwirkungen als die Verwendungvector<shared_ptr>
. Ich denke, Sie können den Typ möglicherweise nicht im ptr_vector deklarieren (es ist eine Weile her), aber die Semantik macht es meiner Meinung nach wert. Grundsätzlich wird ein Objekt, das Sie aus der Liste entfernen, automatisch gelöscht. Dies zeigt auch offensichtliche Eigenverantwortung.Wenn ich auf etwas verweisen möchte, versuche ich, es als Verweis anstelle eines nackten Zeigers zu verwenden. Manchmal ist dies nicht praktikabel (dh wenn Sie nach dem Erstellen des Objekts eine Referenz benötigen). In beiden Fällen zeigen Verweise offensichtlich, dass Sie das Objekt nicht besitzen, und wenn Sie die gemeinsame Zeigersemantik überall sonst befolgen, verursachen nackte Zeiger im Allgemeinen keine zusätzliche Verwirrung (insbesondere, wenn Sie eine Regel "Keine manuellen Löschvorgänge" befolgen). .
Mit dieser Methode konnte ein iPhone-Spiel, an dem ich gearbeitet habe, nur einen einzigen
delete
Anruf tätigen, und zwar in der von mir geschriebenen Brücke zwischen Obj-C und C ++.Generell bin ich der Meinung, dass Memory Management zu wichtig ist, um es den Menschen zu überlassen. Wenn Sie das Löschen automatisieren können, sollten Sie dies tun. Wenn der Overhead von shared_ptr zur Laufzeit zu teuer ist (vorausgesetzt, Sie haben die Threading-Unterstützung usw. deaktiviert), sollten Sie möglicherweise etwas anderes (z. B. ein Bucket-Muster) verwenden, um Ihre dynamischen Zuordnungen zu reduzieren.
quelle
Verwenden Sie das richtige Werkzeug für den Job.
Wenn Ihr Programm Ausnahmen auslösen kann, stellen Sie sicher, dass Ihr Code ausnahmebewusst ist. Die Verwendung von intelligenten Zeigern, RAII und das Vermeiden von Zweiphasenkonstruktionen sind gute Ausgangspunkte.
Wenn Sie zyklische Referenzen ohne eindeutige Besitzersemantik haben, können Sie eine Garbage Collection-Bibliothek verwenden oder Ihr Design überarbeiten.
Gute Bibliotheken ermöglichen es Ihnen, das Konzept und nicht den Typ zu codieren, sodass es in den meisten Fällen keine Rolle spielt, welchen Zeigertyp Sie verwenden, und zwar über Ressourcenverwaltungsprobleme hinaus.
Wenn Sie in einer Umgebung mit mehreren Threads arbeiten, stellen Sie sicher, dass Sie wissen, ob Ihr Objekt möglicherweise von mehreren Threads gemeinsam genutzt wird. Einer der Hauptgründe für die Verwendung von boost :: shared_ptr oder std :: tr1 :: shared_ptr ist die Verwendung eines thread-sicheren Referenzzählers.
Wenn Sie sich Sorgen über die getrennte Zuordnung der Referenzzählungen machen, gibt es viele Möglichkeiten, dies zu umgehen. Mit der Bibliothek boost :: shared_ptr können Sie die Referenzzähler zusammenfassen oder boost :: make_shared (meine Präferenz) verwenden, um das Objekt und den Referenzzähler in einer einzigen Zuordnung zuzuordnen und so die meisten Bedenken hinsichtlich Cache-Fehlern zu beseitigen. Sie können den Leistungsverlust beim Aktualisieren der Referenzanzahl in leistungskritischem Code vermeiden, indem Sie einen Verweis auf das Objekt auf der obersten Ebene halten und direkte Verweise auf das Objekt weitergeben.
Wenn Sie den gemeinsamen Besitz benötigen, aber nicht die Kosten für Referenzzählung oder Speicherbereinigung bezahlen möchten, sollten Sie unveränderliche Objekte oder eine Kopie auf Schreibsprache verwenden.
Bedenken Sie, dass Ihre größten Leistungsgewinne bei weitem auf der Ebene der Architektur und anschließend auf der Ebene des Algorithmus liegen werden. Diese Bedenken auf niedriger Ebene sind zwar sehr wichtig, sollten aber erst angegangen werden, nachdem Sie die Hauptprobleme behoben haben. Wenn Sie mit Leistungsproblemen auf der Ebene von Cache-Fehlern zu tun haben, müssen Sie eine ganze Reihe von Problemen berücksichtigen, wie z. B. falsches Teilen, das nichts mit Zeigern zu tun hat.
Wenn Sie intelligente Zeiger verwenden, um Ressourcen wie Texturen oder Modelle gemeinsam zu nutzen, sollten Sie eine speziellere Bibliothek wie Boost.Flyweight in Betracht ziehen.
Sobald der neue Standard übernommen wurde, wird die Arbeit mit teuren Objekten und Containern durch Verschiebungssemantik, Wertverweise und perfekte Weiterleitung wesentlich einfacher und effizienter. Speichern Sie bis dahin keine Zeiger mit destruktiver Kopiersemantik wie auto_ptr oder unique_ptr in einem Container (Standardkonzept). Erwägen Sie die Verwendung der Boost.Pointer Container-Bibliothek oder das Speichern von Smart Pointern mit gemeinsamem Besitz in Containern. Bei leistungskritischem Code sollten Sie beide vermeiden, um aufdringliche Container wie die in Boost.Intrusive zu vermeiden.
Die Zielplattform sollte Ihre Entscheidung nicht zu sehr beeinflussen. Eingebettete Geräte, Smartphones, dumme Telefone, PCs und Konsolen können den Code problemlos ausführen. Projektanforderungen wie strenge Speicherbudgets oder keine dynamische Zuordnung je / nach dem Laden sind zutreffendere Bedenken und sollten Ihre Auswahl beeinflussen.
quelle
Wenn Sie C ++ 0x verwenden, verwenden Sie
std::unique_ptr<T>
.Es gibt keinen Performance-Overhead, im Gegensatz zu
std::shared_ptr<T>
denen , die den Overhead für die Referenzzählung haben. Ein unique_ptr besitzt seinen Zeiger, und Sie können den Besitz mit der Verschiebungssemantik von C ++ 0x übertragen . Sie können sie nicht kopieren, sondern nur verschieben.Es kann auch in Containern verwendet werden, die z. B.
std::vector<std::unique_ptr<T>>
binärkompatibel und in der Leistung identisch sindstd::vector<T*>
, aber keinen Speicher verlieren, wenn Sie Elemente löschen oder den Vektor löschen. Dies hat auch eine bessere Kompatibilität mit STL-Algorithmen alsptr_vector
.IMO für viele Zwecke ist dies ein idealer Container: Direktzugriff, ausnahmesicher, verhindert Speicherlecks, geringer Overhead für die Vektorumverteilung (mischt nur um Zeiger hinter den Kulissen). Sehr nützlich für viele Zwecke.
quelle
Es empfiehlt sich, zu dokumentieren, welche Klassen welche Zeiger besitzen. Vorzugsweise verwenden Sie nur normale Objekte und keine Zeiger, wann immer Sie können.
Wenn Sie jedoch den Überblick über Ressourcen behalten müssen, ist die Übergabe von Zeigern die einzige Option. Es gibt einige Fälle:
Ich denke, das deckt ziemlich genau ab, wie ich meine Ressourcen im Moment verwalte. Die Speicherkosten eines Zeigers wie shared_ptr sind im Allgemeinen doppelt so hoch wie die Speicherkosten eines normalen Zeigers. Ich denke nicht, dass dieser Aufwand zu groß ist, aber wenn Sie wenig Ressourcen haben, sollten Sie überlegen, Ihr Spiel so zu gestalten, dass die Anzahl der intelligenten Zeiger verringert wird. In anderen Fällen entwerfe ich nur nach guten Prinzipien wie den obigen Aufzählungszeichen und der Profiler sagt mir, wo ich mehr Geschwindigkeit benötige.
quelle
Wenn es speziell um die Pointer von Boost geht, sollten sie meiner Meinung nach vermieden werden, solange ihre Implementierung nicht genau Ihren Anforderungen entspricht. Sie sind mit höheren Kosten verbunden, als man zunächst erwarten würde. Sie bieten eine Schnittstelle, über die Sie wichtige und wichtige Teile Ihres Speicher- und Ressourcenmanagements überspringen können.
Wenn es um Softwareentwicklung geht, denke ich, dass es wichtig ist, über Ihre Daten nachzudenken. Es ist sehr wichtig, wie Ihre Daten im Speicher dargestellt werden. Der Grund dafür ist, dass die CPU-Geschwindigkeit viel schneller gestiegen ist als die Speicherzugriffszeit. Dies macht die Speicher-Caches häufig zum Hauptengpass der meisten modernen Computerspiele. Durch die lineare Ausrichtung Ihrer Daten im Speicher gemäß der Zugriffsreihenfolge wird der Cache erheblich entlastet. Diese Art von Lösungen führt häufig zu saubereren Designs, einfacherem Code und definitiv zu Code, der leichter zu debuggen ist. Intelligente Zeiger führen leicht zu häufigen dynamischen Speicherzuweisungen von Ressourcen, wodurch sie über den gesamten Speicher verteilt werden.
Dies ist keine vorzeitige Optimierung, sondern eine gesunde Entscheidung, die so früh wie möglich getroffen werden kann und sollte. Es ist eine Frage des architektonischen Verständnisses der Hardware, auf der Ihre Software ausgeführt wird, und es ist wichtig.
Bearbeiten: Es gibt ein paar Dinge, die in Bezug auf die Leistung von geteilten Zeigern beachtet werden müssen:
quelle
Ich neige dazu, überall intelligente Zeiger zu verwenden. Ich bin mir nicht sicher, ob dies eine wirklich gute Idee ist, aber ich bin faul und sehe keinen wirklichen Nachteil [außer wenn ich eine C-Zeiger-Arithmetik machen wollte]. Ich benutze boost :: shared_ptr, weil ich weiß, dass ich es kopieren kann - wenn zwei Entitäten ein Bild teilen, sollte eines sterben und das andere das Bild nicht verlieren.
Der Nachteil dabei ist, dass ein Objekt nicht gelöscht wird, wenn es etwas löscht, auf das es zeigt und das es besitzt, aber auch etwas anderes darauf zeigt.
quelle
Die Vorteile der Speicherverwaltung und der Dokumentation durch gute Smart Pointer bedeuten, dass ich sie regelmäßig verwende. Wenn der Profiler jedoch aufruft und mir mitteilt, dass mich eine bestimmte Nutzung kostet, greife ich auf die neolithischere Zeigerverwaltung zurück.
quelle
Ich bin alt, oldskool und ein Fahrradzähler. In meiner eigenen Arbeit verwende ich rohe Zeiger und keine dynamischen Zuweisungen zur Laufzeit (mit Ausnahme der Pools selbst). Alles ist gepoolt und das Eigentum ist sehr streng und niemals übertragbar. Wenn es wirklich gebraucht wird, schreibe ich einen benutzerdefinierten kleinen Block-Allokator. Ich stelle sicher, dass es während des Spiels einen Zustand gibt, in dem sich jeder Pool selbst löschen kann. Wenn die Dinge haarig werden, wickle ich Objekte in Griffe, damit ich sie verschieben kann, aber ich möchte lieber nicht. Behälter sind benutzerdefinierte und extrem nackte Knochen. Ich verwende auch keinen Code mehr.
Obwohl ich niemals die Tugend all der intelligenten Zeiger und Container und Iteratoren und so weiter bestreiten würde, bin ich dafür bekannt, dass ich in der Lage bin, extrem schnell (und einigermaßen zuverlässig) zu codieren. wie Herzinfarkte und ewige Alpträume).
Bei der Arbeit ist natürlich alles anders, es sei denn, ich erstelle Prototypen, die ich zum Glück viel tun darf.
quelle
Fast keine, obwohl dies zugegebenermaßen eine seltsame Antwort ist und wahrscheinlich bei weitem nicht für alle geeignet ist.
In meinem persönlichen Fall hat es sich jedoch als sehr viel nützlicher erwiesen, alle Instanzen eines bestimmten Typs in einer zentralen Sequenz mit wahlfreiem Zugriff (threadsicher) zu speichern und stattdessen mit 32-Bit-Indizes (relative Adressen, z. B.) zu arbeiten. eher als absolute Zeiger.
Für den Anfang:
T
niemals zu stark im Speicher verstreut sind. Dadurch werden Cache-Ausfälle für alle Arten von Zugriffsmustern verringert, selbst wenn verknüpfte Strukturen wie Bäume durchlaufen werden, wenn die Knoten nicht mit Zeigern, sondern mit Indizes verknüpft sind.Das heißt, Bequemlichkeit ist ein Nachteil sowie die Typensicherheit. Wir können keine Instanz zugreifen ,
T
ohne Zugang zu beiden Behältern und Index. Und ein einfaches Altesint32_t
sagt uns nichts darüber, auf welchen Datentyp es sich bezieht, daher gibt es keine Typensicherheit. Wir könnten versehentlich versuchen,Bar
über einen Index auf eine zuzugreifenFoo
. Um das zweite Problem zu lindern, mache ich oft so etwas:Das scheint albern, gibt mir aber die Typensicherheit zurück, damit die Leute nicht versehentlich versuchen können
Bar
,Foo
ohne einen Compilerfehler auf einen Index zuzugreifen . Der Einfachheit halber akzeptiere ich nur die leichten Unannehmlichkeiten.Eine andere Sache, die für die Leute eine große Unannehmlichkeit sein könnte, ist, dass ich keinen vererbungsbasierten OOP-Polymorphismus verwenden kann, da dies einen Basiszeiger erfordern würde, der auf alle Arten von unterschiedlichen Untertypen mit unterschiedlichen Größen- und Ausrichtungsanforderungen zeigen kann. Aber ich verwende heutzutage nicht viel Vererbung - bevorzuge den ECS-Ansatz.
Was
shared_ptr
versuche ich es nicht so viel zu verwenden. Die meiste Zeit finde ich es nicht sinnvoll, das Eigentum zu teilen, und dies kann willkürlich zu logischen Lecks führen. Zumindest auf hohem Niveau gehört eines oft zu einer Sache. Ich fand es oft verlockend,shared_ptr
die Lebensdauer eines Objekts an Orten zu verlängern, die sich nicht wirklich mit Eigentum befassten, wie zum Beispiel bei einer lokalen Funktion in einem Thread, um sicherzustellen, dass das Objekt nicht zerstört wird, bevor der Thread beendet ist es benutzen.Um dieses Problem anzugehen, bevor
shared_ptr
ich oder GC oder ähnliches verwende, bevorzuge ich häufig kurzlebige Aufgaben, die aus einem Thread-Pool ausgeführt werden. Wenn dieser Thread die Zerstörung eines Objekts anfordert, wird die tatsächliche Zerstörung auf einen Safe verschoben Zeitpunkt, zu dem das System sicherstellen kann, dass kein Thread auf diesen Objekttyp zugreifen muss.Manchmal verwende ich immer noch Ref-Counting, behandle es aber wie eine Strategie des letzten Auswegs. Und es gibt ein paar Fälle, in denen es wirklich Sinn macht, Eigentümer zu sein, wie die Implementierung einer beständigen Datenstruktur, und ich finde, dass es durchaus Sinn macht,
shared_ptr
sofort danach zu greifen .Trotzdem verwende ich meistens Indizes und verwende sowohl rohe als auch intelligente Zeiger sparsam. Ich mag Indizes und die Arten von Türen, die sich öffnen, wenn Sie wissen, dass Ihre Objekte zusammenhängend gespeichert und nicht über den Speicherbereich verstreut sind.
quelle