multithreading c/c++ - C++11 führte ein standardisiertes Speichermodell ein.Was heißt das? Und wie wird es die C++Programmierung beeinflussen?



c# lernen (6)

C ++ 11 führte ein standardisiertes Speichermodell ein, aber was genau bedeutet das? Und wie wird es die C ++ Programmierung beeinflussen?

Dieser Artikel (von Gavin Clarke, der Herb Sutter zitiert) sagt, dass

Das Speichermodell bedeutet, dass C ++ - Code jetzt eine standardisierte Bibliothek zum Aufrufen hat, unabhängig davon, wer den Compiler erstellt hat und auf welcher Plattform er ausgeführt wird. Es gibt eine Standardmethode, um zu steuern, wie verschiedene Threads mit dem Speicher des Prozessors kommunizieren.

"Wenn Sie über das Teilen von [Code] über verschiedene Kerne im Standard sprechen, sprechen wir über das Speichermodell. Wir werden es optimieren, ohne die folgenden Annahmen zu brechen, die die Leute im Code machen werden", sagte Sutter .

Nun, ich kann mir diese und ähnliche, online verfügbare Absätze auswendig merken (da ich seit meiner Geburt mein eigenes Gedächtnismodell habe: P) und kann sogar als Antwort auf Fragen von anderen posten, aber um ehrlich zu sein, verstehe ich das nicht genau .

Was ich im Grunde wissen möchte, ist, dass C ++ - Programmierer früher schon Multithread-Anwendungen entwickelt haben. Wie kommt es dann darauf an, ob es sich um POSIX-Threads oder Windows-Threads oder C ++ 11-Threads handelt? Was sind die Vorteile? Ich möchte die Details auf niedriger Ebene verstehen.

Ich habe auch das Gefühl, dass das C ++ 11-Speichermodell irgendwie mit C ++ 11-Multithreading-Unterstützung verwandt ist, da ich diese beiden oft zusammen sehe. Wenn ja, wie genau? Warum sollten sie verwandt sein?

Da ich nicht weiß, wie Interna von Multi-Threading funktioniert und was Memory Model im Allgemeinen bedeutet, bitte helfen Sie mir, diese Konzepte zu verstehen. :-)


Answers

Wenn Sie Mutexe verwenden, um alle Ihre Daten zu schützen, sollten Sie sich keine Sorgen machen. Mutexe bieten immer ausreichende Bestell- und Sichtbarkeitsgarantien.

Wenn Sie jetzt Atomics oder Lock-Free-Algorithmen verwenden, müssen Sie über das Speichermodell nachdenken. Das Speichermodell beschreibt genau, wann Atomics Bestell- und Sichtbarkeitsgarantien bietet und portable Zäune für handcodierte Garantien bietet.

Zuvor wurden Atomics mithilfe von Compiler-Intrinsics oder einer höheren Bibliotheksebene erstellt. Zäune wären mit CPU-spezifischen Anweisungen (Speicherbarrieren) gemacht worden.


Das bedeutet, dass der Standard jetzt Multithreading definiert und definiert, was im Kontext mehrerer Threads geschieht. Natürlich verwendeten Leute unterschiedliche Implementierungen, aber das ist wie die Frage, warum wir eine std::string wenn wir alle eine home-rolled string Klasse verwenden könnten.

Wenn Sie über POSIX-Threads oder Windows-Threads sprechen, ist das eine Illusion, da Sie eigentlich von x86-Threads sprechen, da es sich um eine Hardwarefunktion handelt, die gleichzeitig ausgeführt wird. Das C ++ 0x-Speichermodell garantiert, ob Sie auf x86 oder ARM oder MIPS stehen oder was Sie sonst noch brauchen.


Zuerst müssen Sie lernen, wie ein Sprachanwalt zu denken.

