Schließen Sie ADL-Kontakte


Wie schreibe ich deinen Namen für immer in die Geschichte? Der erste, der zum Mond fliegt? Der erste, der einen fremden Geist trifft? Wir haben einen einfacheren Weg - Sie können sich in den C ++ - Sprachstandard einfügen.


Eric Nibler, Autor von C ++ Ranges, liefert ein gutes Beispiel. „Denk dran. Der 19. Februar 2019 ist der Tag, an dem der Begriff „Nibloid“ beim WG21-Treffen zum ersten Mal gesprochen wurde “, schrieb er auf Twitter.


Wenn Sie zu CppReference gehen, finden Sie dort im Abschnitt cpp / algorithm / Bereichscpp / Algorithmus / Bereiche viele Referenzen (niebloid). Zu diesem Zweck wurde sogar eine separate dsc_niebloid- Wiki-Vorlage erstellt.


Leider habe ich keinen offiziellen vollständigen Artikel zu diesem Thema gefunden und beschlossen, meinen eigenen zu schreiben. Dies ist eine kleine, aber faszinierende Reise in die Abgründe der architektonischen Astronautik, auf der wir in den Abgrund des ADL-Wahnsinns eintauchen und Nibloide kennenlernen können.


Wichtig: Ich bin kein echter Schweißer, sondern ein Javist, der manchmal Fehler im C ++ - Code nach Bedarf korrigiert. Wenn Sie sich etwas Zeit nehmen, um Denkfehler zu finden, wäre das schön. "Hilf Dasha, dem Reisenden, etwas Vernünftiges zu sammeln."


Nachschlagen


Zuerst müssen Sie sich für die Bedingungen entscheiden. Dies sind bekannte Dinge, aber „das Explizite ist besser als das Implizite“, daher werden wir sie separat diskutieren. Ich verwende keine echte russischsprachige Terminologie, sondern Englisch. Dies ist notwendig, da sogar das Wort "Einschränkung" im Kontext dieses Artikels mit mindestens drei englischen Versionen verknüpft werden kann, deren Unterschied für das Verständnis wichtig ist.


In C ++ gibt es beispielsweise das Konzept einer Namenssuche oder mit anderen Worten eine Suche: Wenn ein Name in einem Programm gefunden wird, wird er während der Kompilierung mit seiner Deklaration kompiliert.


Eine Suche kann qualifiziert werden (wenn der Name rechts vom Berechtigungsoperator des Bereichs steht :: :) und in anderen Fällen nicht qualifiziert sein. Wenn die Suche qualifiziert ist, umgehen wir die entsprechenden Mitglieder der Klasse, des Namespace oder der Aufzählung. Man könnte dies die "vollständige" Version des Datensatzes nennen (wie es in der Übersetzung von Straustrup zu tun scheint), aber es ist besser, die ursprüngliche Schreibweise zu belassen, da dies auf eine ganz bestimmte Art von Vollständigkeit verweist.


ADL


Wenn die Suche nicht qualifiziert ist, müssen wir genau wissen, wo nach dem Namen gesucht werden muss. Und hier ist eine Besonderheit namens ADL enthalten: argumentabhängige Suche oder auch die Suche nach Koenig (derjenige, der den Begriff „Anti-Pattern“ geprägt hat, der im Lichte des folgenden Textes etwas symbolisch ist). Nicolai Josuttis beschreibt es in seinem Buch „Die C ++ - Standardbibliothek: Ein Tutorial und eine Referenz“ wie folgt: „Der Punkt ist, dass Sie den Namespace der Funktion nicht qualifizieren müssen, wenn mindestens einer der Argumenttypen im Namespace dieser Funktion definiert ist.“


Wie soll es aussehen?


 #include <iostream> int main() { //  . //   , operator<<    ,  ADL , //    std    std::operator<<(std::ostream&, const char*) std::cout << "Test\n"; //    .      -     . operator<<(std::cout, "Test\n"); // same, using function call notation //    : // Error: 'endl' is not declared in this namespace. //      endl(),  ADL  . std::cout << endl; //  . //    ,       ADL. //     std,   endl      std. endl(std::cout); //    : // Error: 'endl' is not declared in this namespace. //  ,  - (endl) -     . (endl)(std::cout); } 

