Ich hab die Frage jetzt auch erstmal so verstanden wie Kristhy, dass starcow momentan
std::vector<std::unique_ptr<...>> verwendet und auf
std::vector<...> umsteigen möchte, aber weiterhin stabile Zeiger zu den Objekten in den Vektor bewahren will.
Um beliebig viele Objekte hinzuzufügen ist die Lösung von Kristhy gut.
Eine Alternative Methode ist es sehr viel Speicher zu reservieren (z.B. 100 GB, mit
mmap/
VirtualAlloc) und dann nur soweit zu füllen wie man ihn wirklich braucht. Der Vorteil hiervon ist, dass die Iteration als einfaches Array wieder trivial ist und man die dass man die Objekte sehr effizient im Vektor via ihrem Index nachschlagen kann. Der Nachteil ist, dass man unterschiedliche APIs für Windows und Linux (bzw. generell POSIX) verwenden muss und das man besonders bei 32 Bit Anwendungen mit wenig Adressraum ggf. nicht sorglos genug Speicher reservieren kann, der auf jeden Fall ausreicht.
Egal ob die Krishtys-Blockmethode oder die Speicherreservationsmethode verwendest, möchte ich folgendes zu bedenken geben: Du kannst dann nicht mehr einfach Objekt löschen. Normalerweise würde ja ein "Loch" im Array entstehen. Du musst also entweder leere Dummy-Objekte an der Stelle einfügen (was sehr nervig zu verwalten ist und sehr ungünstig für die Sprungvorhersage und SIMD) oder ein anderes Objekt dorthin verschieben (normalerweise das letzte Objekt der Liste). Und im zweiten Fall musst du wieder Objekte verschieben! Dieses Problem ist sehr nervig. Du musst dann alle anderen Objekte die auf dein Objekt verweisen aktualisieren. Das kann man zwar auch automatisch im Move-Konstruktur anstoßen, aber die Verweise hin und zurück, damit du überhaupt weißt,
wer einen Zeiger besitzt der aktualisiert werden muss, sind sehr nervig zu verwalten. Wenn die Verweise von den anderen Objekte nicht ganz so Performance-kritisch sind, kann man auch noch einen Hilfszeiger einrichten. Also du kannst deinem Objekt ein
std::unique_ptr<MyClass*> geben und (nur) diesen Zeiger bei Verschiebungen aktualisieren. Andere Objekte dürfen dann nur indirekt auf das Objekt via diesem Hilfszeiger verweisen, der automatisch aktualisiert wird. Das ist nicht ganz so ätzend zu implementieren, aber der Preis ist natürlich, dass der Weg von anderen Objekten aus zu deinem Objekt nun eine doppelte Dereferenzierung erfordert und langsamer geworden ist. Wenn das auch performancekritisch ist, ist das auch nicht optimal.
Wenn es Querverweise zwischen Objekten gibt, gibt es meiner Meinung nach auch gar keine wirklich optimale Lösung.
Noch ein paar Gedanken bezüglich "Structure of Arrays":
Ich würde den "Structure of Arrays"-Ansatz nicht nur für
extreme Performance empfehlen. Meiner Erfahrung nach ist das nicht die Art Optimierung mit der man nach vergeblichen Suchen noch ein paar Prozent Effizienz rauskratzen kann (das wäre das ich dann als "extrem" bezeichnen würde), sondern eher die Art wie man ein Modul strukturiert, so dass es um ein Vielfaches schneller läuft.
Ich würde den Ansatz immer empfehlen, wenn man
sehr viele Objekte gleicher Bauart hat und ganz besonders, wenn man SIMD verwenden kann/will. Das ist ein wichtiges Kriterium um den Anwendungsfall zu identifizieren, und das Gegenteil kann auch ein Ausschlusskriterium sein. Wenn du z.B. eine GUI programmierst und vlt. ein paar duzend Buttons vom gleichen Typ hast, die noch dazu nicht gleichen Berechnungen durchmachen wozu SIMD geeignet wäre, dann ist der "Structure of Arrays"-Ansatz sicherlich wenig bis gar nicht profitabel. Er ist vlt. sogar kontraproduktiv. "Datenoriertierung" heißt übrigens nicht, überall blind "Structure of Arrays" hinzukleistern, sondern sich genau anzugucken WIE und WOFÜR die Daten benutzt werden und danach das Datenlayout zu optimieren. Wenn eine Operation auf einem einzelnen Objekt sehr viele Eigenschaften benützt (also nicht bloß besitzt, sondern auch zugreift), dann ist das verteilen auf mehrere Arrays gerade schlecht und man sollte gegenteilig "Arrays of Structures" für die die bessere Performance verwenden. Dann lohnt es sich eher zu schauen, dass diese verschiedenen Eigenschaften des selben Objekts sequentiell im Speicher liegen.
Ein konkretes Beispiel hierzu: Wenn man eine Hash-Table programmiert, könnte man denken, man macht am besten zwei parallele Arrays. Eins für die Hashes und eins für die Objekte. ABER: Das ist oft kontraproduktiv! In einer Hash-Table werden nach dem Hash sehr oft die Objekte zugegriffen. Insbesondere: Wenn es nicht mehr in den Cache passt, nützt das separate Array kaum etwas, weil trotzdem in den meisten Cache Lines noch ein Objekt liegt und deshalb fast alle Cache Lines benötigt werden. Deswegen ist in diesem Fall "Arrays of structure" für Hashtables i.d.R. effizienter (außer man benützt SIMD um z.B. die Hashes zu vergleichen, wie beispielsweise die Google-/Abseil-Hashmap). Zu dieser Thematik empfehle ich auch
diesen Artikel von Malte Skarupke bzw. den zugehörigen
CppCon-Vortrag.
Man kann übrigens auch einen Kompromiss-Weg gehen und einzelne Daten in eigene Arrays oder kleinere Strukturen packen, ohne gleich ALLE Eigenschaften als SoA auszulegen.
Es gibt in sehr neuen Intel-CPUs in AVX512 (und teilweise AVX2) spezielle Befehle/Intrinsics für Gather- und Scatter-Operationen, die relativ zu früher sehr effizient sind. Simple Lade- und Speicheroperationen sind trotzdem noch besser. Und die Gather-Befehle (AMD unterstützt bisher nur AVX2) haben sehr maue Performance auf AMD-CPUs.
Außerdem ist Cache-Effizienz ein wichtiger Punkt. Wenn du sehr viele Objekte verarbeiten willst, passt nicht mehr alles immer perfekt in den Cache. Deshalb entstehen Latenz- und, besonders übel, Throughput-Probleme dabei, die Daten zur CPU zu bringen. Latenz-Probleme kannst du auch mit Prefetch-Instruktionen lindern bzw. macht die CPU hier ohnehin bereits oft automatisch einen guten Job, aber Throughput-Probleme sind katastrophal. Hier hilft dann nur noch geschickteres Datenlayout wie z.B. mit "Structure of Arrays". (Wenn man beispielsweise das obige Beispiel mit dem Hilfszeiger, um Zeigerinvalidierung zu vermeiden, anschaut: Hier wird man zur Laufzeit diesen Hilfszeiger wahrscheinlich nur sehr selten vom Objekt aus zugreifen. Deshalb würde es den Cache ein bisschen entlasten, wenn man ganzen Hilfszeiger in ein eigenes Array packt im SoA-Stil.)