Die C ++ - Spezifikation bezieht sich nicht auf einen bestimmten Compiler, ein bestimmtes Betriebssystem oder eine bestimmte CPU. Es bezieht sich auf eine abstrakte Maschine , die eine Verallgemeinerung der tatsächlichen Systeme ist. In der Welt der Sprachjuristen besteht die Aufgabe des Programmierers darin, Code für die abstrakte Maschine zu schreiben; Die Aufgabe des Compilers ist es, diesen Code auf einer konkreten Maschine zu aktualisieren. Wenn Sie streng nach der Spezifikation codieren, können Sie sicher sein, dass Ihr Code kompiliert und ohne Änderung auf jedem System mit einem kompatiblen C ++ - Compiler ausgeführt wird, egal ob heute oder in 50 Jahren.

Die abstrakte Maschine in der Spezifikation C ++ 98 / C ++ 03 ist grundsätzlich single-threaded. Daher ist es nicht möglich, Multi-Thread-C ++ - Code zu schreiben, der in Bezug auf die Spezifikation "vollständig portierbar" ist. Die Spezifikation sagt noch nichts über die Atomizität von Speicherladungen und -speichern oder die Reihenfolge, in der Ladevorgänge und Speichervorgänge auftreten können, ganz zu schweigen von Dingen wie Mutexe.

Natürlich können Sie in der Praxis Multi-Thread-Code für bestimmte konkrete Systeme schreiben - wie Pthreads oder Windows. Aber es gibt keine Standardmethode zum Schreiben von Multithread-Code für C ++ 98 / C ++ 03.

Die abstrakte Maschine in C ++ 11 ist vom Entwurf her multi-threaded. Es hat auch ein wohldefiniertes Speichermodell ; Das heißt, was der Compiler tun kann und was nicht, wenn es darum geht, auf Speicher zuzugreifen.

Betrachten Sie das folgende Beispiel, in dem auf ein Paar globaler Variablen gleichzeitig von zwei Threads zugegriffen wird:

           Global
           int x, y;

Thread 1            Thread 2
x = 17;             cout << y << " ";
y = 37;             cout << x << endl;

Was könnte Thread 2 ausgeben?

Unter C ++ 98 / C ++ 03 ist dies nicht einmal Undefined Behavior; Die Frage selbst ist bedeutungslos, weil der Standard nichts als "Thread" bezeichnet.

Unter C ++ 11 ist das Ergebnis Undefined Behavior, weil Lasten und Speicher im Allgemeinen nicht atomar sein müssen. Was nicht wie eine Verbesserung erscheinen mag ... Und das ist es nicht.

Aber mit C ++ 11 können Sie dies schreiben:

           Global
           atomic<int> x, y;

Thread 1                 Thread 2
x.store(17);             cout << y.load() << " ";
y.store(37);             cout << x.load() << endl;

Jetzt werden die Dinge viel interessanter. Zunächst ist das Verhalten hier definiert . Thread 2 könnte jetzt 0 0 (wenn es vor Thread 1 ausgeführt wird), 37 17 (wenn es nach Thread 1 ausgeführt wird) oder 0 17 (wenn es ausgeführt wird, nachdem Thread 1 x zugewiesen hat, aber bevor es y zugewiesen wird).

Was es nicht drucken kann, ist 37 0 , weil der Standardmodus für atomare Ladungen / Speicher in C ++ 11 sequentielle Konsistenz erzwingt. Das bedeutet nur, dass alle Ladevorgänge und Speichervorgänge "so als ob" sie in der Reihenfolge geschehen würden, in der Sie sie in jedem Thread geschrieben haben, während Operationen zwischen Threads verschachtelt werden können, wie es das System auch mag. Das Standardverhalten von Atomics stellt also sowohl die Atomarität als auch die Reihenfolge für Lasten und Speicher bereit.

Auf einer modernen CPU kann die Sicherstellung der sequentiellen Konsistenz teuer sein. Insbesondere wird der Compiler wahrscheinlich zwischen jedem Zugriff volle Speicherbarrieren aussenden. Aber wenn Ihr Algorithmus Out-Of-Order-Lasten und -Speicher tolerieren kann; dh wenn es Atomizität erfordert, aber nicht Ordnung; dh wenn es 37 0 als Ausgabe von diesem Programm tolerieren kann, dann können Sie dies schreiben:

           Global
           atomic<int> x, y;