Komm mit ADL zur Hölle


Es würde einfach erscheinen. Oder nicht? Erstens arbeitet ADL je nach Art des Arguments auf neun verschiedene Arten , um mit einem Besen zu töten.


Stellen Sie sich zweitens rein praktisch vor, wir hätten eine Art Swap-Funktion. Es stellt sich heraus, dass std::swap(obj1,obj2); und using std::swap; swap(obj1, obj2); using std::swap; swap(obj1, obj2); kann sich ganz anders verhalten. Wenn ADL aktiviert ist, wird aus mehreren verschiedenen Swaps der gewünschte bereits anhand der Namespaces der Argumente ausgewählt! Je nach Sichtweise kann diese Redewendung sowohl als positives als auch als negatives Beispiel angesehen werden :-)


Wenn es Ihnen nicht ausreicht, können Sie das Brennholz in den Ofen des Hutes fallen lassen. Dies wurde kürzlich von Arthur O'Dwyer gut geschrieben . Ich hoffe, er bestraft mich nicht dafür, dass ich sein Beispiel benutze.


Stellen Sie sich vor, Sie haben ein Programm dieser Art:


 #include <stdio.h> namespace A { struct A {}; void call(void (*f)()) { f(); } } void f() { puts("Hello world"); } int main() { call(f); } 

Natürlich wird es nicht mit einem Fehler kompiliert:


 error: use of undeclared identifier 'call'; did you mean 'A::call'? call(f); ^~~~ A::call 

Wenn Sie dort jedoch eine völlig unbenutzte Überladung der Funktion f hinzufügen, funktioniert alles!


 #include <stdio.h> namespace A { struct A {}; void call(void (*f)()) { f(); } } void f() { puts("Hello world"); } void f(A::A); // UNUSED int main() { call(f); } 

In Visual Studio wird es immer noch kaputt gehen, aber das ist ihr Schicksal, das nicht funktioniert.


Wie kam es dazu? Lassen Sie uns in den Standard eintauchen (ohne Übersetzung, da eine solche Übersetzung ein außergewöhnlich monströses Durcheinander von Schlagworten wäre):


Wenn das Argument der Name oder die Adresse eines Satzes überladener Funktionen und / oder Funktionsvorlagen ist, sind seine zugeordneten Entitäten und Namespaces die Vereinigung derjenigen, die jedem der Mitglieder des Satzes zugeordnet sind, d. H. Die Entitäten und Namespaces, die seinem Parameter zugeordnet sind Typen und Rückgabetyp. [...] Wenn der oben genannte Satz überladener Funktionen mit einer Vorlagen-ID benannt wird, enthalten die zugehörigen Entitäten und Namespaces auch diejenigen des Typs Vorlagenargumente und der Vorlagenvorlagenargumente.

Nehmen Sie nun einen Code wie folgt:


 #include <stdio.h> namespace B { struct B {}; void call(void (*f)()) { f(); } } template<class T> void f() { puts("Hello world"); } int main() { call(f<B::B>); } 

In beiden Fällen werden Argumente erhalten, die keinen Typ haben. f und f<B::B> sind die Namen der Mengen überladener Funktionen (aus der obigen Definition), und eine solche Menge hat keinen Typ. Um eine Überlastung in eine einzelne Funktion zu reduzieren, müssen Sie wissen, welcher Typ von Funktionszeiger für die beste Aufrufüberlastung am besten geeignet ist. Sie müssen also eine Reihe von Kandidaten für einen call sammeln, was bedeutet, dass Sie einen Suchanruf ausführen müssen. Und dafür startet ADL!


