C ++ Abkürzung Spickzettel und mehr. Teil 1: C ++

Einmal wurde ich für die Position eines C ++ - Entwicklers in einem anständigen und sogar bekannten Büro interviewt. Ich hatte damals schon einige Erfahrungen, damals wurde ich sogar als führender Entwickler bei meinem Arbeitgeber bezeichnet. Aber als ich gefragt wurde, ob ich solche Dinge wie DRY, KISS, YAGNI, NIH wüsste, musste ich immer wieder mit „Nein“ antworten.

Ich habe natürlich kläglich versagt. Aber dann wurden die obigen Abkürzungen gegoogelt und erinnert. Während ich thematische Artikel und Bücher las, mich auf Interviews vorbereitete und nur mit Kollegen sprach, lernte ich mehr Neues, vergaß sie, googelte erneut und verstand. Vor ein paar Monaten erwähnte einer meiner Kollegen beiläufig im IIFE-Arbeitschat im Kontext von C ++. Wie dieser Großvater in einem Witz fiel ich fast vom Herd und stieg wieder in Google ein.



Zu diesem Zeitpunkt habe ich beschlossen, (hauptsächlich für mich selbst) einen Spickzettel für Abkürzungen zu erstellen, die für einen C ++ - Entwickler nützlich sind. Dies bedeutet nicht, dass sie nur für C ++ gelten oder dass es sich um All-All-All-Konzepte aus C ++ handelt (Sie können Bände über Sprachsprachen schreiben). Nein, dies sind nur Konzepte, denen ich tatsächlich in Arbeiten und Interviews begegnet bin, normalerweise ausgedrückt in Form von Abkürzungen. Nun, ich habe absolut triviale Dinge wie LIFO, FIFO, CRUD, OOP, GCC und MSVC verpasst.

Trotzdem kamen die Abkürzungen anständig vor, so dass ich den Spickzettel in zwei Teile teilte: stark charakteristisch für C ++ und häufiger. Wenn es angebracht war, habe ich die Konzepte zusammengefasst, ansonsten habe ich sie einfach alphabetisch aufgelistet. Im Allgemeinen macht ihre Reihenfolge nicht viel Sinn.

Grundlegende Dinge:
ODR
POD
POF
PIMPL
RAII
RTTI
STL
UB

Feinheiten der Sprache:
ADL
CRTP
CTAD
EBO
IIFE
NVI
RVO und NRVO
SFINAE
SBO, SOO, SSO

UPDATE:
Lebenslauf
LTO
PCH
PGO
SEH / VEH
TMP
VLA

Grundlegende Dinge


ODR


Eine Definitionsregel. Die Regel einer Definition. Vereinfacht bedeutet Folgendes:

  • Innerhalb einer einzelnen Übersetzungseinheit kann jede Variable, Funktion, Klasse usw. nicht mehr als eine Definition haben. Es gibt so viele Anzeigen wie möglich (außer Übertragungen ohne einen bestimmten Basistyp, die einfach nicht ohne Definition deklariert werden können), aber nicht mehr als eine Definition. Weniger möglich, wenn die Entität nicht verwendet wird.
  • Während des gesamten Programms muss jede verwendete Nicht-Inline-Funktion und Variable genau eine Definition haben. Jede verwendete Inline-Funktion und Variable muss in jeder Übersetzungseinheit eine Definition haben.
  • Einige Entitäten - beispielsweise Klassen, Inline-Funktionen und eine Variable, Vorlagen, Aufzählungen usw. - können mehrere Definitionen in einem Programm haben (jedoch nicht mehr als eine in einer Übersetzungseinheit). Tatsächlich geschieht dies, wenn derselbe Header, der beispielsweise eine vollständig implementierte Klasse enthält, mit mehreren CPP-Dateien verbunden ist. Aber diese Definitionen sollten übereinstimmen (ich vereinfache stark, aber das Wesentliche ist dies). Sonst wird es UB sein .

Der Compiler erkennt leicht eine ODR- Verletzung innerhalb einer Übersetzungseinheit. Er kann jedoch nichts tun, wenn die Regel auf Programmebene verletzt wird - schon allein deshalb, weil der Compiler jeweils eine Übersetzungseinheit verarbeitet.

Der Linker kann viel mehr Verstöße feststellen, ist aber streng genommen nicht dazu verpflichtet (weil UB laut Standard hier ist) und kann etwas verpassen. Darüber hinaus ist die Suche nach ODR- Verstößen in der Verknüpfungsphase quadratisch komplex, und die Zusammenstellung von C ++ - Code ist nicht so schnell.

Infolgedessen liegt die Hauptverantwortung für die Einhaltung dieser Regel (insbesondere auf Programmebene) beim Entwickler selbst. Und ja - nur Entitäten mit einem externen Link können ODR im Programmmaßstab verletzen. diejenigen von innen (d. h. in anonymen Namespaces definiert) nehmen an diesem Karneval nicht teil.

Lesen Sie mehr: einmal (Englisch) , zwei (Englisch)

Pod


Einfache alte Daten. Einfache Datenstruktur. Die einfachste Definition: Dies ist eine solche Struktur, die Sie in binärer Form an die C-Bibliothek senden / von dieser empfangen können. Oder, das ist das gleiche, richtig mit einfachem memcpy kopieren.

Von Standard zu Standard hat sich die vollständige Definition im Detail geändert. Der neueste C ++ 17 POD definiert derzeit, wie

  • Skalartyp
  • oder eine Klasse / Struktur / Vereinigung, die:
    - Es gibt eine triviale Klasse
    - Es gibt eine Klasse mit einem Standardgerät
    - enthält keine nicht statischen Nicht- POD -Felder
  • oder ein Array dieser Typen