Thread 1                            Thread 2
x.store(17,memory_order_relaxed);   cout << y.load(memory_order_relaxed) << " ";
y.store(37,memory_order_relaxed);   cout << x.load(memory_order_relaxed) << endl;

Je moderner die CPU ist, desto wahrscheinlicher ist es, dass sie schneller ist als das vorherige Beispiel.

Schließlich, wenn Sie nur bestimmte Lasten und Speicher in der richtigen Reihenfolge halten müssen, können Sie schreiben:

           Global
           atomic<int> x, y;

Thread 1                            Thread 2
x.store(17,memory_order_release);   cout << y.load(memory_order_acquire) << " ";
y.store(37,memory_order_release);   cout << x.load(memory_order_acquire) << endl;

Dies bringt uns zurück zu den geordneten Ladungen und speichert - so ist 37 0 keine mögliche Ausgabe mehr - aber dies mit minimalem Overhead. (In diesem trivialen Beispiel ist das Ergebnis dasselbe wie die vollständige sequenzielle Konsistenz; in einem größeren Programm wäre dies nicht der Fall.)

Wenn die einzigen Ausgaben, die Sie sehen möchten, 0 0 oder 37 17 , können Sie natürlich einen Mutex um den ursprünglichen Code wickeln. Aber wenn du so weit gelesen hast, wette ich, du weißt bereits, wie das funktioniert, und diese Antwort ist schon länger, als ich es beabsichtigt hatte :-).

Also, unter dem Strich. Mutexe sind großartig und C ++ 11 standardisiert sie. Aber manchmal wollen Sie aus Gründen der Performance niedrigere Level-Primitive (zB das klassische Double-Checked-Locking-Pattern ). Der neue Standard bietet High-Level-Gadgets wie Mutexe und Zustandsvariablen und bietet auch Low-Level-Gadgets wie atomare Typen und die verschiedenen Geschmacksrichtungen der Speicherbarriere. Jetzt können Sie anspruchsvolle, hochperformante simultane Routinen vollständig in der vom Standard spezifizierten Sprache schreiben, und Sie können sicher sein, dass Ihr Code sowohl auf den heutigen als auch den heutigen Systemen kompiliert und unverändert bleibt.

Obwohl man ehrlich ist, sollte man sich, wenn man kein Experte ist und an einem ernsthaften Low-Level-Code arbeitet, wahrscheinlich an Mutexe und Condition-Variablen halten. Das beabsichtige ich zu tun.

Weitere Informationen zu diesem Thema finden Sie in diesem Blogbeitrag .


Ich werde nur die Analogie geben, mit der ich Speicherkonsistenzmodelle (oder kurz Speichermodelle) verstehe. Es ist inspiriert von Leslie Lamports bahnbrechendem Artikel "Zeit, Uhren und die Reihenfolge der Ereignisse in einem verteilten System" . Die Analogie ist zutreffend und hat grundlegende Bedeutung, aber kann für viele Leute übertrieben sein. Ich hoffe jedoch, dass es ein mentales Bild (eine bildliche Darstellung) liefert, das das Nachdenken über Gedächtniskonsistenzmodelle erleichtert.

Betrachten wir die Historien aller Speicherorte in einem Raum-Zeit-Diagramm, in dem die horizontale Achse den Adressraum repräsentiert (dh jeder Speicherplatz wird durch einen Punkt auf dieser Achse repräsentiert) und die vertikale Achse die Zeit darstellt (wir werden sehen, im Allgemeinen gibt es keine universelle Vorstellung von Zeit). Die Historie der Werte, die von jeder Speicherstelle gehalten werden, wird daher durch eine vertikale Spalte bei dieser Speicheradresse dargestellt. Jede Wertänderung ist darauf zurückzuführen, dass einer der Threads einen neuen Wert an diesen Ort schreibt. Mit einem Speicherbild meinen wir die Gesamtheit / Kombination von Werten aller Speicherstellen, die zu einer bestimmten Zeit durch einen bestimmten Thread beobachtbar sind .