Aber normalerweise sollten wir für ADL die Arten von Argumenten kennen! Und hier brechen Clang, ICC und MSVC fälschlicherweise wie folgt (GCC jedoch nicht):


 [build] ..\..\main.cpp(15,5): error: use of undeclared identifier 'call'; did you mean 'B::call'? [build] call(f<B::B>); [build] ^~~~ [build] B::call [build] ..\..\main.cpp(4,10): note: 'B::call' declared here [build] void call(void (*f)()) { [build] ^ 

Sogar die Entwickler von Compilern mit ADL haben eine etwas angespannte Beziehung.


Scheint ADL immer noch eine gute Idee zu sein? Einerseits müssen wir einen solchen sklavischen Code nicht mehr höflich schreiben:


 std::cout << "Hello, World!" << std::endl; std::operator<<(std::operator<<(std::cout, "Hello, World!"), "\n"); 

Auf der anderen Seite haben wir der Kürze halber die Tatsache gehandelt, dass es jetzt ein System gibt, das auf völlig unmenschliche Weise funktioniert. Eine tragische und majestätische Geschichte darüber, wie die Leichtigkeit, Halloworld zu schreiben, die gesamte Sprache über Jahrzehnte hinweg beeinflussen kann.


Bereiche und Konzepte


Wenn Sie die Beschreibung der Nibler Rangers-Bibliothek öffnen, werden Sie bereits vor der Erwähnung von Nibloiden auf viele andere Marker stoßen, die als (Konzept) bezeichnet werden . Dies ist bereits ein hübsches Zeug, aber nur für den Fall (für Alte und Javisten) werde ich Sie daran erinnern, was es ist .


Konzepte werden als benannte Sätze von Einschränkungen bezeichnet, die für Vorlagenargumente gelten, um die besten Funktionsüberladungen und die am besten geeigneten Vorlagenspezialisierungen auszuwählen.


 template <typename T> concept bool HasStringFunc = requires(T a) { { to_string(a) } -> string; }; void print(HasStringFunc a) { cout << to_string(a) << endl; } 

Hier haben wir eine Einschränkung auferlegt, dass das Argument eine to_string Funktion haben muss, die einen to_string zurückgibt. Wenn wir versuchen, ein Spiel in den print , das nicht unter die Einschränkungen fällt, wird dieser Code einfach nicht kompiliert.


Dies vereinfacht den Code erheblich. Sehen Sie sich beispielsweise an, wie Nibler in range -v3 sortiert hat , was in C ++ vom 14.11.17 funktioniert. Es gibt einen wunderbaren Code wie diesen:


 #define CONCEPT_PP_CAT_(X, Y) X ## Y #define CONCEPT_PP_CAT(X, Y) CONCEPT_PP_CAT_(X, Y) /// \addtogroup group-concepts /// @{ #define CONCEPT_REQUIRES_(...) \ int CONCEPT_PP_CAT(_concept_requires_, __LINE__) = 42, \ typename std::enable_if< \ (CONCEPT_PP_CAT(_concept_requires_, __LINE__) == 43) || (__VA_ARGS__), \ int \ >::type = 0 \ /**/ 

Damit Sie später Folgendes tun können:


 struct Sortable_ { template<typename Rng, typename C = ordered_less, typename P = ident, typename I = iterator_t<Rng>> auto requires_() -> decltype( concepts::valid_expr( concepts::model_of<concepts::ForwardRange, Rng>(), concepts::is_true(ranges::Sortable<I, C, P>()) )); }; using Sortable = concepts::models<Sortable_, Rng, C, P>; template<typename Rng, typename C = ordered_less, typename P = ident, CONCEPT_REQUIRES_(!Sortable<Rng, C, P>())> void operator()(Rng &&, C && = C{}, P && = P{}) const { ... 

Ich hoffe, Sie wollten das alles schon sehen und nur vorbereitete Konzepte in einem neuen Compiler verwenden.


Anpassungspunkte


Das nächste interessante Element im Standard ist customization.point.object . Sie werden aktiv in der Nibler Ranges-Bibliothek verwendet.


Der Anpassungspunkt ist eine Funktion, die von der Standardbibliothek verwendet wird, damit er für Benutzertypen im Namespace des Benutzers überladen werden kann. Diese Überladungen können mithilfe von ADL gefunden werden.


Anpassungspunkte werden unter cust der folgenden Architekturprinzipien cust ( cust ist der Name für einen imaginären Anpassungspunkt):


  • Der Code, der cust entweder in der qualifizierten Form std::cust(a) oder in nicht qualifizierter Form geschrieben: using std::cust; cust(a); using std::cust; cust(a); . Beide Einträge müssen sich identisch verhalten. Insbesondere müssen sie Benutzerüberladungen im Namespace finden, der den Argumenten zugeordnet ist.
  • Code, der cust in Form eines std::cust; cust(a); std::cust; cust(a); sollte nicht in der Lage sein, die Beschränkungen für std::cust zu umgehen.
  • Benutzerdefinierte Punktaufrufe sollten auf jedem relativ modernen Compiler effizient und optimal funktionieren.
  • Die Entscheidung sollte keine neuen Verstöße gegen die Single Definition Rule (ODR) hervorrufen .

Um zu verstehen, was es ist, können Sie sich den N4381 ansehen . Auf den ersten Blick scheinen sie eine Möglichkeit zu sein, eigene Versionen von begin , swap , data und dergleichen zu schreiben, und die Standardbibliothek nimmt sie mit ADL auf.


Die Frage ist, wie sich dies von der alten Praxis unterscheidet, wenn der Benutzer eine Überladung für einige begin für seinen eigenen Typ und Namespace beginnen. Und warum sind sie überhaupt Objekte?


Tatsächlich sind dies Instanzen von Funktionsobjekten im std . Ihr Zweck besteht darin, zuerst Typprüfungen (als Konzepte konzipiert) für alle Argumente in einer Reihe durchzuführen und dann den Aufruf an die richtige Funktion im std oder ihn in ADL zum Verkauf anzubieten.


In der Tat ist dies nicht die Art von Dingen, die Sie in einem regulären Nicht-Bibliotheksprogramm verwenden würden. Dies ist eine Funktion der Standardbibliothek, mit der Sie an zukünftigen Erweiterungspunkten eine Konzeptprüfung hinzufügen können, die wiederum dazu führt, dass schönere und verständlichere Fehler angezeigt werden, wenn Sie etwas in den Vorlagen durcheinander gebracht haben.


Der derzeitige Ansatz für Anpassungspunkte weist einige Probleme auf. Erstens ist es sehr einfach, alles zu zerbrechen. Stellen Sie sich diesen Code vor:


 template<class T> void f(T& t1, T& t2) { using std::swap; swap(t1, t2); } 

Wenn wir versehentlich einen qualifizierten Aufruf von std::swap(t1, t2) tätigen std::swap(t1, t2) wird unsere eigene Version von swap niemals gestartet, egal was wir dort ablegen. Noch wichtiger ist jedoch, dass es keine Möglichkeit gibt, Konzeptprüfungen zentral an solche benutzerdefinierten Funktionsimplementierungen anzuhängen. In N4381 schreiben sie:


„Stellen Sie sich vor, dass std::begin eines Tages in Zukunft erfordern wird, dass sein Argument als Range Konzept modelliert wird. Das Hinzufügen einer solchen Einschränkung hat einfach keine Auswirkung auf den Code, der idiomatisch mit std::begin :


 using std::begin; begin(a); 

Wenn der Startaufruf an die vom Benutzer erstellte überladene Version gesendet wird, werden die Einschränkungen für std::begin einfach ignoriert. “


Die im Propozal beschriebene Lösung löst beide Probleme. Dazu verwenden wir den Ansatz dieser spekulativen Implementierung von std::begin (Sie können sich Godbolt ansehen ):


 #include <utility> namespace my_std { namespace detail { struct begin_fn { /*   ,         begin(arg)  arg.begin().  -   . */ template <class T> auto operator()(T&& arg) const { return impl(arg, 1L); } template <class T> auto impl(T&& arg, int) const requires requires { begin(std::declval<T>()); } { return begin(arg); } // ADL template <class T> auto impl(T&& arg, long) const requires requires { std::declval<T>().begin(); } { return arg.begin(); } // ... }; } //        inline constexpr detail::begin_fn begin{}; } 

Ein qualifizierter Aufruf von my_std::begin(someObject) geht immer über my_std::detail::begin_fn - und das ist gut so. Was passiert mit einem unqualifizierten Anruf? Lesen wir noch einmal unsere Zeitung:


„Wenn begin unmittelbar nach dem Erscheinen von my_std::begin innerhalb des Bereichs ohne my_std::begin , ändert sich die Situation etwas. In der ersten Phase der Suche wird der Name begin in das globale Objekt my_std::begin . Da die Suche ein Objekt und keine Funktion gefunden hat, wird die zweite Phase der Suche nicht ausgeführt. Mit anderen Worten, wenn my_std::begin ein Objekt ist, dann verwenden Sie die Konstruktion my_std::detail::begin_fn begin; begin(a); my_std::detail::begin_fn begin; begin(a); einfach äquivalent zu std::begin(a); "Und wie wir gesehen haben, startet dies benutzerdefinierte ADL."


Aus diesem Grund kann die Konzeptüberprüfung in einem Funktionsobjekt im std bevor ADL die vom Benutzer bereitgestellte Funktion aufruft. Es gibt keine Möglichkeit, dieses Verhalten auszutricksen.


Wie werden Anpassungspunkte angepasst?


Tatsächlich ist „Anpassungspunktobjekt“ (CPO) kein guter Name. Aus dem Namen ist nicht ersichtlich, wie sie sich ausdehnen, welche Mechanismen sich unter der Haube befinden, welche Funktionen sie bevorzugen ...


Was uns zum Begriff "nibloid" führt. Ein Nibloid ist ein solcher CPO, der die Funktion X aufruft, wenn sie in der Klasse definiert ist, andernfalls die Funktion X aufruft, wenn eine geeignete freie Funktion vorhanden ist, andernfalls versucht er, einen Fallback der Funktion X auszuführen.


So versuchen beispielsweise die nibloiden ranges::swap beim Aufrufen von ranges::swap(a, b) zuerst, a.swap(b) . Wenn es keine solche Methode gibt, wird versucht, swap(a, b) mit ADL aufzurufen. Wenn dies nicht funktioniert, versuchen Sie auto tmp = std::move(a); a = std::move(b); b = std::move(tmp) auto tmp = std::move(a); a = std::move(b); b = std::move(tmp) auto tmp = std::move(a); a = std::move(b); b = std::move(tmp) .


Zusammenfassung


Wie Matt auf Twitter scherzte , schlug Dave aus Gründen der Konsistenz einmal vor, funktionale Objekte mit ADL "funktionieren" zu lassen, genau wie reguläre Funktionen. Die Ironie ist, dass ihre Fähigkeit, ADL zu deaktivieren und für ihn unsichtbar zu sein, nun zu ihren Hauptvorteilen geworden ist.


Dieser gesamte Artikel war eine Vorbereitung dafür.


" Ich habe einfach alles verstanden, das ist alles. Wirst du zuhören ?


Haben Sie jemals etwas angeschaut, und es schien verrückt und dann in einem anderen Licht auf
verrückte Dinge, die sie normal sehen?



Fürchte dich nicht. Fürchte dich nicht. Ich fühle mich im Herzen so gut. Alles wird gut. Ich habe mich seit vielen Jahren nicht mehr so ​​gut gefühlt. Alles wird gut.



Minute der Werbung. Bereits in dieser Woche , vom 19. bis 20. April, findet die C ++ Russia 2019 statt - eine Konferenz mit Hardcore-Präsentationen sowohl zur Sprache selbst als auch zu praktischen Themen wie Multithreading und Performance. Die Konferenz wird übrigens von Nicolai Josuttis eröffnet, dem Autor der im Artikel erwähnten C ++ - Standardbibliothek: Ein Tutorial und eine Referenz. Sie können sich mit dem Programm vertraut machen und Tickets auf der offiziellen Website kaufen. Es bleibt nur noch sehr wenig Zeit, dies ist die letzte Chance.

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


All Articles