Konzepte: Vereinfachung der Implementierung von STD Utility-Klassen


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.


 //  `concept`    ? #define concept constexpr bool 

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:


 // bool expression evaluated in compile-time static_assert(Assignable<float, int>); template<typename T> void test() noexcept(NothrowDefaultConstructible<T>) { T t; ... } 

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:


 //  type-parameter-key (class, typename) template<Trivial T> class simple_optional { T value; bool initialized = false; ... }; 

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:


 //   class any { struct SB { static constexpr size_t Size = ...; static constexpr size_t Alignment = ...; aligned_storage_t<Size, Alignment> storage; }; union { SB sb; void* handle; }; template<Small<SB::Size, SB::Alignment> T> any(T const & t) : sb(...) ... }; 

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:


 // Terse syntax (  auto) class any { struct SB { static constexpr size_t Size = ...; static constexpr size_t Alignment = ...; aligned_storage_t<Size, Alignment> storage; }; union { SB sb; void* handle; }; any(Small<SB::Size, SB::Alignment> auto const & t) : sb(...) ... }; 

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:


 //Terse syntax (  auto) template<typename T> concept LEPtr = Small<T, sizeof(void *), alignof(void *)>; template<typename T> auto get_handle(T& object) { if constexpr (LEPtr<T>) return object; else return &object; } 

In C ++ 20 kann davor geschrieben werden, dass es nicht nur auto , sondern auch auto :


 // Terse syntax (  auto) template<typename T> concept LEPtr = Small<T, sizeof(void *), alignof(void *)>; template<typename T> LEPtr auto get_handle(T &object) { if constexpr (LEPtr<T>) return object; else return &object; } 

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:


 // concepts,    ++20 template<typename Iterator> concept RandomAccessIterator = BidirectionalIterator<Iterator> && ...; template<typename R> concept Sortable = RandomAccessIterator<Iterator<R>> && Comparable<ValueType<R>>; template<Sortable Range> void sort(Range &) {...} 

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:


 //concepts,    ++20,  struct X {}; void test() { vector<int> vi; sort(vi); // OK list <int> li; sort(li); // Fail, list<int>::iterator is not random access vector< X > vx; sort(vx); // Fail, X is not Comparable } 

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?


 //concepts,    ++17 #define concept constexpr bool template<typename T> concept Comparable = is_convertible_v< decltype(declval<T const &>() < declval<T const &>()), bool >; template<typename Iterator> concept RandomAccessIterator = BidirectionalIterator<Iterator> && ...; template<typename R> concept Sortable = RandomAccessIterator<Iterator<R>> && Comparable<ValueType<R>>; template<typename Range, typename = enable_if_t<Sortable<Range>>> void sort(Range &) { ... } 

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:


 // requires-clause template<typename R> concept Sortable = RandomAccessIterator<Iterator<R>> && Comparable<ValueType<R>>; template<Sortable Range> void sort(Range &) { ... } 

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 &) { ... } 

Zusammenfassende Liste der konzeptbezogenen Funktionen


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>>); // Fail 

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>); // OK 

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>>); // OK struct X { struct Inner { int size_t; }; Inner* size() const; }; static_assert(Sizable<X>); // Fail 

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; // must fail pair<int, A> a1; { 1, 2 }; // must be OK 

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>, // not dependent is_default_constructible<S> >>> pair() : f(), s() {} }; 

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 = {}; // candidate template ignored: requirement 'is_implicity_default_constructible<B>::value' was not satisfied with... 

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>>; // compilation error 

, 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 ' . , , . optionalaligned_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; // #1 optional(optional const &) requires(TriviallyCopyConstructible<T>) = default; // #2 optional(optional const &) noexcept(NothrowCopyConstructible<T>) {...} // #3 ... ~optional() requires(TriviallyDestructible<T>) = default; ~optional() noexcept(NothroeDestructible<T>) {...} }; 

, . -, , 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; 

,


, . :


 // Equivalent, but functionally not equivalent template<typename T> enable_if_t<(sizeof(T) < 239)> f(); template<typename T> enable_if_t<(sizeof(T) > 239)> f(); // Not equivalent template<typename T> requires(sizeof(T) < 239) void f(); template<typename T> requires(sizeof(T) > 239) void f(); 

, 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 — ?»

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


All Articles