c++ namespaces overload-resolution argument-dependent-lookup

c++ - ¿Cuáles son los peligros de ADL?



namespaces overload-resolution (2)

Hay un gran problema con la búsqueda dependiente de argumentos. Considere, por ejemplo, la siguiente utilidad:

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

Es lo suficientemente simple, ¿verdad? Podemos llamar a print_n() y pasarle cualquier objeto y llamará a print para imprimir el objeto n veces.

En realidad, resulta que si solo miramos este código, no tenemos absolutamente ninguna idea de a qué función llamará print_n . Puede ser la plantilla de función de print que se proporciona aquí, pero podría no serlo. ¿Por qué? Búsqueda dependiente de argumentos.

Como ejemplo, digamos que ha escrito una clase para representar a un unicornio. Por alguna razón, también ha definido una función llamada print (¡qué coincidencia!) Que simplemente hace que el programa se cuelgue al escribir en un puntero nulo sin referencias (quién sabe por qué lo hizo, eso no es importante):

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; } }

Luego, escribes un pequeño programa que crea un unicornio y lo imprime cuatro veces:

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

Compilas este programa, lo ejecutas y ... se cuelga. "¡De ninguna manera!", Dices: "¡Acabo de llamar a print_n , que llama a la función de impresión para imprimir el unicornio cuatro veces!" Sí, eso es cierto, pero no ha llamado a la función de print que esperaba que llamara. Se llama my_stuff::print .

¿Por qué my_stuff::print seleccionado? Durante la búsqueda de nombres, el compilador ve que el argumento de la llamada a print es del tipo unicorn , que es un tipo de clase que se declara en el espacio de nombres my_stuff .

Debido a la búsqueda dependiente de los argumentos, el compilador incluye este espacio de nombres en su búsqueda de funciones candidatas llamadas print . Encuentra my_stuff::print , que luego se selecciona como el mejor candidato viable durante la resolución de sobrecarga: no se requiere conversión para llamar a cualquiera de las funciones de print candidatas y las funciones no de plantilla son preferibles a las plantillas de función, por lo que la función my_stuff::print es el mejor partido.

(Si no lo cree, puede compilar el código en esta pregunta tal como está y ver ADL en acción).

Sí, la búsqueda dependiente de los argumentos es una característica importante de C ++. Esencialmente se requiere para lograr el comportamiento deseado de algunas características del lenguaje, como los operadores sobrecargados (considere la biblioteca de secuencias). Dicho esto, también es muy, muy defectuoso y puede conducir a problemas realmente desagradables. Ha habido varias propuestas para corregir búsquedas dependientes de argumentos, pero ninguna de ellas ha sido aceptada por el comité de estándares de C ++.

Hace algún tiempo, leí un artículo que explicaba varias dificultades de la búsqueda dependiente de argumentos, pero ya no puedo encontrarlo. Se trataba de obtener acceso a cosas a las que no deberías acceder o algo así. Así que pensé en preguntar aquí: ¿cuáles son los peligros de ADL?


La respuesta aceptada es simplemente errónea: no es un error de ADL. Muestra un antipatrónomo descuidado para usar llamadas de función en la codificación diaria: ignorancia de nombres dependientes y confianza ciegamente en nombres de funciones no calificados.

En resumen, si está utilizando un nombre no calificado en la postfix-expression de postfix-expression de una llamada a función, debería haber reconocido que ha otorgado la posibilidad de que la función pueda "anularse" en otro lugar (sí, este es un tipo de polimorfismo estático). Por lo tanto, la ortografía del nombre no calificado de una función en C ++ es exactamente parte de la interfaz .

En el caso de la respuesta aceptada, si print_n realmente necesita una print ADL (es decir, que permita anularla), debería haber sido documentada con el uso de una print no calificada como un aviso explícito, por lo que los clientes recibirían un contrato que debería print cuidadosamente declarado y la mala conducta sería toda la responsabilidad de my_stuff . De lo contrario, es un error de print_n . La solución es simple: calificar print con prefix utility:: . Esto es de hecho un error de print_n , pero apenas un error de las reglas de ADL en el lenguaje.

Sin embargo, existen cosas no deseadas en la especificación del lenguaje, y técnicamente, no solo una . Se realizaron más de 10 años, pero aún no se ha corregido nada en el lenguaje. Se pierden por la respuesta aceptada (excepto que el último párrafo es únicamente correcto hasta ahora). Vea este paper para más detalles.

Puedo adjuntar un caso real contra el nombre de búsqueda desagradable. Estaba implementando is_nothrow_swappable donde __cplusplus < 201703L . Me resultó imposible confiar en ADL para implementar dicha función una vez que tengo una plantilla de función de swap declarada en mi espacio de nombres. Tal swap siempre se encontraría junto con std::swap introducido por un idiomático using std::swap; para usar ADL bajo las reglas de ADL, y luego vendría la ambigüedad de swap donde se is_nothrow_swappable a la plantilla de swap (que instanciaría is_nothrow_swappable para obtener la noexcept-specification adecuada). Combinado con las reglas de búsqueda en dos fases, el orden de las declaraciones no cuenta, una vez que se incluye el encabezado de la biblioteca que contiene la plantilla de swap . Por lo tanto, a menos que sobrecargue todos mis tipos de biblioteca con la función de swap especializada (para suprimir cualquier swap plantillas genéricas candidato que coincida con la resolución de sobrecarga después de ADL), no puedo declarar la plantilla. Irónicamente, la plantilla de swap declarada en mi espacio de nombres es exactamente para utilizar ADL (considere boost::swap ) y es uno de los clientes directos más importantes de is_nothrow_swappable en mi biblioteca (BTW, boost::swap no respeta la especificación de excepción) . Esto perfectamente venció mi propósito, suspiro ...

#include <type_traits> #include <utility> #include <memory> #include <iterator> namespace my { #define USE_MY_SWAP_TEMPLATE true #define HEY_I_HAVE_SWAP_IN_MY_LIBRARY_EVERYWHERE false namespace details { using ::std::swap; template<typename T> struct is_nothrow_swappable : std::integral_constant<bool, noexcept(swap(::std::declval<T&>(), ::std::declval<T&>()))> {}; } // namespace details using details::is_nothrow_swappable; #if USE_MY_SWAP_TEMPLATE template<typename T> void swap(T& x, T& y) noexcept(is_nothrow_swappable<T>::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 }

Pruebe https://wandbox.org/permlink/4pcqdx0yYnhhrASi y convierta USE_MY_SWAP_TEMPLATE en true para ver la ambigüedad.