
In den letzten Jahren hat C ++ sprunghafte Fortschritte gemacht, und es kann sehr, sehr schwierig sein, mit allen Feinheiten und Feinheiten der Sprache Schritt zu halten. Ein neuer Standard ist nicht weit entfernt, die Einführung neuer Trends ist jedoch nicht der schnellste und einfachste Prozess. Daher empfehle ich, einige Zeit vor C ++ 20 zu aktualisieren oder einige besonders „rutschige“ Stellen des aktuellen Standards zu entdecken Sprache.
Heute werde ich Ihnen sagen, warum, wenn constexpr kein Ersatz für Makros ist, was die "Interna" der strukturierten Bindung und ihre "Fallstricke" sind und es stimmt, dass die Kopierelision immer jetzt funktioniert und Sie jede Rückgabe ohne zu zögern schreiben können.
Wenn Sie keine Angst haben, sich die Hände ein wenig schmutzig zu machen und in die „Innenseiten“ Ihrer Zunge einzutauchen, heißen wir Sie bei Cat willkommen.
wenn constexpr
Beginnen wir mit dem einfachsten -
if constexpr
Sie mit
if constexpr
den Zweig für bedingte Ausdrücke verwerfen können, für den die gewünschte Bedingung selbst in der Kompilierungsphase nicht erfüllt ist.
Es scheint, dass dies ein Ersatz für das Makro
#if
, um die "zusätzliche" Logik auszuschalten? Nein. Überhaupt nicht.
Erstens hat ein solches
if
Eigenschaften, die für Makros nicht verfügbar sind. Im Inneren können Sie jeden
constexpr
Ausdruck zählen, der in
bool
constexpr
kann. Nun, und zweitens sollte der Inhalt des verworfenen Zweigs syntaktisch und semantisch korrekt sein.
Aufgrund der zweiten Anforderung,
if constexpr
nicht verwendet werden kann, sind nicht vorhandene Funktionen (plattformabhängiger Code kann auf diese Weise nicht explizit getrennt werden) oder aus Sicht der Konstruktionssprache schlecht (z. B. "
void T = 0;
").
Was ist der Sinn von
if constexpr
? Der Hauptpunkt liegt in den Vorlagen. Für sie gibt es eine spezielle Regel: Der verworfene Zweig wird nicht instanziiert, wenn die Vorlage instanziiert wird. Dies erleichtert das Schreiben von Code, der irgendwie von den Eigenschaften der Vorlagentypen abhängt.
In den Vorlagen sollte jedoch nicht vergessen werden, dass der Code in den Zweigen zumindest für eine (sogar rein potenzielle) Variante der Instanziierung korrekt sein muss. Daher ist es einfach
static_assert(false)
, beispielsweise
static_assert(false)
in einen der Zweige zu schreiben (dies ist erforderlich)
static_assert
abhängig von einem vorlagenabhängigen Parameter.
Beispiele:
void foo() {
template<class T> void foo() {
template<class T> void foo() { if constexpr (condition1) {
Dinge, an die man sich erinnern sollte
- Der Code in allen Zweigen muss korrekt sein.
- In Vorlagen wird der Inhalt verworfener Zweige nicht instanziiert.
- Der Code in einem Zweig muss für mindestens eine rein potenzielle Variante der Instanziierung der Vorlage korrekt sein.
Strukturierte Bindung

In C ++ 17 wurde ein recht praktischer Mechanismus zum Zerlegen verschiedener tupelartiger Objekte angezeigt, mit dem Sie ihre internen Elemente bequem und präzise an benannte Variablen binden können:
Mit einem tupelartigen Objekt meine ich ein solches Objekt, für das die Anzahl der verfügbaren internen Elemente zum Zeitpunkt der Kompilierung bekannt ist (aus "Tupel" - eine geordnete Liste mit einer festen Anzahl von Elementen (Vektor)).
Solche Definitionen fallen unter diese Definition als:
std::pair
,
std::tuple
,
std::array
, Arrays der Form "
T a[N]
" sowie verschiedene selbstgeschriebene Strukturen und Klassen.
Stop ... Können Sie Ihre eigenen Strukturen für die strukturelle Bindung verwenden? Spoiler: Sie können (obwohl Sie manchmal hart arbeiten müssen (aber mehr dazu weiter unten)).
Wie funktioniert es?
Die Arbeit der strukturellen Verknüpfung verdient einen separaten Artikel, aber da wir speziell über „rutschige“ Orte sprechen, werde ich versuchen, kurz zu erklären, wie alles funktioniert.
Der Standard bietet die folgende Syntax zum Definieren der Bindung:
attr (optional)
cv-auto ref-operator (optional)
Ausdruck [
Bezeichnerliste ];
attr
- optionale Attributliste;
cv-auto
- auto mit möglichen const / flüchtigen Modifikatoren;
ref-operator
- optionaler Referenzspezifizierer (& oder &&);
identifier-list
- eine Liste der Namen neuer Variablen;
expression
ist ein Ausdruck, der zu einem tupelartigen Objekt führt, das zum Binden verwendet wird (Ausdruck kann die Form " = expr
", " {expr}
" oder " (expr)
" haben).
Es ist wichtig zu beachten, dass die Anzahl der Namen in der
identifier-list
mit der Anzahl der Elemente im Objekt übereinstimmen muss, die sich aus dem
expression
.
Auf diese Weise können Sie Konstruktionen des Formulars schreiben:
const volatile auto && [a,b,c] = Foo{};
Und hier kommen wir zum ersten „rutschigen“ Ort: Treffen eines Ausdrucks der Form „
auto a = expr;
", Sie meinen normalerweise, dass der Typ"
a
"durch den Ausdruck"
expr
"berechnet wird, und Sie erwarten, dass im Ausdruck"
const auto& [a,b,c] = expr;
"Das gleiche wird gemacht, nur die Typen für"
a,b,c
"sind die entsprechenden
const&
element-Typen von"
expr
"...
Die Wahrheit ist anders: Der
cv-auto ref-operator
Spezifizierer wird verwendet, um den Typ einer unsichtbaren Variablen zu berechnen, der das Ergebnis der Berechnung von expr zugewiesen wird (dh der Compiler ersetzt "
const auto& [a,b,c] = expr
" durch "
const auto& e = expr
").
So erscheint eine neue unsichtbare Entität (im Folgenden werde ich sie {e} nennen), die Entität ist jedoch sehr nützlich: Sie kann beispielsweise temporäre Objekte materialisieren (daher können Sie sie sicher verbinden “
const auto& [a,b,c] = Foo {};
").
Die zweite rutschige Stelle folgt unmittelbar aus der Ersetzung durch den Compiler: Wenn der für {e} abgeleitete Typ keine Referenz ist, wird das Ergebnis von
expr
nach {e} kopiert.
Welche Typen haben Variablen in der
identifier-list
? Zunächst sind dies nicht genau Variablen. Ja, sie verhalten sich wie echte, gewöhnliche Variablen, aber nur mit dem Unterschied, dass sie sich auf eine mit ihnen
decltype
Entität beziehen und der
decltype
einer solchen Referenzvariablen den Typ der Entität erzeugt, auf den sich diese Variable bezieht:
std::tuple<int, float> t(1, 2.f); auto& [a, b] = t;
Die Typen selbst sind wie folgt definiert:
- Wenn {e} ein Array ist (
T a[N]
), ist der Typ eins - T, cv-Modifikatoren stimmen mit denen des Arrays überein.
- Wenn {e} vom Typ E ist und die Tupelschnittstelle unterstützt, werden die Strukturen definiert:
std::tuple_size<E>
std::tuple_element<i, E>
und Funktion:
get<i>({e}); // {e}.get<i>()
dann ist der Typ jeder Variablen der Typ std::tuple_element_t<i, E>
- In anderen Fällen entspricht der Typ der Variablen dem Typ des Strukturelements, an das die Bindung durchgeführt wird.
Wenn also nur sehr kurz, werden die folgenden Schritte mit der strukturellen Verknüpfung ausgeführt:
- Berechnung des Typs und Initialisierung der unsichtbaren Entität {e} basierend auf den Modifikatoren type
expr
und cv-ref
.
- Erstellen Sie Pseudovariablen und binden Sie sie an {e} Elemente.
Strukturelle Verknüpfung Ihrer Klassen / Strukturen
Das Haupthindernis für die Verknüpfung ihrer Strukturen ist die mangelnde Reflexion in C ++. Selbst der Compiler, der anscheinend genau wissen muss, wie diese oder jene Struktur im Inneren angeordnet ist, hat es schwer: Zugriffsmodifikatoren (öffentlich / privat / geschützt) und Vererbung erschweren die Sache erheblich.
Aufgrund solcher Schwierigkeiten sind die Beschränkungen für die Verwendung ihrer Klassen sehr streng (zumindest
vorerst :
P1061 ,
P1096 ):
- Alle internen nicht statischen Felder einer Klasse müssen aus derselben Basisklasse stammen und zum Zeitpunkt der Verwendung verfügbar sein.
- Oder die Klasse muss "Reflection" implementieren (unterstützt die Tupelschnittstelle).
Die Implementierung der Tupel-Schnittstelle ermöglicht es Ihnen, jede Ihrer Klassen zum Binden zu verwenden, sieht jedoch etwas umständlich aus und birgt eine weitere Gefahr. Verwenden wir sofort ein Beispiel:
Jetzt binden wir:
Foo foo; const auto& [f1] = foo; const auto [f2] = foo; auto& [f3] = foo; auto [f4] = foo;
Und es ist Zeit darüber nachzudenken, welche Typen wir haben? (Wer sofort antworten kann, verdient einen leckeren Schatz.)
decltype(f1); decltype(f2); decltype(f3); decltype(f4);
Warum ist das passiert? Die Antwort liegt in der Standardspezialisierung für
std::tuple_element
:
template<std::size_t i, class T> struct std::tuple_element<i, const T> { using type = std::add_const_t<std::tuple_element_t<i, T>>; };
std::add_const
fügt den Referenztypen keine
std::add_const
hinzu, daher ist der Typ für
Foo
immer
int&
.
Wie kann man das gewinnen? Fügen Sie einfach die Spezialisierung für
const Foo
:
template<> struct std::tuple_element<0, const Foo> { using type = const int&; };
Dann werden alle Typen erwartet:
decltype(f1);
Das gleiche Verhalten gilt übrigens beispielsweise für
std::tuple<T&>
- Sie können einen nicht konstanten Verweis auf das interne Element erhalten, obwohl das Objekt selbst konstant ist.
Dinge, an die man sich erinnern sollte
- "
cv-auto ref
" in " cv-auto ref [a1..an] = expr
" bezieht sich auf die unsichtbare Variable {e}.
- Wenn auf den abgeleiteten Typ {e} nicht verwiesen wird, wird {e} durch Kopieren initialisiert (sorgfältig mit "Schwergewichts" -Klassen).
- Gebundene Variablen sind "implizite" Links (sie verhalten sich wie Links, obwohl
decltype
einen decltype
für sie zurückgibt (es sei denn, die Variable verweist auf einen Link)).
- Bei der Verwendung von Referenztypen zum Binden ist Vorsicht geboten.
Rückgabewertoptimierung (rvo, Kopierelision)

Vielleicht war dies eine der am heißesten diskutierten Funktionen des C ++ 17-Standards (zumindest in meinem Freundeskreis). Und tatsächlich: C ++ 11 brachte die Semantik der Bewegung mit sich, die die Übertragung des "Inneren" des Objekts und die Schaffung verschiedener Fabriken erheblich vereinfachte, und C ++ 17 im Allgemeinen schien es möglich zu machen, nicht darüber nachzudenken, wie das Objekt von einer Fabrikmethode zurückgegeben werden sollte , - jetzt sollte alles ohne Kopieren sein und im Allgemeinen "bald wird alles auf dem Mars blühen" ...
Aber lassen Sie uns ein wenig realistisch sein: Die Optimierung des Rückgabewerts ist nicht die einfachste Implementierung. Ich empfehle dringend, diese Präsentation von cppcon2018: Arthur O'Dwyer „
Rückgabewertoptimierung: Härter als es aussieht “ anzusehen, in der der Autor erklärt, warum es schwierig ist.
Kurzer Spoiler:
Es gibt so etwas wie einen "Slot für den Rückgabewert". Dieser Slot ist im Wesentlichen nur ein Platz auf dem Stapel, der von demjenigen zugewiesen wird, der anruft und an den Angerufenen übergeht. Wenn der aufgerufene Code genau weiß, welches einzelne Objekt zurückgegeben wird, kann er es einfach sofort direkt in diesem Slot erstellen (vorausgesetzt, Größe und Typ des Objekts und des Slots sind identisch).
Was folgt daraus? Nehmen wir es anhand von Beispielen auseinander.
Hier wird alles gut - NRVO wird funktionieren, das Objekt wird sofort im "Slot" erstellt:
Base foo1() { Base a; return a; }
Hier ist es nicht mehr möglich, eindeutig zu bestimmen, welches Objekt das Ergebnis sein soll, daher wird der
Verschiebungskonstruktor (c ++ 11)
implizit aufgerufen :
Base foo2(bool c) { Base a,b; if (c) { return a; } return b; }
Hier ist es etwas komplizierter ... Da sich der Typ des Rückgabewerts vom deklarierten Typ unterscheidet, können Sie
move
nicht implizit aufrufen, sodass der Kopierkonstruktor standardmäßig aufgerufen wird. Um dies zu verhindern, müssen Sie
move
explizit aufrufen:
Base foo3(bool c) { Derived a,b; if (c) { return std::move(a); } return std::move(b); }
Es scheint, dass dies dasselbe ist wie
foo2
, aber der ternäre Operator ist eine sehr
eigenartige Sache ...
Base foo4(bool c) { Base a, b; return std::move(c ? a : b); }
Ähnlich wie
foo4
, aber auch ein anderer Typ, daher
move
genau ein
move
erforderlich:
Base foo5(bool c) { Derived a, b; return std::move(c ? a : b); }
Wie Sie den Beispielen entnehmen können, muss man sich auch in scheinbar trivialen Fällen noch Gedanken darüber machen, wie man Sinn zurückgibt ... Gibt es Möglichkeiten, das Leben ein wenig zu vereinfachen? Ja: clang unterstützt seit einiger Zeit die
Diagnose der Notwendigkeit, einen
move
explizit aufzurufen, und der neue Standard enthält mehrere Vorschläge (
P1155 ,
P0527 ), die einen expliziten
move
weniger notwendig machen.
Dinge, an die man sich erinnern sollte
- RVO / NRVO funktioniert nur, wenn:
- es ist eindeutig bekannt, welches einzelne Objekt im "Rückgabewertschlitz" erstellt werden soll;
- Rückgabeobjekt und Funktionstypen sind identisch.
- Wenn der Rückgabewert mehrdeutig ist, gilt Folgendes:
- Wenn die Typen des zurückgegebenen Objekts und der zurückgegebenen Funktion übereinstimmen, wird move implizit aufgerufen.
- Andernfalls müssen Sie move explizit aufrufen.
- Vorsicht beim ternären Operator: Er ist präzise, erfordert jedoch möglicherweise eine explizite Verschiebung.
- Es ist besser, Compiler mit nützlicher Diagnose (oder zumindest statische Analysatoren) zu verwenden.
Fazit
Und doch liebe ich C ++;)