Zitat aus "A Primer auf Speicherkonsistenz und Cache-Kohärenz"

Das intuitive (und restriktivste) Speichermodell ist die sequentielle Konsistenz (SC), in der eine Multithread-Ausführung wie eine Verschachtelung der sequentiellen Ausführungen jedes konstituierenden Threads aussehen soll, als ob die Threads auf einem Single-Core-Prozessor zeitgemultiplext wären.

Diese globale Speicherordnung kann von einem Programmlauf zum anderen variieren und ist möglicherweise nicht vorher bekannt. Das charakteristische Merkmal von SC ist der Satz horizontaler Schichten in dem Adressraum-Zeit-Diagramm, die Gleichzeitigkeitsebenen (dh Speicherbilder) darstellen. Auf einer gegebenen Ebene sind alle seine Ereignisse (oder Speicherwerte) gleichzeitig. Es gibt eine Vorstellung von absoluter Zeit , in der alle Threads übereinstimmen, welche Speicherwerte gleichzeitig sind. In SC gibt es zu jedem Zeitpunkt nur ein Speicherbild, das von allen Threads geteilt wird. Das heißt, zu jedem Zeitpunkt stimmen alle Prozessoren auf das Speicherbild (dh den Gesamtspeicherinhalt) zu. Dies bedeutet nicht nur, dass alle Threads für alle Speicherbereiche die gleiche Wertefolge anzeigen, sondern dass alle Prozessoren die gleichen Wertekombinationen aller Variablen beobachten. Dies ist gleichbedeutend damit, dass alle Speicheroperationen (an allen Speicherorten) in der gleichen Gesamtfolge von allen Threads beobachtet werden.

In entspannten Speichermodellen schneidet jeder Thread die Adressraum-Zeit auf seine eigene Weise auf. Die einzige Einschränkung besteht darin, dass sich die Segmente eines Threads nicht kreuzen dürfen, da alle Threads sich auf die Historie jedes einzelnen Speicherplatzes einigen müssen (natürlich , Scheiben verschiedener Fäden können und werden einander kreuzen). Es gibt keine universelle Möglichkeit, sie aufzuteilen (keine privilegierte Foliation von Adresse-Raum-Zeit). Slices müssen nicht planar (oder linear) sein. Sie können gekrümmt sein, und dies kann dazu führen, dass ein Thread die von einem anderen Thread geschriebenen Werte aus der Reihenfolge liest, in der sie geschrieben wurden. Die Histogramme unterschiedlicher Speicherpositionen können bei Betrachtung durch einen bestimmten Thread beliebig verschoben (oder gestreckt) werden . Jeder Thread hat einen unterschiedlichen Sinn dafür, welche Ereignisse (oder äquivalent Speicherwerte) gleichzeitig sind. Die Gruppe von Ereignissen (oder Speicherwerten), die gleichzeitig mit einem Thread ausgeführt werden, ist nicht gleichzeitig mit einem anderen. In einem entspannten Speichermodell beobachten daher alle Threads immer noch die gleiche Historie (dh die Folge von Werten) für jeden Speicherort. Sie können jedoch unterschiedliche Speicherbilder (dh Kombinationen von Werten aller Speicherstellen) beobachten. Selbst wenn zwei verschiedene Speicherplätze nacheinander von demselben Thread geschrieben werden, können die zwei neu geschriebenen Werte in anderer Reihenfolge von anderen Threads beobachtet werden.

[Bild aus Wikipedia]

