
Comment écrire son nom dans l'histoire pour toujours? Le premier à voler vers la lune? Le premier à rencontrer un esprit étranger? Nous avons un moyen plus simple - vous pouvez vous adapter au standard du langage C ++.
Eric Nibler, auteur de C ++ Ranges, en fournit un bon exemple. «Souviens-toi de ça. Le 19 février 2019 est le jour où le terme «nibloïde» a été prononcé pour la première fois lors de la réunion du WG21 », a-t-il écrit sur Twitter.
En effet, si vous allez sur CppReference, dans la section cpp / algorithme / rangescpp / algorithme / plages , vous y trouverez de nombreuses références (niebloid). Pour cela, un modèle wiki dsc_niebloid distinct a même été créé.
Malheureusement, je n'ai trouvé aucun article officiel complet sur ce sujet et j'ai décidé d'écrire le mien. Il s'agit d'un petit mais fascinant voyage dans les abîmes de l'astronautique architecturale, dans lequel nous pouvons plonger dans l'abîme de la folie de l'ADL et nous familiariser avec les nibloïdes.
Important: je ne suis pas un vrai soudeur, mais un javiste qui corrige parfois des erreurs dans le code C ++ si nécessaire. Si vous prenez un peu de temps pour trouver des erreurs de raisonnement, ce serait bien. "Aide Dasha le voyageur à collecter quelque chose de raisonnable."
Recherche
Vous devez d'abord décider des conditions. Ce sont des choses bien connues, mais «l'explicite vaut mieux que l'implicite», nous en discuterons donc séparément. Je n'utilise pas de véritable terminologie en russe, mais j'utilise plutôt l'anglais. Cela est nécessaire car même le mot «restriction» dans le contexte de cet article peut être associé à au moins trois versions anglaises, dont la différence est importante pour la compréhension.
Par exemple, en C ++, il y a le concept d'une recherche de nom ou, en d'autres termes, une recherche: lorsqu'un nom est trouvé dans un programme, il compile avec sa déclaration lors de la compilation.
Une recherche peut être qualifiée (si le nom est à droite de l'opérateur d'autorisation de la portée ::
:), et non qualifiée dans d'autres cas. Si la recherche est qualifiée, nous contournons les membres correspondants de la classe, de l'espace de noms ou de l'énumération. On pourrait appeler cela la version «complète» de l'enregistrement (comme cela semble être fait dans la traduction de Straustrup), mais il vaut mieux laisser l'orthographe originale, car cela fait référence à un type d'exhaustivité très spécifique.
ADL
Si la recherche n'est pas qualifiée, nous devons comprendre exactement où chercher le nom. Et ici, une fonctionnalité spéciale appelée ADL est incluse: la recherche dépendante de l'argument , ou bien - la recherche de Koenig (celui qui a inventé le terme «anti-modèle», qui est un peu symbolique à la lumière du texte suivant). Nicolai Josuttis dans son livre "The C ++ Standard Library: A Tutorial and Reference" le décrit comme suit: "Le fait est que vous n'avez pas besoin de qualifier l'espace de noms de la fonction si au moins un des types d'arguments est défini dans l'espace de noms de cette fonction."
À quoi devrait-il ressembler?
#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); }
Descendez en enfer avec ADL
Cela semble simple. Ou pas? Tout d'abord, selon le type d'argument, ADL fonctionne de neuf manières différentes , pour tuer avec un balai.
Deuxièmement, purement pratique, imaginez que nous ayons une sorte de fonction d'échange. Il s'avère que std::swap(obj1,obj2);
et en using std::swap; swap(obj1, obj2);
using std::swap; swap(obj1, obj2);
peut se comporter complètement différemment. Si ADL est activé, à partir de plusieurs swaps différents, celui dont vous avez besoin est déjà sélectionné en fonction des espaces de noms des arguments! Selon le point de vue, cet idiome peut être considéré à la fois comme un exemple positif et négatif :-)
S'il vous semble que cela ne suffit pas, vous pouvez déposer le bois de chauffage dans le four du chapeau. Ceci a été récemment bien écrit par Arthur O'Dwyer . J'espère qu'il ne me punira pas pour avoir utilisé son exemple.
Imaginez que vous ayez un programme de ce type:
#include <stdio.h> namespace A { struct A {}; void call(void (*f)()) { f(); } } void f() { puts("Hello world"); } int main() { call(f); }
Bien sûr, il ne compile pas avec une erreur:
error: use of undeclared identifier 'call'; did you mean 'A::call'? call(f); ^~~~ A::call
Mais si vous y ajoutez une surcharge complètement inutilisée de la fonction f
, alors tout fonctionnera!
#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); }
Sur Visual Studio, il va encore se casser, mais tel est son sort, ne fonctionne pas.
Comment est-ce arrivé? Plongeons-nous dans la norme (sans traduction, car une telle traduction serait un méli-mélo de mots à la mode exceptionnellement monstrueux):
Si l'argument est le nom ou l'adresse d'un ensemble de fonctions et / ou de modèles de fonctions surchargés, ses entités et espaces de noms associés sont l'union de ceux associés à chacun des membres de l'ensemble, c'est-à-dire les entités et espaces de noms associés à son paramètre types et type de retour. [...] De plus, si l'ensemble susmentionné de fonctions surchargées est nommé avec un template-id, ses entités et espaces de noms associés incluent également ceux de son type template-arguments et son template template-arguments.
Maintenant, prenez un code comme celui-ci:
#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>); }
Dans les deux cas, des arguments sont obtenus sans type. f
et f<B::B>
sont les noms des ensembles de fonctions surchargées (de la définition ci-dessus), et un tel ensemble n'a pas de type. Pour réduire une surcharge en une seule fonction, vous devez comprendre quel type de pointeur de fonction convient le mieux à la meilleure surcharge d' call
. Vous devez donc collecter un ensemble de candidats pour l' call
, ce qui signifie exécuter un call
recherche. Et pour cela, ADL va commencer!
Mais généralement pour ADL, nous devons connaître les types d'arguments! Et ici, Clang, ICC et MSVC se cassent par erreur comme suit (mais GCC ne le fait pas):
[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] ^
Même les créateurs de compilateurs avec ADL ont une relation un peu tendue.
Eh bien, ADL semble-t-il toujours être une bonne idée? D'une part, nous n'avons plus besoin d'écrire un tel code servile de manière polie:
std::cout << "Hello, World!" << std::endl; std::operator<<(std::operator<<(std::cout, "Hello, World!"), "\n");
D'un autre côté, nous avons échangé par souci de brièveté le fait qu'il existe maintenant un système qui fonctionne de manière complètement inhumaine. Une histoire tragique et majestueuse sur la façon dont la facilité d'écrire Halloworld peut affecter la langue entière sur une échelle de décennies.
Gammes et concepts
Si vous ouvrez la description de la bibliothèque Nibler Rangers , même avant la mention des nibloïdes, vous tomberez sur de nombreux autres marqueurs appelés (concept) . C'est déjà un joli truc, mais juste au cas où (pour les vieux et les javistes) je vais vous rappeler ce que c'est .
Les concepts sont appelés ensembles nommés de contraintes qui s'appliquent aux arguments de modèle pour sélectionner les meilleures surcharges de fonction et les spécialisations de modèle les plus appropriées.
template <typename T> concept bool HasStringFunc = requires(T a) { { to_string(a) } -> string; }; void print(HasStringFunc a) { cout << to_string(a) << endl; }
Ici, nous avons imposé une restriction selon laquelle l'argument doit avoir une fonction to_string
qui renvoie une chaîne. Si nous essayons de mettre un jeu dans l' print
qui ne relève pas des restrictions, ce code ne sera tout simplement pas compilé.
Cela simplifie considérablement le code. Par exemple, voyez comment Nibler a fait le tri dans les plages-v3 , qui fonctionne en C ++ 11/14/17. Il y a un merveilleux code comme celui-ci:
#define CONCEPT_PP_CAT_(X, Y) X ## Y #define CONCEPT_PP_CAT(X, Y) CONCEPT_PP_CAT_(X, Y)
Pour que plus tard vous puissiez faire:
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 { ...
J'espère que vous vouliez déjà voir tout cela et utiliser simplement des concepts préparés dans un nouveau compilateur.
Points de personnalisation
La prochaine chose intéressante qui peut être trouvée dans la norme est customization.point.object . Ils sont activement utilisés dans la bibliothèque Nibler Ranges.
Le point de personnalisation est une fonction utilisée par la bibliothèque standard afin qu'il puisse être surchargé pour les types d'utilisateurs dans l'espace de noms de l'utilisateur, et ces surcharges peuvent être trouvées à l'aide d'ADL.
Les points de personnalisation sont conçus avec les principes architecturaux suivants à l' cust
( cust
est le nom de certains points de personnalisation imaginaires):
- Le code qui appelle
cust
écrit sous la forme qualifiée std::cust(a)
ou non qualifié: using std::cust; cust(a);
using std::cust; cust(a);
. Les deux entrées doivent se comporter de manière identique. En particulier, ils doivent trouver toutes les surcharges utilisateur dans l'espace de noms associé aux arguments. - Code qui utilise
cust
sous la forme d'une std::cust; cust(a);
std::cust; cust(a);
ne devrait pas pouvoir contourner les restrictions imposées à std::cust
. - Les appels de points personnalisés devraient fonctionner efficacement et de manière optimale sur tout compilateur assez moderne.
- La décision ne devrait pas créer de nouvelles violations de la règle de définition unique (RLL) .
Pour comprendre de quoi il s'agit, vous pouvez jeter un œil au N4381 . À première vue, ils ressemblent à un moyen d'écrire vos propres versions de begin
, swap
, data
, etc., et la bibliothèque standard les récupère à l'aide d'ADL.
La question est, en quoi cela diffère-t-il de l'ancienne pratique, lorsque l'utilisateur écrit une surcharge pour certains begin
pour son propre type et espace de noms? Et pourquoi sont-ils même des objets?
En fait, ce sont des instances d' objets fonctionnels dans l' std
. Leur objectif est de tirer d'abord des vérifications de type (conçues comme des concepts) sur tous les arguments d'affilée, puis d'envoyer l'appel à la fonction correcte dans l' std
ou de le mettre en vente dans ADL.
En fait, ce n'est pas le genre de chose que vous utiliseriez dans un programme normal sans bibliothèque. C'est une fonctionnalité de la bibliothèque standard, qui vous permettra d'ajouter une vérification de concept aux futurs points d'extension, ce qui entraînera à son tour l'affichage d'erreurs plus belles et plus compréhensibles si vous avez gâché quelque chose dans les modèles.
L'approche actuelle des points de personnalisation pose quelques problèmes. Premièrement, il est très facile de tout casser. Imaginez ce code:
template<class T> void f(T& t1, T& t2) { using std::swap; swap(t1, t2); }
Si nous appelons accidentellement std::swap(t1, t2)
notre propre version de swap
ne démarrera jamais, peu importe ce que nous y mettons. Mais plus important encore, il n'y a aucun moyen d'attacher de manière centralisée des vérifications de concept à ces implémentations de fonctions personnalisées. Dans N4381, ils écrivent:
«Imaginez qu'un jour à l'avenir, std::begin
exigera que son argument soit modélisé comme un concept Range
. L'ajout d'une telle restriction n'aura tout simplement aucun effet sur le code en utilisant std::begin
:
using std::begin; begin(a);
Après tout, si l'appel de begin
est envoyé à la version surchargée créée par l'utilisateur, les restrictions sur std::begin
simplement ignorées. »
La solution décrite dans la proposition résout les deux problèmes, pour cela nous utilisons l'approche de cette implémentation spéculative de std::begin
(vous pouvez regarder godbolt ):
#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{}; }
Un appel qualifié de certains my_std::begin(someObject)
passe toujours par my_std::detail::begin_fn
- et c'est bien. Qu'arrive-t-il à un appel sans réserve? Relisons notre article:
«Dans le cas où begin est appelé sans qualification immédiatement après l'apparition de my_std::begin
dans la portée, la situation change quelque peu. À la première étape de la recherche, le nom begin
résolu en l'objet global my_std::begin
. Étant donné que la recherche a trouvé un objet et non une fonction, la deuxième phase de la recherche n'est pas effectuée. En d'autres termes, si my_std::begin
est un objet, alors utiliser la construction my_std::detail::begin_fn begin; begin(a);
my_std::detail::begin_fn begin; begin(a);
simplement équivalent à std::begin(a);
"Et comme nous l'avons vu, cela lance ADL personnalisé."
C'est pourquoi la validation de concept peut être effectuée dans un objet fonction dans l' std
avant qu'ADL appelle la fonction fournie par l'utilisateur. Il n'y a aucun moyen de tromper ce comportement.
Comment les points de personnalisation se personnalisent-ils?
En fait, «objet de point de personnalisation» (CPO) n'est pas un bon nom. Du nom, il n'est pas clair comment ils se développent, quels mécanismes sont sous le capot, quelles fonctions ils préfèrent ...
Ce qui nous amène au terme «nibloïde». Un nibloïde est un CPO qui appelle la fonction X s'il est défini dans la classe, sinon il appelle la fonction X s'il existe une fonction libre appropriée, sinon il essaie d'exécuter une solution de repli de la fonction X.
Ainsi, par exemple, les ranges::swap
nibloïdes ranges::swap
lors de l'appel de ranges::swap(a, b)
essaieront d'abord d'appeler a.swap(b)
. S'il n'y a pas une telle méthode, il essaiera d'appeler swap(a, b)
utilisant ADL. Si cela ne fonctionne pas, essayez 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)
.
Résumé
Comme Matt a plaisanté sur Twitter, Dave a suggéré une fois de faire fonctionner les objets fonctionnels avec ADL tout comme les fonctions normales, pour des raisons de cohérence. L'ironie est que leur capacité à désactiver l'ADL et à être invisible pour lui est maintenant devenue leur principal avantage.
Cet article entier était une préparation pour cela.
" Je viens de tout comprendre, c'est tout. Voulez-vous écouter ?
Avez-vous déjà regardé quelque chose, et cela semblait fou, puis sous un jour différent
des choses folles les voyant normales?

N'ayez pas peur. N'ayez pas peur. Je me sens tellement bien dans l'âme. Tout ira bien. Je ne me sens pas si bien depuis de nombreuses années. Tout ira bien.

Minute de publicité. Déjà cette semaine , du 19 au 20 avril, C ++ Russia 2019 aura lieu - une conférence remplie de présentations hardcore à la fois sur la langue elle-même et sur des questions pratiques comme le multithreading et la performance. Soit dit en passant, la conférence est ouverte par Nicolai Josuttis, l'auteur de The C ++ Standard Library: A Tutorial and Reference , mentionné dans l'article. Vous pouvez vous familiariser avec le programme et acheter des billets sur le site officiel . Il reste très peu de temps, c'est la dernière chance.