Erster Eindruck von Konzepten



Ich habe mich für die neuen C ++ 20-Feature-Konzepte entschieden.

Konzepte (oder Konzepte , wie das russischsprachige Wiki schreibt) sind eine sehr interessante und nützliche Funktion, die seit langem fehlt.

Im Wesentlichen werden Vorlagenargumente eingegeben.

Das Hauptproblem von Vorlagen vor C ++ 20 besteht darin, dass Sie alles in ihnen ersetzen können, einschließlich etwas, für das sie überhaupt nicht entwickelt wurden. Das heißt, das Vorlagensystem war vollständig untypisiert. Infolgedessen traten unglaublich lange und völlig unlesbare Fehlermeldungen auf, wenn der falsche Parameter an die Vorlage übergeben wurde. Sie haben versucht, dies mit Hilfe verschiedener Sprach-Hacks zu bekämpfen, die ich nicht einmal erwähnen möchte (obwohl ich mich damit befassen musste).

Konzepte sollen dieses Missverständnis korrigieren. Sie fügen den Vorlagen ein Schreibsystem hinzu, das sehr leistungsfähig ist. Nachdem ich die Funktionen dieses Systems verstanden hatte, begann ich, die verfügbaren Materialien im Internet zu studieren.

Ehrlich gesagt bin ich ein wenig schockiert :) C ++ ist eine bereits komplizierte Sprache, aber zumindest gibt es eine Entschuldigung: Es ist passiert. Beim Entwerfen einer Sprache wurde die Metaprogrammierung von Vorlagen entdeckt und nicht festgelegt. Und dann, als sie die nächsten Versionen der Sprache entwickelten, mussten sie sich an diese „Entdeckung“ anpassen, da in der Welt viel Code geschrieben wurde. Konzepte sind eine grundlegend neue Chance. Und es scheint mir, dass ihre Umsetzung bereits eine gewisse Undurchsichtigkeit aufweist. Vielleicht ist dies eine Folge der Notwendigkeit, die enorme Menge an vererbten Fähigkeiten zu berücksichtigen? Versuchen wir es herauszufinden ...

Allgemeine Information


Ein Konzept ist eine neue Sprachentität, die auf der Vorlagensyntax basiert. Ein Konzept hat einen Namen, Parameter und einen Körper - ein Prädikat, das abhängig von den Parametern des Konzepts einen konstanten (d. H. In der Kompilierungsphase berechneten) logischen Wert zurückgibt. So:

template<int I> 
concept Even = I % 2 == 0;  

template<typename T>
concept FourByte = sizeof(T)==4;

Technisch gesehen sind Konzepte Template-Constexpr-Ausdrücken wie bool sehr ähnlich:

template<int I>
constexpr bool EvenX = I % 2 == 0; 

template<typename T>
constexpr bool FourByteX = sizeof(T)==4;

Sie können Konzepte sogar in allgemeinen Ausdrücken verwenden:

bool b1 = Even<2>; 

Verwenden von


Die Hauptidee der Konzepte besteht darin, dass sie anstelle des Typnamens oder der Klassenschlüsselwörter in Vorlagen verwendet werden können. Wie Metatypen ("Typen für Typen"). Somit wird eine statische Typisierung in die Vorlagen eingeführt.

template<FourByte T>
void foo(T const & t) {}

Wenn wir nun int als Vorlagenparameter verwenden, wird der Code in den allermeisten Fällen kompiliert. und wenn doppelt, wird eine kurze und verständliche Fehlermeldung ausgegeben. Einfache und übersichtliche Eingabe von Vorlagen, bisher ist alles in Ordnung.

erfordert


Dies ist ein neues "kontextuelles" C ++ 20-Schlüsselwort mit einem doppelten Zweck: erfordert Klausel und erfordert Ausdruck. Wie später gezeigt wird, führt diese seltsame Keyword-Einsparung zu Verwirrung.

erfordert Ausdruck


Betrachten Sie zunächst den Ausdruck. Die Idee ist ziemlich gut: Dieses Wort hat einen Block in geschweiften Klammern, dessen Code zur Kompilierung ausgewertet wird. Der dortige Code sollte zwar nicht in C ++ geschrieben sein, sondern in einer speziellen Sprache, die C ++ nahe kommt, aber seine eigenen Eigenschaften hat (dies ist die erste Kuriosität, es war durchaus möglich, nur C ++ - Code zu erstellen).

