Quels sont les pièges de l’ADL?

Il y a quelque temps, j’ai lu un article qui expliquait plusieurs écueils de la recherche dépendante des arguments, mais je ne le trouve plus. Il s’agissait d’accéder à des choses auxquelles vous ne devriez pas avoir access ou quelque chose du genre. Alors j’ai pensé que je demanderais ici: quels sont les pièges de l’ADL?

Il y a un gros problème avec la recherche dépendante des arguments. Considérons, par exemple, l’utilitaire suivant:

#include  namespace utility { template  void print(T x) { std::cout << x << std::endl; } template  void print_n(T x, unsigned n) { for (unsigned i = 0; i < n; ++i) print(x); } } 

C'est assez simple, non? Nous pouvons appeler print_n() et lui passer n'importe quel object. Il appellera print pour imprimer l'object n fois.

En fait, il s'avère que si nous ne regardons que ce code, nous n’avons absolument aucune idée de la fonction appelée par print_n . Il peut s'agir du modèle de fonction d' print donné ici, mais ce n'est peut-être pas le cas. Pourquoi? Recherche dépendante de l'argument.

Par exemple, supposons que vous ayez écrit une classe pour représenter une licorne. Pour une raison quelconque, vous avez également défini une fonction nommée print (quelle coïncidence!) Qui provoque simplement le blocage du programme en écrivant dans un pointeur nul déréférencé (qui sait pourquoi vous l'avez fait, ce n'est pas important):

 namespace my_stuff { struct unicorn { /* unicorn stuff goes here */ }; std::ostream& operator<<(std::ostream& os, unicorn x) { return os; } // Don't ever call this! It just crashes! I don't know why I wrote it! void print(unicorn) { *(int*)0 = 42; } } 

Ensuite, vous écrivez un petit programme qui crée une licorne et l’imprime quatre fois:

 int main() { my_stuff::unicorn x; utility::print_n(x, 4); } 

Vous comstackz ce programme, exécutez-le et ... il plante. "Quoi?! Pas question", vous dites: "Je viens d'appeler print_n , qui appelle la fonction d'impression pour imprimer la licorne quatre fois!" Oui, c'est vrai, mais cela n'a pas appelé la fonction d' print vous vous attendiez. Il s'appelle my_stuff::print .

Pourquoi my_stuff::print sélectionné? Lors de la recherche de nom, le compilateur voit que l'argument de l'appel à print est de type unicorn , qui est un type de classe déclaré dans l'espace de noms my_stuff .

En raison de la recherche dépendante des arguments, le compilateur inclut cet espace de noms dans sa recherche de fonctions candidates nommées print . Il trouve my_stuff::print , qui est ensuite sélectionné comme le meilleur candidat viable lors de la résolution de la surcharge: aucune conversion n'est requirejse pour appeler les fonctions d' print candidates et les fonctions non-modèle sont préférables aux modèles de fonction my_stuff::print le meilleur match.

(Si vous n'y croyez pas, vous pouvez comstackr le code dans cette question tel quel et voir ADL en action.)

Oui, la recherche dépendante des arguments est une fonctionnalité importante de C ++. Il est essentiellement nécessaire pour obtenir le comportement souhaité de certaines fonctionnalités du langage comme les opérateurs surchargés (considérez la bibliothèque de stream). Cela dit, c'est aussi très, très imparfait et peut mener à des problèmes vraiment laids. Plusieurs propositions ont été faites pour corriger la recherche dépendante des arguments, mais aucune d'entre elles n'a été acceptée par le comité des normes C ++.

La réponse acceptée est simplement fausse – ce n’est pas un bogue de l’ADL. Il montre un anti-pattern imprudent pour utiliser les appels de fonctions dans le codage quotidien – ignorance des noms dépendants et s’appuyer aveuglément sur des noms de fonctions non qualifiés.

En résumé, si vous utilisez un nom non qualifié dans l’ postfix-expression d’un appel de fonction, vous devriez avoir reconnu que vous avez la possibilité que la fonction puisse être “surchargée” ailleurs (oui, il s’agit d’une sorte de polymorphism statique). Ainsi, l’orthographe du nom non qualifié d’une fonction en C ++ fait exactement partie de l’ interface .

Dans le cas de la réponse acceptée, si le print_n vraiment besoin d’une print ADL (c.-à-d. print_n permettant d’être annulé), il aurait dû être documenté avec l’utilisation d’ print non qualifiés en tant qu’avis explicite. soigneusement déclarée et la mauvaise conduite serait de la responsabilité de my_stuff . Sinon, c’est un bug de print_n . Le correctif est simple: qualifiez print avec le préfixe utility:: . C’est en effet un bug de print_n , mais à peine un bug des règles ADL dans le langage.

Cependant, il existe des éléments indésirables dans la spécification du langage, et techniquement, pas un seul . Ils sont réalisés plus de 10 ans, mais rien dans la langue n’est encore fixé. Ils sont manqués par la réponse acceptée (sauf que le dernier paragraphe est uniquement correct jusqu’à présent). Voir ce document pour plus de détails.

Je peux append un cas réel à la recherche de nom. J’étais en is_nothrow_swappable implémenter is_nothrow_swappable__cplusplus < 201703L . J'ai trouvé impossible de compter sur ADL pour implémenter une telle fonctionnalité une fois que j'ai un modèle de fonction de swap déclaré dans mon espace de noms. Un tel swap serait toujours trouvé avec std::swap introduit par un idiomatique using std::swap; pour utiliser ADL sous les règles ADL, alors il y aurait une ambiguïté de swap où le template swap (qui instancierait is_nothrow_swappable pour obtenir la noexcept-specification appropriée) est appelé. Combiné aux règles de recherche en deux phases, l'ordre des déclarations ne compte pas, une fois que l'en-tête de la bibliothèque contenant le modèle de swap est inclus. Donc, à moins de surcharger tous mes types de bibliothèque avec une fonction de swap spécialisée (pour supprimer tout swap modèle générique candidat correspondant par une résolution de surcharge après ADL), je ne peux pas déclarer le modèle. Ironiquement, le gabarit de swap déclaré dans mon espace de nommage utilise exactement ADL (considérez boost::swap ) et il est l'un des clients directs les plus significatifs de is_nothrow_swappable dans ma bibliothèque (BTW, boost::swap ne respecte pas la spécification d'exception) . Cela bat parfaitement mon but, soupire ...

 #include  #include  #include  #include  namespace my { #define USE_MY_SWAP_TEMPLATE true #define HEY_I_HAVE_SWAP_IN_MY_LIBRARY_EVERYWHERE false namespace details { using ::std::swap; template struct is_nothrow_swappable : std::integral_constant(), ::std::declval()))> {}; } // namespace details using details::is_nothrow_swappable; #if USE_MY_SWAP_TEMPLATE template void swap(T& x, T& y) noexcept(is_nothrow_swappable::value) { // XXX: Nasty but clever hack? std::iter_swap(std::addressof(x), std::addressof(y)); } #endif class C {}; // Why I declared 'swap' above if I can accept to declare 'swap' for EVERY type in my library? #if !USE_MY_SWAP_TEMPLATE || HEY_I_HAVE_SWAP_IN_MY_LIBRARY_EVERYWHERE void swap(C&, C&) noexcept {} #endif } // namespace my int main() { my::C a, b; #if USE_MY_SWAP_TEMPLATE my::swap(a, b); // Even no ADL here... #else using std::swap; // This merely works, but repeating this EVERYWHERE is not attractive at all... and error-prone. swap(a, b); // ADL rocks? #endif } 

Essayez https://wandbox.org/permlink/4pcqdx0yYnhhrASi et USE_MY_SWAP_TEMPLATE sur true pour voir l'ambiguïté.