Leser, die Einsteins Spezielle Relativitätstheorie kennen, werden bemerken, worauf ich anspiele. Die Wörter von Minkowski in das Reich der Speichermodelle übersetzen: Adressraum und Zeit sind Schatten von Adresse-Raum-Zeit. In diesem Fall projiziert jeder Beobachter (dh ein Faden) Schatten von Ereignissen (dh Speicher speichert / lädt) auf seine eigene Weltlinie (dh seine Zeitachse) und seine eigene Ebene der Gleichzeitigkeit (seine Adresse-Raum-Achse). . Threads im C ++ 11-Speichermodell entsprechen Beobachtern , die sich in der speziellen Relativität relativ zueinander bewegen. Sequenzielle Konsistenz entspricht der galiläischen Raumzeit (dh alle Beobachter sind sich einig über eine absolute Reihenfolge von Ereignissen und ein globales Gleichzeitigkeitsgefühl).

Die Ähnlichkeit zwischen Speichermodellen und spezieller Relativitätstheorie rührt von der Tatsache her, dass beide eine teilweise geordnete Menge von Ereignissen definieren, die oft als kausale Menge bezeichnet werden. Einige Ereignisse (z. B. Speicher) können andere Ereignisse beeinflussen (aber nicht davon betroffen sein). Ein C ++ 11-Thread (oder ein Beobachter in der Physik) ist nicht mehr als eine Kette (dh eine vollständig geordnete Menge) von Ereignissen (z. B. lädt und speichert Speicher auf möglicherweise unterschiedliche Adressen).

In der Relativitätstheorie wird eine Ordnung in das scheinbar chaotische Bild von teilweise geordneten Ereignissen wiederhergestellt, da die einzige zeitliche Ordnung, auf die sich alle Beobachter geeinigt haben, die Ordnung unter "zeitartigen" Ereignissen ist (dh jene Ereignisse, die im Prinzip durch jedes langsamere Teilchen verbindbar sind) als die Lichtgeschwindigkeit im Vakuum). Nur die zeitähnlichen Ereignisse sind invariant geordnet. Zeit in der Physik, Craig Callender .

Im C ++ 11-Speichermodell wird ein ähnlicher Mechanismus (das Acquire-Release-Konsistenzmodell) verwendet, um diese lokalen Kausalitätsbeziehungen zu ermitteln .

Um eine Definition der Speicherkonsistenz und eine Motivation für den Abbruch von SC zu geben, werde ich aus "Eine Grundlegendes zur Speicherkonsistenz und Cache-Kohärenz" zitieren.

Bei einer Maschine mit gemeinsam genutztem Speicher definiert das Speicherkonsistenzmodell das architektonisch sichtbare Verhalten seines Speichersystems. Das Korrektheitskriterium für einen einzelnen Prozessorkern teilt das Verhalten zwischen " einem korrekten Ergebnis " und " vielen falschen Alternativen " auf. Dies liegt daran, dass die Architektur des Prozessors vorschreibt, dass die Ausführung eines Threads einen gegebenen Eingabezustand in einen einzigen wohldefinierten Ausgabezustand umwandelt, selbst bei einem Out-of-Order-Core. Shared-Memory-Konsistenzmodelle betreffen jedoch die Lade- und Speichervorgänge mehrerer Threads und ermöglichen normalerweise viele korrekte Ausführungen, während viele (mehr) falsche Ausführungen nicht zulässig sind. Die Möglichkeit mehrerer korrekter Ausführungen ist auf die ISA zurückzuführen, die es erlaubt, mehrere Threads gleichzeitig auszuführen, oft mit vielen möglichen legalen Interleavings von Anweisungen von verschiedenen Threads.