Wenn der Code korrekt ist - erfordert Ausdruck, dass true zurückgegeben wird, andernfalls false. Der Code selbst wird natürlich niemals in die Codegenerierung einbezogen, ähnlich wie Ausdrücke in Größe oder Dekltyp.

Leider ist das Wort kontextbezogen und funktioniert nur innerhalb von Vorlagen, dh außerhalb der Vorlage wird dies nicht kompiliert:

bool b = requires { 3.14 >> 1; };

Und in der Vorlage - bitte:

template<typename T>
constexpr bool Shiftable = requires(T i) { i>>1; };

Und es wird funktionieren:

bool b1 = Shiftable<int>; // true
bool b2 = Shiftable<double>; // false

Die Hauptverwendung von erfordert Ausdruck ist die Erstellung von Konzepten. Auf diese Weise können Sie beispielsweise das Vorhandensein von Feldern und Methoden in einem Typ überprüfen. Ein sehr beliebter Fall.

template <typename T>
concept Machine = 
  requires(T m) {  //   `m` ,   Machine
	m.start();     //    `m.start()` 
	m.stop();      //   `m.stop()`
};  

Übrigens müssen alle Variablen, die im getesteten Code erforderlich sein können (nicht nur die Vorlagenparameter), in Klammern deklariert werden. Aus irgendeinem Grund ist das Deklarieren einer Variablen einfach nicht möglich.

Typprüfung im Inneren erfordert


Hier beginnen die Unterschiede zwischen dem erforderlichen Code und dem Standard-C ++. Um die zurückgegebenen Typen zu überprüfen, wird eine spezielle Syntax verwendet: Das Objekt wird in geschweiften Klammern gesetzt, ein Pfeil wird platziert und danach wird ein Konzept geschrieben, das der Typ erfüllen muss. Darüber hinaus ist die Verwendung von direkten Typen nicht zulässig.

Überprüfen Sie, ob die Rückgabe der Funktion in int konvertiert werden kann:

requires(T v, int i) {
  { v.f(i) } -> std::convertible_to<int>;
}  

Überprüfen Sie, ob die Rückgabefunktion genau int ist:

requires(T v, int i) {
  { v.f(i) } -> std::same_as<int>; 
}  

(std :: same_as und std :: convertible_to sind Konzepte aus der Standardbibliothek).

Wenn Sie keinen Ausdruck einschließen, dessen Typ in geschweiften Klammern markiert ist, versteht der Compiler nicht, was er von ihm will, und interpretiert die gesamte Zeichenfolge als einen einzelnen Ausdruck, der für die Kompilierung überprüft werden muss.

erfordert innen erfordert


Das Schlüsselwort require hat eine besondere Bedeutung. Verschachtelte Anforderungsausdrücke (bereits ohne geschweifte Klammern) werden nicht zur Kompilierung, sondern auf Gleichheit wahr oder falsch geprüft. Wenn sich ein solcher Ausdruck als falsch herausstellt, stellt sich der einschließende Ausdruck sofort als falsch heraus (und die weitere Kompilierungsanalyse wird unterbrochen). Generelle Form:

requires { 
  expression;         // expression is valid
  requires predicate; // predicate is true
};

Als Prädikat können beispielsweise zuvor definierte Konzepte oder Typmerkmale verwendet werden. Beispiel:

requires(Iter it) {
  //     (   Iter   *  ++)
  *it++;
 
  //    -  
  requires std::convertible_to<decltype(*it++), typename Iter::value_type>;
 
  //    -  
  requires std::is_convertible_v<decltype(*it++), typename Iter::value_type>;
}

Gleichzeitig sind verschachtelte Anforderungsausdrücke mit Code in geschweiften Klammern zulässig, der auf Gültigkeit überprüft wird. Wenn Sie jedoch einfach einen erforderlichen Ausdruck in einen anderen schreiben, wird der verschachtelte Ausdruck (alles als Ganzes, einschließlich des verschachtelten Schlüsselworts require) einfach auf Gültigkeit überprüft:

requires (T v) { 
  requires (typename T::value_type x) { ++x; }; //     , 
												//     !
};  

Daher entstand eine seltsame Form mit Doppelanforderungen:

requires (T v) { 
  requires requires (typename T::value_type x) { ++x; }; //       "++x"
};  

Hier ist so eine lustige Fluchtsequenz von "erfordert".

Eine andere Kombination von zwei erfordert übrigens diese Zeitklausel (siehe unten) und den Ausdruck:

template <typename T>
  requires requires(T x, T y) { bool(x < y); }
bool equivalent(T const& x, T const& y)
{
  return !(x < y) && !(y < x);
};

erfordert Klausel


Kommen wir nun zu einer anderen Verwendung des Wortes, die erforderlich ist - um Einschränkungen eines Vorlagentyps zu deklarieren. Dies ist eine Alternative zur Verwendung von Konzeptnamen anstelle von Typnamen. Im folgenden Beispiel sind alle drei Methoden gleichwertig:

//  require
template<typename Cont>
	requires Sortable<Cont>
void sort(Cont& container);

//   require (  )
template<typename Cont>
void sort(Cont& container) requires Sortable<Cont>;

//    typename
template<Sortable Cont>
void sort(Cont& container)  

Die erfordernde Deklaration kann mehrere Prädikate verwenden, die von logischen Operatoren kombiniert werden.

template <typename T>
  requires is_standard_layout_v<T> && is_trivial_v<T>
void fun(T v); 
 
int main()
{
  std::string s;
 
  fun(1);  // ok
  fun(s);  // compiler error
}

Invertieren Sie jedoch einfach eine der Bedingungen, da ein Kompilierungsfehler auftritt:

template <typename T>
  requires is_standard_layout_v<T> && !is_trivial_v<T>
void fun(T v); 

Hier ist ein Beispiel, das auch nicht kompiliert wird

template <typename T>
  requires !is_trivial_v<T>
void fun(T v);	

Der Grund dafür sind die Mehrdeutigkeiten, die beim Parsen einiger Ausdrücke auftreten. Zum Beispiel in einer solchen Vorlage:

template <typename T> 
  requires (bool)&T::operator short unsigned int foo();

Es ist unklar, wem wir den Operator oder den Prototyp der Funktion foo () ohne Vorzeichen zuordnen sollen. Daher entschieden die Entwickler, dass ohne Klammern, da Argumente eine Klausel erfordern, nur eine sehr begrenzte Menge von Entitäten verwendet werden kann - wahre oder falsche Literale, Feldnamen vom Typ bool des Formularwerts, Wert, T :: Wert, ns :: Trait :: Wert, Konzeptnamen vom Typ Concept und erfordern Ausdrücke. Alles andere sollte in Klammern stehen:

template <typename T>
  requires (!is_trivial_v<T>)
void fun(T v);

Nun zu Prädikat-Features in der require-Klausel


Betrachten Sie ein anderes Beispiel.

template <typename T>
  requires is_trivial_v<typename T::value_type> 
void fun(T v); 

In diesem Beispiel erfordert require ein Merkmal, das vom Typ des verschachtelten value_type abhängt. Es ist nicht im Voraus bekannt, ob ein beliebiger Typ einen solchen verschachtelten Typ hat, der an die Vorlage übergeben werden kann. Wenn Sie beispielsweise einen einfachen int-Typ an eine solche Vorlage übergeben, tritt ein Kompilierungsfehler auf. Wenn wir jedoch zwei Spezialisierungen der Vorlage haben, tritt kein Fehler auf. Es wird nur eine weitere Spezialisierung ausgewählt.

template <typename T>
  requires is_trivial_v<typename T::value_type> 
void fun(T v) { std::cout << "1"; } 
 
template <typename T>
void fun(T v) { std::cout << "2"; } 
 
int main()
{
  fun(1);  // displays: "2"
}

Daher wird die Spezialisierung nicht nur verworfen, wenn das Prädikat der require-Klausel false zurückgibt, sondern auch, wenn es sich als falsch herausstellt.

Die Klammern um das Prädikat sind eine wichtige Erinnerung daran, dass in der require-Klausel die Umkehrung des Prädikats nicht das Gegenteil des Prädikats selbst ist. Damit,

requires is_trivial_v<typename T::value_type> 

bedeutet, dass das Merkmal korrekt ist und true zurückgibt. Dabei

!is_trivial_v<typename T::value_type> 