Trivialklasse:

  • hat mindestens eine nicht gelöscht:
    - Standardkonstruktor
    - Konstruktor kopieren
    - beweglicher Konstruktor
    - Zuweisungsoperator kopieren
    - Zuweisungsoperator verschieben
  • Alle Standardkonstruktoren, die Konstruktoren und Zuweisungsoperatoren kopieren und verschieben, sind trivial (vereinfacht - vom Compiler generiert) oder remote
  • hat einen trivialen nicht entfernten Destruktor
  • Alle Basistypen und alle Felder von Klassentypen haben triviale Destruktoren
  • keine virtuellen Methoden (einschließlich Destruktor)
  • Keine virtuellen Basistypen

Klasse mit einem Standardgerät (Standardlayoutklasse):

  • keine virtuellen Methoden
  • Keine virtuellen Basistypen
  • Keine nicht statischen Linkfelder
  • Alle nicht statischen Felder haben denselben Zugriffsmodifikator (öffentlich / geschützt / privat).
  • Alle nicht statischen Felder und Basisklassen sind ebenfalls Typen mit einem Standardgerät
  • Alle nicht statischen Felder der Klasse selbst und aller ihrer Vorfahren werden in einer einzigen Klasse deklariert (d. h. in der Klasse selbst oder in einem der Vorfahren).
  • Es erbt nicht zweimal denselben Typ, d. H. Es ist unmöglich, dies zu tun:
     struct A {}; struct B : A {}; struct C : A{}; struct D : B, C {}; 
  • Der Typ des ersten nicht statischen Felds oder, wenn es sich um ein Array handelt, der Typ seines Elements darf nicht mit einem der Grundtypen übereinstimmen (aufgrund des obligatorischen EBO in diesem Fall).

In C ++ 20 gibt es jedoch nicht mehr das Konzept des POD- Typs, sondern nur noch den trivialen Typ und den Typ mit dem Standardgerät.

Lesen Sie mehr: eins (Russisch) , zwei (Englisch) , drei (Englisch)

POF


Einfache alte Funktion. Eine einfache Funktion im C-Stil, die im Standard vor C ++ 14 einschließlich nur im Zusammenhang mit Signalhandlern erwähnt wurde. Die Voraussetzungen dafür sind:

  • verwendet nur Dinge, die C und C ++ gemeinsam haben (d. h. keine Ausnahmen und beispielsweise try-catch )
  • verursacht keine direkten oder indirekten Nicht- POF- Funktionen, mit Ausnahme von std::atomic_init Operationen ( std::atomic_init , std::atomic_fetch_add usw.)

Nur solche Funktionen, die auch eine C-Verbindung ( extern "C" ) haben, dürfen vom Standard als Signalhandler verwendet werden. Die Unterstützung anderer Funktionen hängt vom Compiler ab.

In C ++ 17 verschwindet das POF- Konzept, anstatt dass es als signal-sichere Auswertung erscheint. In solchen Berechnungen sind verboten:

  • ruft alle Funktionen der Standardbibliothek auf, außer atomar, sperrfrei
  • new und delete Anrufe
  • mit dynamic_cast
  • Aufruf der Entität thread_local
  • jede Arbeit mit Ausnahmen
  • Initialisierung einer lokalen statischen Variablen
  • Warten auf den Abschluss der statischen Variableninitialisierung

Wenn der Signalhandler einen der oben genannten Schritte ausführt, verspricht der Standard UB .

Weiterlesen: Zeit (Englisch)

PIMPL


Zeiger auf die Implementierung. Zeiger auf die Umsetzung. Die klassische Redewendung in C ++, auch bekannt als D-Zeiger, undurchsichtiger Zeiger, Kompilierungsfirewall. Es besteht in der Tatsache, dass alle privaten Methoden, Felder und anderen Implementierungsdetails einer bestimmten Klasse einer separaten Klasse zugeordnet sind und nur öffentliche Methoden (d. H. Eine Schnittstelle) und ein Zeiger auf eine Instanz dieser neuen separaten Klasse in der ursprünglichen Klasse verbleiben. Zum Beispiel:

foo.hpp
 class Foo { public: Foo(); ~Foo(); void doThis(); int doThat(); private: class Impl; std::unique_ptr<Impl> pImpl_; }; 


foo.cpp
 #include "foo.hpp" class Foo::Impl { // implementation }; Foo::Foo() : pImpl_(std::make_unique<Impl>()) {} Foo::~Foo() = default; void Foo::doThis() { pImpl_->doThis(); } int Foo::doThat() { return pImpl_->doThat(); } 


Warum ist dies notwendig, d. H. Vorteile:

  • Kapselung: Benutzer der Klasse erhalten über die Header-Verbindung nur das, was sie benötigen - eine öffentliche Schnittstelle. Wenn sich die Implementierungsdetails ändern, muss der Clientcode nicht neu kompiliert werden (siehe ABI ).
  • Kompilierungszeit: Da der öffentliche Header nichts über die Implementierung weiß, enthält er nicht die vielen benötigten Header. Dementsprechend wird die Anzahl implizit verbundener Header im Clientcode reduziert. Die Suche nach Namen und die Auflösung von Überladungen wird ebenfalls vereinfacht, da die öffentliche Überschrift keine privaten Mitglieder enthält (obwohl sie privat sind, nehmen sie an diesen Prozessen teil).