Entspannte oder schwache Speicherkonsistenzmodelle werden durch die Tatsache motiviert, dass die meisten Speicherordnungen in starken Modellen unnötig sind. Wenn ein Thread zehn Datenelemente und dann ein Synchronisierungskennzeichen aktualisiert, ist es den Programmierern normalerweise egal, ob die Datenelemente in der Reihenfolge zueinander aktualisiert werden, sondern nur, dass alle Datenelemente aktualisiert werden, bevor das Flag aktualisiert wird (normalerweise mithilfe von FENCE-Befehlen) ). Entspannte Modelle versuchen, diese erhöhte Bestellflexibilität zu erfassen und nur die Befehle beizubehalten, die Programmierer " benötigen ", um sowohl eine höhere Leistung als auch eine höhere Korrektheit von SC zu erreichen. Zum Beispiel werden in bestimmten Architekturen FIFO-Schreibpuffer von jedem Kern verwendet, um die Ergebnisse von festgeschriebenen (zurückgezogenen) Speichern zu halten, bevor die Ergebnisse in die Caches geschrieben werden. Diese Optimierung verbessert die Leistung, verletzt jedoch SC. Der Schreibpuffer verbirgt die Latenz des Wartens eines Speicherfehlers. Da Geschäfte üblich sind, ist es ein wichtiger Vorteil, bei den meisten von ihnen das Abwürgen zu vermeiden. Bei einem Single-Core-Prozessor kann ein Schreibpuffer architektonisch unsichtbar gemacht werden, indem sichergestellt wird, dass ein Ladevorgang an Adresse A den Wert des letzten Speichers an A zurückgibt, auch wenn sich ein oder mehrere Speicher für A im Schreibpuffer befinden. Dies geschieht typischerweise, indem entweder der Wert des letzten Speichers an A zu dem Ladevorgang von A umgangen wird, wobei "zuletzt" durch die Programmreihenfolge bestimmt wird, oder indem ein Ladevorgang von A angehalten wird, wenn ein Speicher zu A im Schreibpuffer ist . Wenn mehrere Kerne verwendet werden, hat jeder seinen eigenen umgehenden Schreibpuffer. Ohne Schreibpuffer ist die Hardware SC, aber mit Schreibpuffern ist dies nicht der Fall, wodurch Schreibpuffer in einem Multicore-Prozessor architektonisch sichtbar gemacht werden.

Store-Store-Neuordnung kann passieren, wenn ein Kern einen Nicht-FIFO-Schreibpuffer hat, der Speicher in einer anderen Reihenfolge ablaufen lässt als in der Reihenfolge, in der sie eingegeben wurden. Dies kann auftreten, wenn der erste Speicher im Cache fehlschlägt, während der zweite auftritt, oder wenn der zweite Speicher mit einem früheren Speicher (dh vor dem ersten Speicher) koaleszieren kann. Die Neuordnung von Ladeanforderungen kann auch bei dynamisch geplanten Kernen auftreten, die Anweisungen außerhalb der Programmreihenfolge ausführen. Das kann sich genauso verhalten wie das Umsortieren von Speichern auf einem anderen Kern (Können Sie eine Beispielverschachtelung zwischen zwei Threads finden?). Das Umordnen einer früheren Ladung mit einem späteren Speicher (eine Neuordnung des Lade-Speichers) kann viele falsche Verhaltensweisen verursachen, z. B. das Laden eines Werts nach dem Aufheben der Sperre, die ihn schützt (wenn der Speicher die Entsperr-Operation ist). Es ist zu beachten, dass Speicherlade-Neuordnungen auch aufgrund einer lokalen Umgehung in dem gemeinsam implementierten FIFO-Schreibpuffer auftreten können, selbst mit einem Kern, der alle Befehle in der Programmreihenfolge ausführt.

Weil Cache-Kohärenz und Speicherkonsistenz manchmal verwirrt sind, ist es lehrreich, auch dieses Zitat zu haben:

Im Gegensatz zur Konsistenz ist Cache-Kohärenz weder für Software sichtbar noch erforderlich. Coherence versucht, die Caches eines Shared-Memory-Systems so funktionell unsichtbar zu machen wie die Caches eines Single-Core-Systems. Korrekte Kohärenz stellt sicher, dass ein Programmierer nicht feststellen kann, ob und wo ein System Caches hat, indem er die Ergebnisse von Ladevorgängen und Speichern analysiert. Dies liegt daran, dass eine korrekte Kohärenz sicherstellt, dass die Caches niemals ein neues oder anderes funktionales Verhalten ermöglichen (Programmierer können unter Verwendung von Timing- Informationen immer noch in der Lage sein, eine wahrscheinliche Cachestruktur abzuleiten ). Der Hauptzweck von Cache-Kohärenzprotokollen besteht darin, die Single-Writer-Multiple-Reader (SWMR) für jeden Speicherplatz invariant zu halten. Ein wichtiger Unterschied zwischen Kohärenz und Konsistenz besteht darin, dass Kohärenz auf einer Speicherbasis angegeben wird, während Konsistenz in Bezug auf alle Speicherstellen spezifiziert wird.

In Fortsetzung unseres mentalen Bildes entspricht die SWMR-Invariante der physikalischen Anforderung, dass es höchstens ein Teilchen an einem Ort gibt, aber es kann eine unbegrenzte Anzahl von Beobachtern von jedem Ort geben.


Bei Sprachen, die kein Speichermodell angeben, schreiben Sie Code für die Sprache und das Speichermodell, das von der Prozessorarchitektur angegeben wird. Der Prozessor kann wählen, Speicherzugriffe für die Leistung neu zu ordnen. Wenn Ihr Programm also Datenrennen hat (ein Datenrennen ist, wenn mehrere Kerne / Hyper-Threads gleichzeitig auf denselben Speicher zugreifen können), ist Ihr Programm aufgrund seiner Abhängigkeit vom Prozessorspeichermodell nicht plattformübergreifend. In den Intel- oder AMD-Softwarehandbüchern finden Sie Informationen dazu, wie die Prozessoren Speicherzugriffe neu ordnen können.

Sehr wichtig ist, dass Sperren (und Parallelitätssemantik mit Sperren) normalerweise plattformübergreifend implementiert werden ... Wenn Sie also Standardsperren in einem Multithread-Programm ohne Datenrennen verwenden, müssen Sie sich nicht um plattformübergreifende Speichermodelle kümmern .

Interessanterweise haben Microsoft-Compiler für C ++ Semantik für flüchtige Elemente, die eine C ++ - Erweiterung ist, um mit dem Fehlen eines Speichermodells in C ++ umzugehen. http://msdn.microsoft.com/en-us/library/12a04hfd(v=vs.80).aspx . Da Windows jedoch nur auf x86 / x64 ausgeführt wird, sagt das nicht viel aus (Intel- und AMD-Speichermodelle machen es einfach und effizient, eine Semantik zum Akquirieren / Freigeben in einer Sprache zu implementieren).


Die Verwendung der Funktionen begin und end fügt eine Ebene der Indirektion hinzu. Normalerweise wird dies getan, um mehr Flexibilität zu ermöglichen.

In diesem Fall kann ich an einige Anwendungen denken.

Die offensichtlichste Verwendung ist für C-Arrays (keine c-Zeiger).

Ein anderes ist, wenn versucht wird, einen Standardalgorithmus für einen nichtkonformen Container zu verwenden (dh dem Container fehlt eine .begin() -Methode). Vorausgesetzt, Sie können nicht nur den Container reparieren, ist die nächste beste Option, die Startfunktion zu überlasten. Herb schlägt vor, dass Sie immer die begin Funktion verwenden, um Einheitlichkeit und Konsistenz in Ihrem Code zu fördern. Anstatt sich zu erinnern, welche Container-Support-Methode begin und welche Funktion begin .

Nebenbei sollte das nächste C ++ rev die D- Pseudo-Mitgliedsnotation kopieren. Wenn a.foo(b,c,d) nicht definiert ist, versucht es stattdessen foo(a,b,c,d) . Es ist nur ein kleiner syntaktischer Zucker, um uns armen Menschen zu helfen, die Subjekt bevorzugen, dann Verbbestellung.





c++ multithreading c++11 language-lawyer memory-model