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.