Preis, d. H. Nachteile:

  • Plus mindestens eine Zeiger-Dereferenzierung und plus ein Funktionsaufruf beim Zugriff auf öffentliche Methoden.
  • Die Größe der benötigten Speicherklasse wird um die Größe des Zeigers erhöht.
  • Ein Teil dieses Speichers (höchstwahrscheinlich größer) wird auf dem Heap zugewiesen, was sich auch negativ auf die Leistung auswirkt.
  • Die logische Konstanz kann leicht verletzt werden. Ein solcher Code wird beispielsweise kompiliert:

     void Foo::doThis() const { pImpl_->doThis(); // cosnt method pImpl_->doSmthElse(); // non-const method } 

Einige dieser Mängel können behoben werden, aber der Preis verkompliziert den Code weiter und führt zusätzliche Abstraktionsebenen ein (siehe FTSE ).

Lesen Sie mehr: eins (russisch) , zwei (russisch) , drei (englisch)

RAII


Ressourcenbeschaffung ist Initialisierung. Das Erfassen einer Ressource ist die Initialisierung. Die Bedeutung dieser Redewendung ist, dass die Beibehaltung einer bestimmten Ressource während der gesamten Lebensdauer des entsprechenden Objekts anhält. Die Erfassung der Ressource erfolgt zum Zeitpunkt der Erstellung / Initialisierung des Objekts, der Freigabe - zum Zeitpunkt der Zerstörung / Finalisierung desselben Objekts.

Seltsamerweise (hauptsächlich für C ++ - Programmierer) wird diese Redewendung auch in anderen Sprachen verwendet, selbst in Sprachen mit einem Garbage Collector. In Java ist es try-- , in Python die with Anweisung, in C # die using Direktive, in Go the defer . Aber es ist in C ++ mit seiner absolut vorhersehbaren Lebensdauer von Objekten, in die RAII besonders organisch passt.

In C ++ wird eine Ressource normalerweise im Konstruktor erfasst und im Destruktor freigegeben. Beispielsweise steuern intelligente Zeiger den Speicher auf diese Weise, Dateistreams verwalten Dateien und Mutex sperrt Mutexe. Das Schöne ist, dass unabhängig davon, wie der Block verlassen wird (Gültigkeitsbereich) - ob dies an einem der Austrittspunkte normal ist oder eine Ausnahme ausgelöst wurde - das in diesem Block erstellte Ressourcensteuerungsobjekt zerstört und die Ressource freigegeben wird. Das heißt, Neben der Kapselung von RAII in C ++ trägt es auch zur Gewährleistung der Sicherheit im Sinne von Ausnahmen bei.

Einschränkungen, wo ohne sie. Destruktoren in C ++ geben keine Werte zurück und sollten kategorisch keine Ausnahmen auslösen. Wenn die Freigabe der Ressource von der einen oder anderen begleitet wird, ist es dementsprechend erforderlich, zusätzliche Logik im Destruktor des Steuerobjekts zu implementieren.

Lesen Sie mehr: einmal (Russisch) , zwei (Englisch)

RTTI


Informationen zum Laufzeit-Typ. Typidentifikation zur Laufzeit. Dies ist ein Mechanismus zum Abrufen von Informationen über den Typ eines Objekts oder Ausdrucks zur Laufzeit. Es existiert in anderen Sprachen, aber in C ++ wird es verwendet für:

  • dynamic_cast
  • typeid und type_info
  • Ausnahme fangen

Eine wichtige Einschränkung: RTTI verwendet eine Tabelle mit virtuellen Funktionen und funktioniert daher nur für polymorphe Typen (ein virtueller Destruktor ist ausreichend). Eine wichtige Erklärung: dynamic_cast und typeid verwenden nicht immer RTTI und funktionieren daher für nicht polymorphe Typen. Um beispielsweise einen Link zu einem Nachkommen dynamisch in einen Link zu einem Vorfahren umzuwandeln , wird RTTI nicht benötigt, da alle Informationen zur Kompilierungszeit verfügbar sind.

RTTI ist nicht kostenlos, wenn auch ein wenig, wirkt sich jedoch negativ auf die Leistung und Größe des verbrauchten Speichers aus (daher der häufige Rat, dynamic_cast wegen seiner Langsamkeit nicht zu verwenden). Mit Compilern können Sie RTTI daher in der Regel deaktivieren. GCC und MSVC versprechen, dass dies die Richtigkeit des Abfangens von Ausnahmen nicht beeinträchtigt.

Lesen Sie mehr: einmal (Russisch) , zwei (Englisch)

STL


Standardvorlagenbibliothek. Standardvorlagenbibliothek. Teil der C ++ - Standardbibliothek, die generische Container, Iteratoren, Algorithmen und Hilfsfunktionen bereitstellt.

Trotz seines bekannten Namens wurde STL im Standard noch nie so genannt. Aus den Abschnitten des Standards kann die STL eindeutig der Container-Bibliothek, der Iterator-Bibliothek, der Algorithmus-Bibliothek und teilweise der allgemeinen Dienstprogrammbibliothek zugeordnet werden.

In Stellenbeschreibungen finden Sie häufig zwei separate Anforderungen - Kenntnisse in C ++ und Vertrautheit mit STL . Ich habe das nie verstanden, weil STL seit dem ersten Standard von 1998 ein wesentlicher Bestandteil der Sprache ist.

Lesen Sie mehr: einmal (Russisch) , zwei (Englisch)

UB


Undefiniertes Verhalten. Undefiniertes Verhalten. Dieses Verhalten tritt in den Fehlerfällen auf, für die der Standard keine Anforderungen stellt. Viele davon sind im Standard explizit aufgeführt und führen zu UB . Dazu gehören zum Beispiel:

  • Verletzung der Grenzen eines Arrays oder STL- Containers
  • Verwendung einer nicht initialisierten Variablen
  • Dereferenzieren eines Nullzeigers
  • Ganzzahlüberlauf mit Vorzeichen

Das Ergebnis von UB hängt von allem in einer Reihe ab - sowohl von der Compiler-Version als auch vom Wetter auf dem Mars. Darüber hinaus kann dieses Ergebnis alles sein: ein Kompilierungsfehler und eine korrekte Ausführung sowie ein Absturz. Unbestimmtes Verhalten ist böse, es ist notwendig, es loszuwerden.

Undefiniertes Verhalten sollte dagegen nicht mit nicht spezifiziertem Verhalten verwechselt werden . Nicht angegebenes Verhalten ist das korrekte Verhalten des richtigen Programms, das jedoch mit Genehmigung des Standards vom Compiler abhängt. Und der Compiler muss es nicht dokumentieren. Dies ist beispielsweise die Reihenfolge, in der die Argumente einer Funktion ausgewertet werden, oder die Implementierungsdetails von std::map .

Nun, hier können Sie sich an das implementierungsdefinierte Verhalten erinnern. Von nicht spezifizierten unterscheidet sich in der Verfügbarkeit der Dokumentation. Beispiel: Der Compiler kann den Typ std::size_t beliebig groß machen, muss jedoch angeben, welcher.

Lesen Sie mehr: eins (russisch) , zwei (russisch) , drei (englisch)

Die Feinheiten der Zunge


ADL


Argumentabhängige Suche. Argumentabhängige Suche. Er ist die Suche nach Koenig - zu Ehren von Andrew Koenig. Dies ist ein Satz von Regeln zum Auflösen nicht qualifizierter Funktionsnamen (d. H. Namen ohne den Operator :: :) zusätzlich zur üblichen Namensauflösung. Einfach ausgedrückt: Der Name einer Funktion wird in Namespaces nachgeschlagen, die sich auf ihre Argumente beziehen (dies ist der Bereich, der den Typ des Arguments, den Typ selbst, wenn es sich um eine Klasse handelt, alle ihre Vorfahren usw. enthält).

Einfachstes Beispiel
 #include <iostream> namespace N { struct S {}; void f(S) { std::cout << "f(S)" << std::endl; }; } int main() { N::S s; f(s); } 

Die Funktion f im Namespace N nur gefunden, weil ihr Argument zu diesem Raum gehört.

Auch der triviale std::cout << "Hello World!\n" verwendet ADL , std::basic_stream::operator<< für const char* nicht überladen ist. Das erste Argument für diese Anweisung ist jedoch std::basic_stream , und der Compiler sucht und findet eine geeignete Überladung im std .

Einige Details: ADL ist nicht anwendbar, wenn bei einer regulären Suche eine Deklaration eines Klassenmitglieds oder eine Funktionsdeklaration im aktuellen Block ohne Verwendung using oder eine Deklaration weder einer Funktion noch einer Funktionsvorlage gefunden wurde. Oder wenn der Funktionsname in Klammern angegeben ist (das obige Beispiel lässt sich nicht mit (f)(s) kompilieren; Sie müssen (N::f)(s); schreiben (N::f)(s); ).

Manchmal zwingt ADL Sie, vollständig qualifizierte Funktionsnamen zu verwenden, wenn dies unnötig erscheint.

Beispielsweise wird dieser Code nicht kompiliert
 namespace N1 { struct S {}; void foo(S) {}; } namespace N2 { void foo(N1::S) {}; void bar(N1::S s) { foo(s); } } 


Lesen Sie mehr: eins (Englisch) , zwei (Englisch) , drei (Englisch)

CRTP


Seltsamerweise wiederkehrendes Vorlagenmuster. Seltsames rekursives Muster. Das Wesentliche der Vorlage ist wie folgt:

  • Einige Klassen erben von der Vorlagenklasse
  • Die untergeordnete Klasse wird als Vorlagenparameter ihrer Basisklasse verwendet

Es ist einfacher, ein Beispiel zu geben:

 template <class T> struct Base {}; struct Derived : Base<Derived> {}; 

CRTP ist ein Paradebeispiel für statischen Polymorphismus. Die Basisklasse stellt eine Schnittstelle bereit, die abgeleiteten Klassen stellen eine Implementierung bereit. Im Gegensatz zum normalen Polymorphismus gibt es jedoch keinen Aufwand für das Erstellen und Verwenden einer Tabelle mit virtuellen Funktionen.

Beispiel
 template <typename T> struct Base { void action() const { static_cast<T*>(this)->actionImpl(); } }; struct Derived : Base<Derived> { void actionImpl() const { ... } }; template <class Arg> void staticPolymorphicHandler(const Arg& arg) { arg.action(); } 

Bei korrekter Verwendung ist T immer ein Nachkomme von Base , daher reicht static_cast zum static_cast . Ja, in diesem Fall kennt die Basisklasse die untergeordnete Schnittstelle.

Ein weiterer häufiger Anwendungsbereich für CRTP ist die Erweiterung (oder Einschränkung ) der Funktionalität geerbter Klassen (in einigen Sprachen als Mixin bezeichnet). Vielleicht die bekanntesten Beispiele:

  • struct Derived : singleton<Derived> { … }
  • struct Derived : private boost::noncopyable<Derived> { … }
  • struct Derived : std::enable_shared_from_this<Derived> { … }
  • struct Derived : counter<Derived> { … } - Zählt die Anzahl der erstellten und / oder vorhandenen Objekte

Nachteile bzw. Momente, die Aufmerksamkeit erfordern:

  • Es gibt keine gemeinsame Basisklasse. Sie können keine Sammlung verschiedener Nachkommen erstellen und über einen Zeiger auf den Basistyp darauf zugreifen. Wenn Sie möchten, können Sie Base vom üblichen polymorphen Typ erben.
  • Es gibt eine zusätzliche Möglichkeit, Ihren Fuß aus Unachtsamkeit heraus zu schießen:

    Beispiel
     template <typename T> struct Base {}; struct Derived1 : Base<Derived1> {}; struct Derived2 : Base<Derived1> {}; 

    Sie können jedoch zusätzlichen Schutz hinzufügen:

     private: Base() = default; friend T; 
  • Weil Da alle Methoden nicht virtuell sind, verbergen die Methoden des Nachkommens Methoden der Basisklasse mit denselben Namen. Daher ist es besser, sie anders zu nennen.
  • Im Allgemeinen verfügen Nachkommen über öffentliche Methoden, die nur in der Basisklasse verwendet werden sollten. Dies ist nicht gut, wird aber durch eine zusätzliche Abstraktionsebene korrigiert (siehe FTSE ).


Lesen Sie mehr: einmal (Russisch) , zwei (Englisch)

CTAD


Abzug von Klassenvorlagenargumenten. Automatisches Ableiten des Typs des Klassenvorlagenparameters. Dies ist eine neue Funktion aus C ++ 17. Bisher wurden nur Variablentypen ( auto ) und Funktionsvorlagenparameter automatisch angezeigt, weshalb Hilfsfunktionen wie std::make_pair , std::make_tuple usw. std::make_tuple . Jetzt werden sie zum größten Teil nicht mehr benötigt, da der Compiler können die Parameter von Klassenvorlagen automatisch anzeigen:

 std::pair p{1, 2.0}; // -> std::pair<int, double> auto lck = std::lock_guard{mtx}; // -> std::lock_guard<std::mutex> 

CTAD ist eine neue Möglichkeit, die noch weiterentwickelt werden muss (C ++ 20 verspricht bereits Verbesserungen). In der Zwischenzeit gelten folgende Einschränkungen:

  • Eine teilweise Inferenz von Parametertypen wird nicht unterstützt
     std::pair<double> p{1, 2}; //  std::tuple<> t{1, 2, 3}; //  
  • Vorlagen-Aliase werden nicht unterstützt
     template <class T, class U> using MyPair = std::pair<T, U>; MyPair p{1, 2}; //  
  • Konstruktoren, die nur in Vorlagenspezialisierungen verfügbar sind, werden nicht unterstützt.
     template <class T> struct Wrapper {}; template <> struct Wrapper<int> { Wrapper(int) {}; }; Wrapper w{5}; //  
  • Verschachtelte Vorlagen werden nicht unterstützt
     template <class T> struct Foo { template <class U> struct Bar { Bar(T, U) {}; }; }; Foo::Bar x{ 1, 2.0 }; //  Foo<int>::Bar x{1, 2.0}; // OK 
  • Offensichtlich funktioniert CTAD nicht, wenn der Typ des Vorlagenparameters nicht mit den Konstruktorargumenten zusammenhängt.
     template <class T> struct Collection { Collection(std::size_t size) {}; }; Collection c{5}; //  

In einigen Fällen helfen explizite Inferenzregeln, die im selben Block wie die Klassenvorlage deklariert werden sollen.

Beispiel
 template <class T> struct Collection { template <class It> Collection(It from, It to) {}; }; Collection c{v.begin(), v.end()}; //  template <class It> Collection(It, It)->Collection<typename std::iterator_traits<It>::value_type>; Collection c{v.begin(), v.end()}; //  OK 


Lesen Sie mehr: einmal (Russisch) , zwei (Englisch)

EBO


Leere Basisoptimierung. Optimierung einer leeren Basisklasse. Wird auch als EBCO (Empty Base Class Optimization) bezeichnet.

Wie Sie wissen, kann in C ++ die Größe eines Objekts einer Klasse nicht Null sein. Andernfalls wird die gesamte Arithmetik der Zeiger unterbrochen, da an einer Adresse so viele verschiedene Objekte markiert werden können, wie Sie möchten. Daher haben selbst Objekte leerer Klassen (d. H. Klassen ohne ein einzelnes nicht statisches Feld) eine Größe ungleich Null, die vom Compiler und vom Betriebssystem abhängt und normalerweise gleich 1 ist.

Somit wird Speicher für alle Objekte leerer Klassen vergeblich verschwendet. Aber nicht die Objekte ihrer Nachkommen, denn in diesem Fall macht der Standard ausdrücklich eine Ausnahme. Der Compiler darf keinen Speicher für eine leere Basisklasse reservieren und somit nicht nur 1 Byte der leeren Klasse, sondern alle 4 (je nach Plattform) speichern, da auch eine Ausrichtung vorliegt.

Beispiel
 struct Empty {}; struct Foo : Empty { int i; }; std::cout << sizeof(Empty) << std::endl; // 1 std::cout << sizeof(Foo) << std::endl; // 4 std::cout << sizeof(int) << std::endl; // 4 

Da jedoch verschiedene Objekte desselben Typs nicht an derselben Adresse platziert werden können, funktioniert das EBO nicht, wenn:

  • Eine leere Klasse wird zweimal unter Vorfahren gefunden
     struct Empty {}; struct Empty2 : Empty {}; struct Foo : Empty, Empty2 { int i; }; std::cout << sizeof(Empty) << std::endl; // 1 std::cout << sizeof(Empty2) << std::endl; // 1 std::cout << sizeof(Foo) << std::endl; // 8 
  • Das erste nicht statische Feld ist ein Objekt derselben leeren Klasse oder ihres Nachkommens
     struct Empty {}; struct Foo : Empty { Empty e; int i; }; std::cout << sizeof(Empty) << std::endl; // 1 std::cout << sizeof(Foo) << std::endl; // 8 

In Fällen, in denen Objekte leerer Klassen nicht statische Felder sind, werden keine Optimierungen bereitgestellt (das Attribut [[no_unique_address]] wird [[no_unique_address]] in C ++ 20 [[no_unique_address]] ). Es ist jedoch eine Schande, 4 Bytes (oder wie viel der Compiler benötigt) für jedes dieser Felder auszugeben, sodass Sie Objekte leerer Klassen mit dem ersten nicht leeren nicht statischen Feld selbst "reduzieren" können.

Beispiel
 struct Empty1 {}; struct Empty2 {}; template <class Member, class ... Empty> struct EmptyOptimization : Empty ... { Member member; }; struct Foo { EmptyOptimization<int, Empty1, Empty2> data; }; 

Seltsam, aber in diesem Fall ist die Größe von Foo für verschiedene Compiler unterschiedlich, für MSVC 2019 ist es 8, für GCC 8.3.0 ist es 4. In jedem Fall hat das Erhöhen der Anzahl leerer Klassen keinen Einfluss auf die Größe von Foo .

Lesen Sie mehr: einmal (Englisch) , zwei (Englisch)

IIFE


Sofort aufgerufener Funktionsausdruck. Funktionsausdruck sofort aufgerufen. Im Allgemeinen ist dies eine Redewendung in JavaScript, von der Jason Turner sie zusammen mit dem Namen ausgeliehen hat. Tatsächlich wird nur ein Lambda erstellt und sofort aufgerufen:

 const auto myVar = [&] { if (condition1()) { return computeSomeComplexStuff(); } return condition2() ? computeSonethingElse() : DEFAULT_VALUE; } (); 

Warum ist das notwendig? Nun, zum Beispiel wie im obigen Code, um eine Konstante durch das Ergebnis einer nichttrivialen Berechnung zu initialisieren und den Bereich nicht mit unnötigen Variablen und Funktionen zu verstopfen.

Lesen Sie mehr: einmal (Englisch) , zwei (Englisch)

NVI


Nicht virtuelle Schnittstelle. Nicht virtuelle Schnittstelle. Nach dieser Redewendung sollte eine offene Klassenschnittstelle keine virtuellen Funktionen enthalten. Alle virtuellen Funktionen werden privat gemacht (maximal geschützt) und in offenen nicht virtuellen Funktionen aufgerufen.

Beispiel
 class Base { public: virtual ~Base() = default; void foo() { // check precondition fooImpl(); // check postconditions } private: virtual void fooImpl() = 0; }; class Derived : public Base { private: void fooImpl() override { } }; 

Warum ist das notwendig:

  • Jede offene virtuelle Funktion führt zwei Dinge aus: Sie definiert die öffentliche Schnittstelle der Klasse und beteiligt sich am übergeordneten Verhalten in untergeordneten Klassen. Die Verwendung von NVI eliminiert solche Funktionen bei doppelter Belastung: Die Schnittstelle wird durch einige Funktionen definiert, Verhaltensänderungen durch andere. Sie können beide unabhängig voneinander ändern.
  • Wenn für alle Optionen zur Implementierung einer virtuellen Funktion (Vor- und Nachprüfungen, Mutex-Erfassung usw.) einige allgemeine Anforderungen gelten, ist es sehr praktisch, diese an einem Ort (siehe DRY ) - in der Basisklasse - zu sammeln und zu verhindern, dass die Erben überschreiben . Das heißt, Es stellt sich ein Sonderfall der Mustervorlagenmethode heraus.

Die Gebühr für die Verwendung von NVI ist eine gewisse Schwellung des Codes, eine mögliche Leistungsminderung (aufgrund eines zusätzlichen Methodenaufrufs) und eine erhöhte Anfälligkeit für das Problem einer fragilen Basisklasse (siehe FBC ).

Lesen Sie mehr: einmal (Englisch) , zwei (Englisch)

RVO und NRVO


(Benannt) Rückgabewertoptimierung. Optimierung des (benannten) Rückgabewerts. Dies ist ein Sonderfall der vom Standard zugelassenen Kopierentfernung. Der Compiler kann unnötige Kopien temporärer Objekte weglassen, selbst wenn ihre Konstruktoren und Destruktoren offensichtliche Nebenwirkungen haben. Eine solche Optimierung ist zulässig, wenn die Funktion ein Objekt nach Wert zurückgibt (die beiden anderen zulässigen Fälle der Kopierentfernung sind das Auslösen und Abfangen von Ausnahmen).

Beispiel
 Foo bar() { return Foo(); } int main() { auto f = bar(); } 

Ohne RVO würde hier ein temporäres Objekt Fooin der Funktion erstellt bar, dann würde über den Kopierkonstruktor ein weiteres temporäres Objekt in der Funktion daraus erstellt main(um das Ergebnis zu erhalten bar), und erst dann würde das Objekt erstellt fund der Wert des zweiten temporären Objekts ihm zugewiesen. RVO beseitigt all diese Kopierungen und Zuweisungen, und die Funktion barwird direkt erstellt f.

Dies geschieht folgendermaßen: Eine Funktion mainweist einem Objekt in seinem Stapelrahmen einen Platz zu f. Eine Funktion bar(die bereits in ihrem Frame arbeitet) erhält Zugriff auf diesen im vorherigen Frame zugewiesenen Speicher und erstellt dort das gewünschte Objekt.

NRVO unterscheidet sich vonRVO führt dieselbe Optimierung durch, jedoch nicht, wenn das Objekt im Ausdruck erstellt wird return, sondern wenn das zuvor in der Funktion erstellte Objekt zurückgegeben wird.

Beispiel
 Foo bar() { Foo result; return result; } 

Trotz des scheinbar kleinen Unterschieds ist NRVO viel schwieriger zu implementieren und funktioniert daher in vielen Fällen nicht. Wenn eine Funktion beispielsweise ein globales Objekt oder eines ihrer Argumente zurückgibt oder wenn eine Funktion mehrere Austrittspunkte hat und unterschiedliche Objekte über diese zurückgegeben werden, wird NRVO nicht angewendet .

NRVO funktioniert hier nicht
 Foo bar(bool condition) { if (condition) { Foo f1; return f1; } Foo f2; return f2; } 

Fast alle Compiler unterstützen RVO seit langem . Der Grad der Unterstützung für NRVO kann von Compiler zu Compiler und von Version zu Version variieren.

RVO und NRVO sind nur Optimierungen. Und obwohl das Kopieren des Konstruktors und des Zuweisungsoperators nicht aufgerufen wird, sollten sie in der Klasse des Objekts liegen. Die Regeln haben sich in C ++ 17 etwas geändert: Jetzt wird RVO nicht als Kopierelision betrachtet, es ist obligatorisch geworden und der entsprechende Konstruktor und Zuweisungsoperator werden nicht benötigt.

Hinweis: (N) RVO ist in konstanter Form ein schlüpfriges Thema. Bis einschließlich C ++ 14 wurde nichts darüber gesagt, C ++ 17 erfordert RVO in solchen Ausdrücken, und das kommende C ++ 20 - verbietet.

Ein paar Worte zum Zusammenhang mit der Semantik der Verschiebung. Erstens ist (N) RVO noch effektiver, weil Der Verschiebungskonstruktor und der Destruktor müssen nicht aufgerufen werden. Zweitens, wenn NRVO nicht resultvon derselben Funktion zurückkehrt std::move(result), funktioniert es garantiert nicht. Um Standard zu paraphrasieren: RVO gilt für prvalue, NRVO gilt für lvalue, a std::move(result)ist xvalue.

Lesen Sie mehr: eins (Englisch) , zwei (Englisch) , drei (Englisch)

SFINAE


Ein Substitutionsfehler ist kein Fehler. Fehlgeschlagene Substitution ist kein Fehler. SFINAE ist eine Funktion des Instanziierungsprozesses von Vorlagen - Funktionen und Klassen - in C ++. Wenn eine bestimmte Vorlage nicht instanziiert werden kann, wird dies unter dem Strich nicht als Fehler angesehen, wenn andere Optionen vorhanden sind. Ein vereinfachter Algorithmus zur Auswahl der am besten geeigneten Funktionsüberladung funktioniert beispielsweise folgendermaßen:

  1. Der Name der Funktion wird aufgelöst - der Compiler sucht in allen betrachteten Namespaces nach allen Funktionen mit dem angegebenen Namen (siehe ADL ).
  2. Unangemessene Funktionen werden verworfen - nicht die Anzahl der Argumente, es ist keine Konvertierung von Argumenttypen erforderlich, es war nicht möglich, Typen für die Funktionsvorlage abzuleiten usw.
  3. (viable functions), . — .

SFINAE : , , , ( ). .

SFINAE , , . - , . . , .

Beispiel
 #include <iostream> #include <type_traits> #include <utility> template <class, class = void> struct HasToString : std::false_type {}; //    ,      //   -    ,  //     —  ,    ,   template <class T> struct HasToString<T, std::void_t<decltype(&T::toString)>> : std::is_same<std::string, decltype(std::declval<T>().toString())> {}; struct Foo { std::string toString() { return {}; } }; int main() { std::cout << HasToString<Foo>::value << std::endl; // 1 std::cout << HasToString<int>::value << std::endl; // 0 } 

C++17 static if SFINAE , C++20 . Mal sehen.

Lesen Sie mehr: eins (Russisch) , zwei (Englisch) , drei (Englisch)

SBO, SOO, SSO


Optimierung für kleine Puffer / Objekte / Zeichenfolgen. Optimierung kleiner Puffer / Objekte / Zeilen. Manchmal wird SSO im Sinne der Small Size Optimization verwendet, aber sehr selten. Wir gehen daher davon aus, dass es bei SSO um Zeichenfolgen geht. SBO und SOO sind einfach Synonyme, und SSO ist der bekannteste Sonderfall.

Alle Datenstrukturen, die dynamischen Speicher verwenden, nehmen sicherlich auch einen Platz auf dem Stapel ein. Zumindest um einen Zeiger auf einen Haufen zu speichern. Das Wesentliche dieser Optimierungen ist nicht, Speicher vom Heap für relativ kleine Objekte anzufordern (was relativ teuer ist), sondern sie in den bereits zugewiesenen Stapelspeicherplatz zu legen.

Zum Beispiel könnte std :: string folgendermaßen implementiert werden:

Beispiel
 class string { char* begin_; size_t size_; size_t capacity_; }; 

Die Größe dieser Klasse erhalte ich 24 Bytes (abhängig vom Compiler und der Plattform). Das heißt,Zeichenfolgen, die nicht länger als 24 Zeichen sind, können auf dem Stapel platziert werden. Eigentlich natürlich erst um 24, da man irgendwie zwischen Platzierung auf dem Stapel und auf dem Haufen unterscheiden muss. Aber hier ist der einfachste Weg für kurze Zeilen mit bis zu 8 Zeichen (gleiche Größe - 24 Byte):

Beispiel
 class string { union Buffer { char* begin_; char local_[8]; }; Buffer buffer_; size_t _size; size_t _capacity; }; 

Neben dem Fehlen von Zuordnungen auf dem Heap gibt es einen weiteren Vorteil - einen hohen Grad an Datenlokalität. Ein Array oder Vektor solcher optimierten Objekte belegt tatsächlich nur einen kontinuierlichen Speicherplatz.

Fast alle Implementierungen std::stringverwenden SSO und zumindest einige Implementierungen std::function. Es wird jedoch std::vectorniemals auf diese Weise optimiert, da der Standard verlangt, dass std::swapfür zwei Vektoren kein Kopieren oder Zuweisen ihrer Elemente verursacht wird und dass alle gültigen Iteratoren gültig bleiben. SBO wird es nicht erlauben, diese Anforderungen zu erfüllen (denn std::stringsie sind es nicht). Aber boost::container::small_vector, wie Sie sich vorstellen können, wird SBO verwendet .

Lesen Sie mehr: Zeit (Englisch) ,zwei

UDPATE


Vielen Dank an PyerK für diese zusätzliche Liste von Abkürzungen.

Lebenslauf


Qualifikanten wie const und volatile. constbedeutet, dass das Objekt / die Variable nicht geändert werden kann. Ein Versuch, dies zu tun, führt entweder zu einem Fehler zur Kompilierungszeit oder zu UB zur Laufzeit. volatilebedeutet, dass sich das Objekt / die Variable unabhängig von den Aktionen des Programms ändern kann (z. B. schreiben einige Mikrocontroller-Füllungen etwas in den Speicher), und der Compiler sollte den Zugriff darauf nicht optimieren. Der Zugriff auf ein volatileObjekt, das nicht über einen volatileLink oder Zeiger erfolgt, führt ebenfalls zu UB .

Lesen Sie mehr: eins (russisch) , zwei (englisch) , drei (russisch)

LTO


Verbindungszeitoptimierung. Linkoptimierung. Wie der Name schon sagt, erfolgt diese Optimierung während der Verknüpfung, d. H. Nach der Kompilierung. Der Linker kann etwas tun, was der Compiler nicht gewagt hat: einige Funktionen inline zu machen, nicht verwendeten Code und Daten wegzuwerfen. Erhöht natürlich die Verbindungszeit.

Weiterlesen: Zeit (Englisch)

PCH


Vorkompilierte Header. Vorkompilierte Header. Oft verwendete, aber selten geänderte Header-Dateien werden einmal kompiliert und im internen Compiler-Format gespeichert. Der Zusammenbau des Projekts dauert daher weniger, manchmal sogar viel weniger.

Lesen Sie mehr: Zeit (rus.)

Pgo


Profilgesteuerte Optimierung. Optimierung basierend auf Profilerstellungsergebnissen. Dies ist eine Programmoptimierungsmethode, jedoch nicht durch statische Code-Analyse, sondern durch Start des Testprogramms und Sammeln realer Statistiken. Beispielsweise kann das Verzweigen und Aufrufen virtueller Funktionen auf diese Weise optimiert werden.

Lesen Sie mehr: Zeit (rus.)

Seh / veh


Structured/Vectored Exception Handling. MSVC . try-catch SEH : __try , __except , __finally , , , , - , . . VEH , .

: (.)

TMP


Template Meta-Programmierung. Vorlagen-Metaprogrammierung. Metaprogrammierung ist, wenn ein Programm aufgrund seiner Arbeit ein anderes erstellt. Vorlagen in C ++ implementieren eine solche Metaprogrammierung. Der Vorlagen-Compiler generiert die erforderliche Anzahl von Klassen oder Funktionen. Es ist bekannt, dass TMP in C ++ Turing-vollständig ist, d. H. Jede Funktion kann darauf implementiert werden.

Lesen Sie mehr: Zeit (rus.)

Vla


Arrays mit variabler Länge. Arrays variabler Länge. Das heißt, Arrays, deren Länge zur Kompilierungszeit unbekannt ist:
 void foo(int n) { int array[n]; } 

Der C ++ - Standard erlaubt dies nicht. Was etwas seltsam ist, da sie seit dem C99-Standard in reinem C existieren. Und werden von einigen C ++ - Compilern als Erweiterung unterstützt.

Lesen Sie mehr: Zeit (rus.)

PS


Wenn ich etwas verpasst habe oder mich irgendwo geirrt habe - schreibe in die Kommentare. Denken Sie bitte daran, dass hier nur Abkürzungen aufgeführt sind, die in direktem Zusammenhang mit C ++ stehen. Für andere, aber nicht weniger nützlich, wird es einen separaten Beitrag geben.

Zweiter Teil

Source: https://habr.com/ru/post/de470265/


All Articles