
Konzepte, die in C ++ 20 erscheinen, sind ein langes und viel diskutiertes Thema. Trotz des im Laufe der Jahre angesammelten Materialüberschusses (einschließlich der Reden von Weltklasse-Experten) herrscht unter angewandten Programmierern (die nicht täglich mit dem Standard einschlafen) immer noch Verwirrung darüber, was C ++ 20-Konzepte sind und sind Wir brauchen, wenn enable_if im Laufe der Jahre überprüft wird. Teilweise liegt der Fehler darin, wie sich die Konzepte über ~ 15 Jahre entwickelt haben (Concepts Full + Concept Map -> Concepts Lite), und teilweise darin, dass sich herausstellte, dass sich die Konzepte von ähnlichen Tools in anderen Sprachen unterscheiden (Java / C # generische Grenzen, Rust-Merkmale,. ..).
Unter dem Schnitt - Video und Transkript eines Berichts von Andrey Davydov vom ReSharper C ++ - Team der C ++ Russia 2019- Konferenz. Andrey gab einen kurzen Überblick über die konzeptbezogenen Innovationen von C ++ 20, woraufhin er die Implementierung einiger Klassen und Funktionen von STL untersuchte und C ++ 17- und C ++ 20-Lösungen verglich. Weiter ist die Geschichte in seinem Namen.
Sprechen Sie über Konzepte. Dies ist ein ziemlich komplexes und umfangreiches Thema. Als ich mich auf den Bericht vorbereitete, hatte ich einige Schwierigkeiten. Ich beschloss, mich der Erfahrung eines der besten Redner der C ++ - Community Andrei Alexandrescu zuzuwenden.
Im November 2018 fragte Andrei bei der Eröffnung des Meetings C ++ das Publikum, was das nächste große Feature von C ++ sein würde:
- Konzepte
- Metaklassen
- oder Selbstbeobachtung?
Beginnen wir mit dieser Frage. Denken Sie, dass das nächste große Feature in C ++ Konzepte sein werden?
Konzepte sind laut Alexandrescu langweilig. Dies ist die langweilige Sache, die ich Ihnen vorschlage. Außerdem kann ich immer noch nicht genauso interessant und brandaktuell über Metaklassen wie Herb Sutter oder über Selbstbeobachtung wie Alexandrescu sprechen.
Was meinen wir, wenn wir über Konzepte in C ++ 20 sprechen? Diese Funktion wurde seit mindestens 2003 diskutiert und hat sich in dieser Zeit stark weiterentwickelt. Mal sehen, welche neuen konzeptbezogenen Funktionen in C ++ 20 erschienen sind.
Eine neue Entität namens "Konzepte" wird durch das Schlüsselwort concept
definiert. Dies ist ein Prädikat für Vorlagenparameter. Es sieht ungefähr so aus:
template <typename T> concept NoThrowDefaultConstructible = noexept(T{}); template <typename From, typename To> concept Assignable = std::is_assignable_v<From, To>
Ich habe nicht nur den Ausdruck "für Vorlagenparameter" und nicht "für Typen" verwendet, da Konzepte für nicht standardmäßige Vorlagenparameter definiert werden können. Wenn Sie überhaupt nichts zu tun haben, können Sie ein Konzept für eine Zahl definieren:
template<int I> concept Even = I % 2 == 0;
Es ist jedoch sinnvoller, typische und atypische Vorlagenparameter zu mischen. Wir nennen einen Typ klein, wenn seine Größe und Ausrichtung die angegebenen Grenzwerte nicht überschreitet:
template<typename T, size_t MaxSize, size_t MaxAlign> concept Small = sizeof(T) <= MaxSize && alignof(T) <= MaxAlign;
Wahrscheinlich ist noch nicht klar, warum wir eine neue Entität in der Sprache constexpr bool
müssen und warum das Konzept nicht nur eine constexpr bool
Variable ist.
Wie werden Konzepte verwendet?
Lassen Sie uns zum Verständnis sehen, wie Konzepte verwendet werden.
Erstens können sie genau wie constexpr bool
Variablen überall dort verwendet werden, wo Sie zur Kompilierungszeit einen booleschen Ausdruck benötigen. Zum Beispiel innerhalb von static_assert
oder innerhalb von noexcept
Spezifikationen:
Zweitens können beim Definieren von Vorlagenparametern anstelle der Schlüsselwörter typename
oder class
Konzepte verwendet werden. Definieren Sie eine einfache optional
Klasse, in der einfach ein Paar des initialized
Booleschen Flags und der Werte gespeichert wird. Natürlich gilt eine solche optional
nur für triviale Typen. Daher schreiben wir hier Trivial
, und wenn wir versuchen, aus etwas Nicht-Trivialem, beispielsweise aus std::string
, zu instanziieren, tritt ein Kompilierungsfehler auf:
Konzepte können teilweise angewendet werden. Zum Beispiel implementieren wir unsere Klasse any
mit kleiner Pufferoptimierung. Definieren Sie die SB
Struktur (kleiner Puffer) mit einer festen Size
und Alignment
. Wir speichern die Vereinigung von SB
und den Zeiger auf dem Heap. Und jetzt, wenn ein kleiner Typ in den Konstruktor kommt, können wir ihn einfach in SB
. Um festzustellen, dass ein Typ klein ist, schreiben wir, dass er dem Konzept von Small
. Das Small
Konzept bestand aus drei Vorlagenparametern: Wir haben zwei definiert und eine Funktion aus einem Vorlagenparameter erhalten:
Es gibt eine kürzere Aufzeichnung. Wir schreiben den Namen des Template-Parameters, möglicherweise mit einigen Argumenten, vor auto
. Das vorherige Beispiel wird folgendermaßen umgeschrieben:
Wahrscheinlich können Sie jetzt an jedem Ort, an dem wir auto
schreiben, den Namen des Konzepts davor schreiben.
Definieren Sie die Funktion get_handle
, die ein handle
für das Objekt zurückgibt.
Wir gehen davon aus, dass kleine Objekte selbst handle
werden und bei großen Objekten ein Zeiger auf sie handle
. Da wir zwei Zweige haben, if constexpr
Ausdrücke unterschiedlichen Typs bezeichnen, ist es für uns zweckmäßig, den Typ dieser Funktion nicht explizit anzugeben, sondern den Compiler if constexpr
, ihn auszugeben. Wenn wir dort einfach auto
, verlieren wir die Information, dass der angegebene Wert klein ist und den Zeiger nicht überschreitet:
In C ++ 20 kann davor geschrieben werden, dass es nicht nur auto
, sondern auch auto
:
Benötigt Ausdruck
Benötigt Ausdruck ist eine ganze Familie von expression'ov, alle sind vom Typ bool
und werden in Kompilierungszeit berechnet. Sie werden verwendet, um Anweisungen zu Ausdrücken und Typen zu testen. Benötigt Ausdruck ist sehr nützlich für die Definition von Konzepten.
Constructible
Beispiel. Diejenigen, die in meinem vorherigen Bericht waren, haben ihn bereits gesehen:
template<typename T, typename... Args> concept Constructible = requires(Args... args) { T{args...} };
Und ein Beispiel mit Comparable
. Angenommen, Typ T
ist Comparable
wenn zwei Objekte vom Typ T
mit dem Operator "less" verglichen werden können und das Ergebnis in bool
konvertiert wird. Dieser Pfeil und der Typ danach bedeuten, dass der bool
in bool
konvertiert wird und nicht, dass er gleich bool
:
template<typename T> concept Comparable = requires(T const & a, T const & b) { {a < b} -> bool; };
Was wir untersucht haben, reicht bereits aus, um ein vollwertiges Beispiel für die Verwendung von Konzepten zu zeigen.
Wir haben bereits ein Comparable
Konzept. Definieren wir Konzepte für Iteratoren. RandomAccessIterator
, RandomAccessIterator
ist ein BidirectionalIterator
und einige andere Eigenschaften. Damit definieren wir das Konzept von Sortable
. Range
wird als Sortable
wenn sein RandomAccess
Iterator und seine Elemente verglichen werden können. Und jetzt können wir eine sort
schreiben, die nicht nur das, sondern auch den Sortable Range
akzeptiert:
Wenn wir nun versuchen, diese Funktion von etwas aufzurufen , das das Sortable- Konzept nicht erfüllt, erhalten wir vom Compiler einen guten, SFINAE-freundlichen Fehler mit einer klaren Meldung. Versuchen wir, eine std::list
'oder Vektor von Elementen zu instanziieren, die nicht vergleichbar sind:
Haben Sie bereits ein ähnliches Beispiel für die Verwendung von Konzepten oder etwas sehr Ähnliches gesehen? Ich habe das schon mehrmals gesehen. Ehrlich gesagt hat es mich überhaupt nicht überzeugt. Müssen wir so viele neue Entitäten in der Sprache umzäunen, wenn wir dies in C ++ 17 bekommen können?
Ich habe das Keyword- concept
Makro eingegeben und Comparable
auf diese Weise neu geschrieben. Es ist etwas hässlicher geworden, und dies deutet darauf hin, dass Ausdruck wirklich eine nützliche und bequeme Sache ist. Also haben wir das Konzept von enable_if
definiert und mit enable_if
angegeben, dass die enable_if
den Sortable Range
akzeptiert.
Sie könnten denken, dass diese Methode aufgrund von Kompilierungsfehlermeldungen viel verliert, aber tatsächlich ist dies eine Frage der Qualität der Compiler-Implementierung. Nehmen wir an, Clang hat enable_if
Aufhebens um dieses Thema gemacht und insbesondere darauf enable_if
Sie das erste Argument haben, wenn Sie enable_if
Wenn false
berechnet wird, wird dieser Fehler angezeigt, sodass eine solche Anforderung nicht erfüllt wurde.
Das obige Beispiel scheint durch Konzepte geschrieben zu sein. Ich habe eine Hypothese: Dieses Beispiel ist nicht schlüssig, weil es nicht das Hauptmerkmal von Konzepten verwendet - erfordert Klausel.
Benötigt Klausel
Die Requires-Klausel hängt von fast jeder Vorlagendeklaration oder einer Nicht-Vorlagenfunktion ab. Syntaktisch sieht dies wie das Schlüsselwort require aus, gefolgt von einem booleschen Ausdruck. Dies ist erforderlich, um den Kandidaten für die Vorlagenspezialisierung oder -überladung herauszufiltern, dh es funktioniert genauso wie SFINAE, nur korrekt und nicht durch Hacks:
Wo in unserem sortierten Beispiel können wir die require-Klausel verwenden? Anstelle einer kurzen Syntax zum Anwenden von Konzepten schreiben wir Folgendes:
template<typename R> concept Sortable = RandomAccessIterator<Iterator<R>> && Comparable<ValueType<R>>; template<typename Range> requires Sortable<Range> void sort(Range &) { ... }
Es scheint, dass der Code nur schlechter und größer wurde. Aber jetzt können wir das Sortable
Konzept loswerden. Aus meiner Sicht ist dies eine Verbesserung, da das Sortable
Konzept Sortable
tautologisch ist: Wir nennen Sortable
alles, was an die sort
kann. Dies hat keine physikalische Bedeutung. Wir schreiben den Code folgendermaßen um:
//template<typename R> concept Sortable // = RandomAccessIterator<Iterator<R>> && Comparable<ValueType<R>>; template<typename Range> requires RandomAccessIterator<Iterator<Range>> && Comparable<ValueType<Range>>; void sort(Range &) { ... }
Die Liste der konzeptbezogenen Innovationen in C ++ 20 sieht folgendermaßen aus. Die Elemente in dieser Liste werden sortiert, indem der Nutzen der Funktion aus meiner subjektiven Sicht erhöht wird:
- Neues Entitätskonzept. Es scheint mir, dass es möglich wäre, auf die
concept
indem constexpr bool
Variablen mit zusätzlicher Semantik ausgestattet werden. - Spezielle Syntax zum Anwenden von Konzepten. Natürlich ist es angenehm, aber dies ist nur die Syntax. Wenn C ++ - Programmierer Angst vor schlechter Syntax hätten, wären sie vor langer Zeit aus Angst gestorben.
- Benötigt Ausdruck ist wirklich eine coole Sache, und es ist nicht nur nützlich, um Konzepte zu definieren.
- Die Requires-Klausel ist der größte Wert von Konzepten. Sie ermöglicht es Ihnen, SFINAE und andere legendäre Schrecken von C ++ - Vorlagen zu vergessen.
Mehr dazu erfordert Ausdruck
Bevor wir uns mit der Erforderlichkeitsklausel befassen, müssen einige Worte zum Ausdruck erforderlich sein.
Erstens können sie nicht nur zum Definieren von Konzepten verwendet werden. Der Microsoft-Compiler hat seit jeher die Erweiterung __if_exists
- __if_not_exists
. Es ermöglicht der Kompilierungszeit, das Vorhandensein eines Namens zu überprüfen und abhängig davon die Kompilierung eines Codeblocks zu aktivieren oder zu deaktivieren. Und in der Codebasis, mit der ich vor einigen Jahren gearbeitet habe, war es so etwas. Es gibt eine Funktion f()
, sie nimmt einen Punkt des Vorlagentyps und nimmt die Höhe von diesem Punkt. Es kann durch einen dreidimensionalen oder zweidimensionalen Punkt instanziiert werden. Für dreidimensional betrachten wir die z
Koordinate als Höhe, für zweidimensional wenden wir uns einem speziellen Oberflächensensor zu. Es sieht so aus:
struct Point2 { float x, y; }; struct Point3 { float x, y, z; }; template<typename Point> void f(Point const & p) { float h; __if_exists(Point::z) { h = pz; } __if_not_exists(Point::z) { h = sensor.get_height(p); } }
In C ++ 20 können wir dies umschreiben, ohne Compiler-Erweiterungen mit Standardcode zu verwenden. Es scheint mir, dass es nicht schlimmer ist:
struct Point2 { float x, y; }; struct Point3 { float x, y, z; }; template<typename Point> void f(Point const & p) { float h; if constexpr(requires { Point::z; }) h = pz; else h = sensor.get_height(p); }
Der zweite Punkt ist, dass Sie wachsam sein müssen, wenn die Syntax Ausdruck erfordert.
Sie sind ziemlich mächtig, und diese Kraft wird durch die Einführung vieler neuer syntaktischer Konstruktionen erreicht. Sie können zumindest zuerst verwirrt sein.
Definieren wir ein Sizable
Konzept, das überprüft, ob ein Container eine konstante Methodengröße size
, die size_t
zurückgibt. Wir erwarten natürlich, dass der vector<int>
Sizable
, aber dieser static_assert
. Verstehst du, warum wir einen Fehler haben? Warum wird dieser Code nicht kompiliert?
template<typename Container> concept Sizable = requires(Container const & c) { c.size() -> size_t; }; static_assert(Sizable<vector<int>>);
Lassen Sie mich Ihnen den Code zeigen, der kompiliert wird. Eine solche X
Klasse erfüllt das Sizable
Konzept. Jetzt verstehen Sie, was wir ein Problem haben?
struct X { struct Inner { int size_t; }; Inner* size() const; }; static_assert(Sizable<X>);
Lassen Sie mich die Code-Hervorhebung korrigieren. Links ist der Code farbig, wie ich möchte. Tatsächlich sollte es aber wie rechts gemalt sein:

Sehen Sie, die Farbe von size_t
, die hinter dem Pfeil steht, hat sich geändert? Ich wollte, dass es ein Typ ist, aber es ist nur das Feld, auf das wir zugreifen. Alles, was wir haben, erfordert Ausdruck, ist ein großer Ausdruck, und wir überprüfen seine Richtigkeit. Ja, für Typ X
ist dies ein gültiger Ausdruck, für vector<int>
nein. Um das zu erreichen, was wir wollten, müssen wir den Ausdruck in geschweiften Klammern verwenden:
template<typename Container> concept Sizable = requires(Container const & c) { {c.size()} -> size_t; }; static_assert(Sizable<vector<int>>);
Dies ist jedoch nur ein lustiges Beispiel. Im Allgemeinen müssen Sie nur vorsichtig sein.
Beispiele für die Verwendung von Konzepten
Implementierung von Paarklassen
Weiterhin werde ich einige STL-Fragmente demonstrieren, die in C ++ 17 implementiert werden können, aber ziemlich umständlich sind.
Und dann werden wir sehen, wie wir in C ++ 20 die Implementierung verbessern können.
Beginnen wir mit der pair
.
Dies ist eine sehr alte Klasse, sie befindet sich immer noch in C ++ 98.
Es enthält also keine komplizierte Logik
Ich möchte, dass seine Definition ungefähr so aussieht.
Aus meiner Sicht sollte es ungefähr so enden:
template<typename F, typename S> struct pair { F f; S s; ... };
Laut cppreference haben pair
Designer jedoch nur 8 Teile.
Wenn Sie sich beispielsweise die tatsächliche Implementierung in der Microsoft STL ansehen, gibt es bis zu 15 Konstruktoren der pair
. Wir werden uns diese ganze Kraft nicht ansehen und uns auf den Standardkonstruktor beschränken.
Es scheint, dass es etwas kompliziertes ist? Zunächst verstehen wir, warum es benötigt wird. Wir wollen, wenn eines der Argumente der pair
von einem trivialen Typ war, sagen wir int
, dann wurde es nach dem pair
Paarklasse auf Null initialisiert und blieb nicht uninitialisiert. Dazu möchten wir einen Konstruktor schreiben, der die Wertinitialisierung für die Felder f
(first) und s
(second) aufruft.
template<typename F, typename S> struct pair { F f; S s; pair() : f() , s() {} };
Wenn wir versuchen, ein pair
aus etwas zu instanziieren, das keinen Standardkonstruktor hat, beispielsweise aus einer solchen Klasse
, erhalten wir leider sofort einen Kompilierungsfehler. Das gewünschte Verhalten ist, dass, wenn Sie versuchen, ein pair
erstellen, der Standard ein Kompilierungsfehler ist. Wenn wir jedoch die Werte von f
und s
explizit übergeben, funktioniert alles:
struct A { A(int); }; pair<int, A> a2;
Machen Sie dazu den Standardkonstruktor zu einer Vorlage und beschränken Sie ihn auf SFINAE.
Die erste Idee, die mir in den Sinn kommt, ist, dass wir schreiben, dass dieser Konstruktor nur zulässig ist, wenn f
und s
is_default_constructable
:
template<typename F, typename S> struct pair { F f; S s; template<typename = enable_if_t<conjunction_v< is_default_constructible<F>,
Dies funktioniert nicht, da die Argumente enable_if_t
nur von den Vorlagenparametern der Klasse abhängen. Das heißt, nach der Ersetzung der Klasse werden sie unabhängig und können sofort berechnet werden. Wenn wir jedoch false
, erhalten wir erneut einen harten Compilerfehler.
Um dies zu überwinden, fügen wir diesem Konstruktor weitere Vorlagenparameter hinzu und lassen die Bedingung enable_if_t
von diesen Vorlagenparametern abhängen:
template<typename F, typename S> struct pair { F f; S s; template<typename T = F, typename U = S, typename = enable_if_t<conjunction_v< is_default_constructible<T>, is_default_constructible<U> >>> pair() : f(), s() {} };
Die Situation ist ziemlich lustig. Tatsache ist, dass die Vorlagenparameter T
und U
vom Benutzer nicht explizit festgelegt werden können. In C ++ gibt es keine Syntax zum expliziten Festlegen der Vorlagenparameter des Konstruktors. Sie können vom Compiler nicht ausgegeben werden, da sie nirgendwo angezeigt werden können. Sie können nur vom Standardwert stammen. Das heißt, dieser Code unterscheidet sich effektiv nicht vom Code im vorherigen Beispiel. Aus Sicht des Compilers ist dies jedoch gültig, jedoch nicht im vorherigen Beispiel.
Wir haben unser erstes Problem gelöst, aber wir stehen vor einem zweiten, etwas subtileren. Angenommen, wir haben Klasse B
mit einem expliziten Standardkonstruktor und möchten implizit das pair<int, B>
konstruieren:
struct B { explicit B(); }; pair<int, B> p = {};
Wir können es schaffen, aber standardmäßig sollte es nicht funktionieren. Standardmäßig sollte ein Paar implizit nur standardmäßig erstellt werden, wenn beide Elemente implizit standardmäßig erstellt werden.
Frage: Müssen wir den Konstruktor des expliziten Paares schreiben oder nicht? In C ++ 17 haben wir eine Solomon-Lösung: Schreiben wir sowohl solche als auch solche.
template<typename F, typename S> struct pair { F f; S s; template<typename T = F, typename U = S, typename = enable_if_t<conjunction_v< is_default_constructible<T>, is_default_constructible<U>, is_implicity_default_constructible<T>, is_implicity_default_constructible<U> >>> pair() : f(), s() {} template<...> explicit pair() : f(), s() {} };
Jetzt haben wir zwei Standardkonstruktoren:
- Wir werden einen von ihnen gemäß SFINAE für den Fall abschneiden, dass die Elemente implizit standardmäßig konstruierbar sind.
- und die zweite für den umgekehrten Fall.
Übrigens kenne ich eine solche Lösung, um das Typmerkmal is_implicitly_default_constructible
in C ++ 17 zu implementieren, aber ich kenne die Lösung ohne SFINAE nicht:
template<typrname T> true_type test(T, int); template<typrname T> false_type test(int, ...); template<typrname T> using is_implicity_default_constructible = decltype(test<T>({}, 0));
Wenn wir jetzt versuchen, das pair <int, B>
implizit zu konstruieren, erhalten wir einen Kompilierungsfehler, wie wir wollten:
template<..., typename = enable_if_t<conjuction_v< is_default_constructible<T>, is_default_constructible<U>, is_implicity_default_constructible<T>, is_implicity_default_constructible<U> >>> ... pair<int, B> p = {}; ... candidate template ignored: requirement 'conjunction_v< is_default_constructible<int>, is_default_constructible<B>, is_implicity_default_constructible<int>, is_implicity_default_constructible<B> >' was not satisfied [with T=int, U=B]
In verschiedenen Compilern ist dieser Fehler unterschiedlich verständlich. In diesem Fall sagt der Microsoft-Compiler beispielsweise: "Es war nicht möglich, ein Paar <int, B>
aus leeren geschweiften Klammern zu erstellen." GCC und Clang werden noch hinzufügen: „Wir haben so und so einen Konstruktor ausprobiert, keiner von ihnen ist aufgetaucht“, und sie werden jeweils einen Grund angeben.
Welche Designer haben wir hier? Es gibt Konstruktoren, die vom Compiler zum Kopieren und Verschieben generiert wurden, einige wurden von uns geschrieben. Beim Kopieren und Verschieben ist alles einfach: Sie erwarten einen Parameter, wir übergeben Null. Für unseren Konstruktor ist der Grund, dass die Substitution eine Diskette ist.
GCC sagt: "Substitution fehlgeschlagen, versucht, den enable_if<false>
in enable_if<false>
- konnte enable_if<false>
nicht gefunden werden."
Clang betrachtet diese Situation als Sonderfall. Daher zeigt er diesen Fehler sehr cool. Wenn wir bei der Auswertung von enable_if
ersten Arguments false
, schreibt er, dass die spezifische Anforderung nicht erfüllt ist.
Gleichzeitig haben wir selbst unser Leben verdorben, indem wir die umständliche Bedingung enable_if
. Wir sehen, dass es sich als false
, aber wir sehen noch nicht warum.
Dies kann überwunden werden, wenn wir enable_if
in vier enable_if
:
template<..., typename = enable_if_t<is_default_constructible<T>::value>>, typename = enable_if_t<is_default_constructible<U>::value>>, typename = enable_if_t<is_implicity_default_constructible<T>::value>>, typename = enable_if_t<is_implicity_default_constructible<U>::value>> > ...
Wenn wir nun versuchen, implizit ein Paar zu konstruieren, erhalten wir eine hervorragende Nachricht, dass dieser und jener Kandidat nicht geeignet ist, weil das is_implicitly_default_constructable
nicht erfüllt ist:
pair<int, B> p = {};
Es mag sogar für eine Sekunde scheinen: Warum brauchen wir ein Konzept, wenn wir einen so coolen Compiler haben?
Wir erinnern uns jedoch daran, dass standardmäßig zwei Vorlagenfunktionen zum Implementieren des Konstruktors verwendet werden und jede Vorlage sechs Vorlagenparameter enthält. Für eine Sprache, die behauptet, mächtig zu sein, ist dies eine Pleite.
Wie hilft uns C ++ 20? Entfernen Sie zunächst die Muster, indem Sie diese mit der require-Klausel umschreiben. Was wir zuvor in enable_if
, schreiben wir jetzt in das Argument der enable_if
Klausel:
template<typename F, typename S> struct pair { F f; S s; pair() requires DefaultConstructible<F> && DefaultConstructible<S> && ImplicitlyDefaultConstructible<F> && ImplicitlyDefaultConstructible<S> : f(), s() {} explicit pair() ... };
Das Konzept von ImplicitlyDefaultConstructible
kann mit einem so netten Ausdruck implementiert werden, in dem fast nur Klammern unterschiedlicher Form verwendet werden:
template<typename T> concept ImplicitlyDefaultConstructible = requires { [] (T) {} ({}); };
T
ImplicitlyDefaultConstructible
, , T
. , , SFINAE.
C++20: (conditional) explicit
( noexcept
). explicit
. , explicit
.
template<typename F, typename S> struct pair { F f; S s; explicit(!ImplicityDefaultConstructible<F> || !ImplicityDefaultConstructible<S>) pair() requires DefaultConstructible<F> && DefaultConstructible<S> : f(), s() {} };
, . , DefaultConstructible
, explicit
, explicit
.
Optional C++17
Optional
. , .
. ? , C++ :
enum Option<T> { None, Some(t) }
:
class Optional<T> { final T value; Optional() {this.value = null; } Optional(T value) {this.value = value; } }
C++: null
, value-?
C++ . initialized
storage
, , . T
, optional
T
, C++ memory model.
template<typename T> class optional { bool initialized; aligned_storage_t<sizeof(T), alignof(T)> storage; ...
, . : optional
, optional
. :
... T & get() & { return reinterpret_cast<T &>(storage); } T const & get() const & { return reinterpret_cast<T const &>(storage); } T && get() && { return move(get()); } optional() noexcept : initialized(false) {} optional(T const & value) noexcept(NothrowCopyConstructible<T>) : initialized(true) { new (&storage) T(value); } ~optional() : noexcept(NothrowDestructible<T>) { if (initialized) get().~T(); } };
optional
' . optional
, optional
, , optional
. , copy move .
. : assignment . , . . copy constructor. :
template<typename T> class optional { bool initialized; aligned_storage_t<sizeof(T), alignof(T)> storage; ... optional(optional const & other) noexcept(NothrowCopyConstructible<T>) : initialized(other.initialized) { if (initialized) new (&storage) T(other.get()); } optional& operator =(optional && other) noexcept(...) {...} };
move assignment. , :
optional
' , .- , .
- , — , , .
T
: move constructor, move assignment :
optional& operator =(optional && other) noexcept(...) { if (initialized) { if (other.initialized) { get() = move(other.get()); } else { initialized = false; other.initilized = true; new(&other.storage) T(move(get())); get().~T(); } } else if (other.initialized) { initialized = true; other.initialized = false; new(&storage) T(move(get())); other.get().~T(); } return *this; }
noexcept
:
optional& operator =(optional && other) noexcept(NothrowAssignable<T> && NothrowMoveConstructible<T> && NothrowDestructible<T>) { if (initialized) { if (other.initialized) { get() = move(other.get()); } else { initialized = false; other.initialized = true; new (&other.storage) T(move(get())); get().~T(); } } ... }
optional
:
template<typename T> class optional { ... optional(optional const &) noexcept(NothrowCopyConstructible<T>); optional(optional &&) noexcept(NothrowMoveConstructible<T>); optional& operator =(optional const &) noexcept(...); optional& operator =(optional &&) noexcept(...); };
, pair
:
Optional
-, (, deleted), compilation error.
template class optional<unique_ptr<int>>;
, optional
unique_ptr
,
copy constructor copy assignment deleted. , , SFINAE.
copy move assignment , — . - , copy , .
— . copy : deleted operation , , operation:
deleted_copy_construct
delete
, — default
;copy_construct
, copy_construct
.
template<class Base> struct deleted_copy_construct : Base { deleted_copy_construct(deleted_copy_construct const &) = delete; deleted_copy_construct(deleted_copy_construct &&) = default; deleted_copy_construct& operator =(deleted_copy_construct const &) = default; deleted_copy_construct& operator =(deleted_copy_construct &&) = default; }; template<class Base> struct copy_construct : Base { copy_construct(copy_construct const & other) noexcept(noexcept(Base::construct(other))) { Base::construct(other); } copy_construct(copy_construct &&) = default; copy_construct& operator =(copy_construct const &) = default; copy_construct& operator =(copy_construct &&) = default; };
select_copy_construct
, , CopyConstrictuble
, copy_construct
, deleted_copy_construct
:
template<typename T, class Base> using select_copy_construct = conditional_t<CopyConstructible<T> copy_construct<Base> deleted_copy_construct<Base> >;
, optional
, optional_base
, copy construct
, optional
select_copy_construct<T, optional_base<T>>
. copy :
template<typename T> class optional_base { ... void construct(optional_base const & other) noexcept(NothrowCopyConstructible<T>) { if ((initialized = other.initialized)) new (&storage) t(other.get()); } }; template<typename T> class optional : select_copy_construct<T, optional_base<T>> { ... };
. , , copy_construct
, move_construct
copy_construct
, copy_assign
, , move_construct
, , , :
template<typename T, class Base> using select_move_construct = select_copy_construct<T, conditional_t<MoveConstructible<T>, move_construct<Base> > >; template<typename T, class Base> using select_copy_assign = select_move_construct<T, conditional_t<CopyAssignable<T> && CopyConstructible<T>, copy_assign<Base> delete_copy_assign<Base> > >;
, move_assign
copy_assign
, optional_base
, assignment construct
assign
, optional
select_move_assign<T, optional_base<T>>
.
template<typename T, class Base> using select_move_assign = select_copy_assign<T, ...>; template<typename T> class optional_base { ... void construct(optional_base const&) noexcept(NothrowCopyConstructible<T>); void construct(optional_base &&) noexcept(NothrowMoveConstructible<T>); optional_base& assign(optional_base &&) noexcept(...); optional_base& assign(optional_base const &) noexcept(...); }; template<typename T> class optional : select_move_assign<T, optional_base<T>> { ... };
, :
optional<unique_ptr>
deleted_copy_construct
,
move_construct
. !
optional<unique_ptr<int>> : deleted_copy_construct<...> : move_construct<...> : deleted_copy_assign<...> : move_assign<...> : optional_base<unique_ptr<int>>
: optional
TriviallyCopyable
TriviallyCopyable
.
TriviallyCopyable
? , T
TriviallyCopyable
,
memcpy
. , .
, , , . resize
vector
TriviallyCopyable
, memcpy
, , . , , .
TriviallyCopyable
, , static_assert
', copy-move :
template<typename T> class optional : select_move_assign<T, optional_base<T>> {...}; static_assert(TriviallyCopyable<optional<int>>); static_assert(TriviallyCopyConstructible<optional<int>>); static_assert(TriviallyMoveConstructible<optional<int>>); static_assert(TriviallyCopyAssignable <optional<int>>); static_assert(TriviallyMoveAssignable <optional<int>>); static_assert(TriviallyDestructible <optional<int>>);
static_assert
' . , , . optional
— aligned_storage
, , , , TriviallyCopyable
.
, . , TriviallyCopyable
.
, . select_copy_construct
:
template<typename T, class Base> using select_copy_construct = conditional_t<CopyConstructible<T>, copy_construct<Base> deleted_copy_construct<Base> >;
CopyContructible
copy_construct
, if
compile-time: CopyContructible
TriviallyCopyContructible
, Base
.
template<typename T, class Base> using select_copy_construct = conditional_t<CopyConstructible<T>, conditional_t<TriviallyCopyConstructible<T>, Base, copy_construct<Base> >, deleted_copy_construct<Base> >;
, copy . , select_destruct
. int
, - - , .
template<typename T, class Base> using select_destruct = conditional_t<TriviallyDenstructible<T>, Base, destruct<Base> > >;
, , . , , :
optional<unique_ptr<int>> : deleted_copy_construct<...> : move_construct<...> : deleted_copy_assign<...> : move_assign<...> : destruct<optional_base<unique_ptr<int>>> : optional_base<unique_ptr<int>>
, C++17 optional
7; : operation
, deleted_operation
select_operation
; construct
assign
. , .
- . . : deleted.
, noexcept
.
, , , trivial
, noexcept
. , , trivial
noexcept
, noexcept
, deleted
. . , , .
type trait, , . , , copy : deleted
, nothrow
, ?
, - special member, , , , :
- ,
deleted
, = delete
deleted_copy_construct
; - ,
copy_construct
, c noexcept ; - , , , .
.
optional C++20
C++20 optional
copy ?
:
T
CopyConstructible
, deleted
;TriviallyCopyConstructible
, ;noexcept
.
template<typename T> class optional { ... optional(optional const &) requires(!CopyConstructible<T>) = delete;
, . -, , T
requires clause false
. requires(false)
, , overload resolution. , requires(true)
, .
, .
requires clause = delete
:
= delete
overload resolution, , , deleted .requires(false)
overload resolution.
, copy , , requires clause. .
, . ! C++ , ? , , . , , , . , , , , , optional
.
, , GCC internal compiler error, Clang . , . , .
, , optional
C++20. , , C++17.
aligned_storage aligned_union
: aligned_storage
reinterpret_cast
, reinterpret_cast
constexpr . , compile-time optional
, compile-time. STL aligned_storage
optional
aligned_union
variant
. , , STL Boost optional
variant
. variant
, :
template<bool all_types_are_trivially_destructible, typename...> class _Variant_storage_; template<typename... _Types> using _Variant_storage = _Variant_storage_< conjunction_v<is_trivially_destructible<_Types>...>, _Types... >; template<typename _First, typename... _Rest> class _Variant_storage_<true, _First, _Rest...> { union { remove_const_t<First> _Head; _Variant_storage<_Rest...> _Tail; }; };
variant
. _Variant_storage_
, , -, , variant
, -, . , trivially_destructible
? type alias, . _Variant_storage_
, true
false
. , true
, . trivially_destructible
, union Variant
' .
, , , , . type alias _Variant_storage
. :
template<typename... _Types, bool = conjunction_v<is_trivially_destructible<_Types>...> > class _Variant_storage_;
. , variadic template . , , , _Types
. C++17 , .
C++20 ,
,
requires clause. C++20 requires clause:
template<typename... _Types> class _Variant_storage_; template<typename _First, typename... _Rest> requires(TriviallyDestructible<_First> && ... && TriviallyDestuctible<_Rest>) class _Variant_storage_<_First, _Rest...> { union { remove_const_t<_First> _Head; _Variant_storage_<_Rest...> _Tail }; };
_Variant_storage_
, TriviallyDestructible
. , requires clause , , .
requires clause template type alias
, requires clause template type alias. C++20 - enable_if
, :
template<bool condition, typename T = void> requires condition using enable_if_t = T;
,
, . :
, enable_if
. ? f()
: enable_if
, , 239, , , , 239. , :
- , , template type alias', «void f(); void f();
- , SFINAE, , , .
, enable_if
, , size < 239
, size > 239
. , . , f()
. requires clause. — , .
— , . C++ Russia 2019 Piter, «: core language» . , , : reachable entity visible, ADL, entities internal linkage . , C++ Russia (JetBrains) « ++20 — ?»