würde bedeuten "das Merkmal ist korrekt und gibt falsch zurück" Die
wahre logische Umkehrung des ersten Prädikats ist NICHT ("das Merkmal ist korrekt und gibt wahr zurück") == "das Merkmal ist falsch oder gibt falsch zurück" - dies wird auf etwas komplexere Weise erreicht - durch eine explizite Definition des Konzepts:

template <typename T>
concept value_type_valid_and_trivial 
  = is_trivial_v<typename T::value_type>; 
 
template <typename T>
  requires (!value_type_valid_and_trivial<T>)
void fun(T v); 

Konjunktion und Disjunktion


Die logischen Konjunktions- und Disjunktionsoperatoren sehen wie gewohnt aus, funktionieren jedoch tatsächlich etwas anders als in normalem C ++.

Betrachten Sie zwei sehr ähnliche Codefragmente.

Das erste ist ein Prädikat ohne Klammern:

template <typename T, typename U>
  requires std::is_trivial_v<typename T::value_type>
		|| std::is_trivial_v<typename U::value_type>
void fun(T v, U u); 

Der zweite ist mit Klammern:

template <typename T, typename U>
  requires (std::is_trivial_v<typename T::value_type>
		 || std::is_trivial_v<typename U::value_type>)
void fun(T v, U u); 

Der Unterschied besteht nur in Klammern. Aus diesem Grund gibt es in der zweiten Vorlage nicht zwei Einschränkungen, die durch eine „Anforderungsklausel“ verbunden sind, sondern eine, die durch ein übliches logisches ODER verbunden ist.

Dieser Unterschied ist wie folgt. Betrachten Sie den Code

std::optional<int> oi {};
int i {};
fun(i, oi);

Hier wird die Vorlage durch die Typen int und std :: optional instanziiert.

Im ersten Fall ist der Typ int :: value_type ungültig und die erste Einschränkung ist damit nicht erfüllt.

Der Typ optional :: value_type ist jedoch gültig, das zweite Merkmal gibt true zurück, und da sich zwischen den Einschränkungen ein ODER-Operator befindet, ist das gesamte Prädikat insgesamt erfüllt.

Im zweiten Fall ist dies ein einzelner Ausdruck, der einen ungültigen Typ enthält, weshalb er im Allgemeinen ungültig ist und das Prädikat nicht erfüllt ist. Einfache Klammern ändern also unmerklich die Bedeutung des Geschehens.

Abschließend


Natürlich werden hier nicht alle Merkmale der Konzepte gezeigt. Ich bin einfach nicht weiter gegangen. Aber als erster Eindruck - eine sehr interessante Idee und eine etwas seltsame verwirrte Umsetzung. Und eine lustige Syntax mit Wiederholung erfordert, was wirklich verwirrt. Gibt es wirklich so wenige englische Wörter, dass Sie ein Wort für ganz andere Zwecke verwenden mussten?

Die Idee mit kompiliertem Code ist definitiv gut. Es ist sogar etwas ähnlich wie "Quasi-Quoting" in Syntaxmakros. Aber hat es sich gelohnt, die spezielle Syntax für die Überprüfung von Rückgabetypen zu verwechseln? IMHO, dafür wäre es einfach notwendig, ein separates Schlüsselwort zu erstellen.

Das implizite Mischen der Begriffe "wahr / falsch" und "kompiliert / kompiliert nicht" in einem Haufen, und infolgedessen sind auch Witze mit Klammern falsch. Dies sind unterschiedliche Konzepte, und sie müssen streng in unterschiedlichen Kontexten existieren (obwohl ich verstehe, woher sie stammen - aus der SFINAE-Regel, in der nicht kompilierter Code die Spezialisierung nur stillschweigend von der Betrachtung ausschloss). Aber wenn das Ziel der Konzepte darin besteht, den Code so explizit wie möglich zu gestalten, hat es sich gelohnt, all diese impliziten Dinge in neue Funktionen zu ziehen?

Der Artikel wurde hauptsächlich auf der Grundlage von
akrzemi1.wordpress.com/2020/01/29/requires-expression
akrzemi1.wordpress.com/2020/03/26/requires-clause
(es gibt viel mehr Beispiele und interessante Funktionen)
mit meinen Ergänzungen von geschrieben In anderen Quellen können
alle Beispiele überprüft werdenwandbox.org

All Articles