¿Cuáles son las trampas de ADL?


Hace algún tiempo leí un artículo que explicaba varias trampas 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 tener acceso o algo así. Así que pensé en preguntar aquí: ¿cuáles son las trampas de ADL?

Author: fredoverflow, 2010-06-02

2 answers

Hay un gran problema con la búsqueda dependiente de argumentos. Consideremos, 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 bastante 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, tenemos absolutamente ninguna idea qué función será llamada por print_n. Podría ser la plantilla de función print dada aquí, pero podría no serlo. ¿Por qué? Búsqueda dependiente del argumento.

Como ejemplo, digamos que has escrito una clase para representar a un unicornio. Por alguna razón, también ha definido una función llamada print (¡qué coincidencia!) eso solo causa que el programa se bloquee escribiendo a un puntero nulo desreferenciado (quién sabe por qué hiciste esto; 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; }
}

A continuación, 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);
}

Usted compila este programa, lo ejecuta, y... se accidente. "¿Qué?! De ninguna manera, "usted dice:" Acabo de llamar print_n, que llama a la función print para imprimir el unicornio cuatro veces!"Sí, eso es cierto, pero no ha llamado a la función print que esperaba que llamara. Se llama my_stuff::print.

¿Por qué se selecciona my_stuff::print? Durante la búsqueda de nombres, el compilador ve que el argumento de la llamada a print es de 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 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 ninguna de las funciones candidatas print y las funciones nontemplate son preferidas a las plantillas de funciones, por lo que la función nontemplate my_stuff::print es la mejor coincidencia.

(Si no cree esto, puede compilar el código en esta pregunta tal cual y ver ADL en acción.)

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

 63
Author: James McNellis,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-11-22 05:32:32

La respuesta aceptada es simplemente incorrecta - esto no es un error de ADL. Muestra un anti-patrón descuidado para usar llamadas de función en la codificación diaria: ignorancia de nombres dependientes y confiar ciegamente en nombres de funciones no calificados.

En resumen, si está utilizando un nombre no calificado en el postfix-expression de una llamada a una función, debería haber reconocido que ha concedido la capacidad de que la función pueda ser "anulada" en otro lugar (sí, esto es una especie de polimorfismo estático). Por lo tanto, el 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 el print_n realmente necesita ADL print (es decir, permitir que se anule), debería haberse documentado con el uso de unqualified print como un aviso explícito, por lo que los clientes recibirían un contrato que print debería declararse cuidadosamente y la mala conducta sería toda responsabilidad de my_stuff. De lo contrario, es un error de print_n. La solución es simple: califica print con el prefijo utility::. Esto es de hecho un error de print_n, pero difícilmente un error de las reglas ADL en el lenguaje.

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

I puede agregar 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 tal característica una vez que tengo una plantilla de función swap declarada en mi espacio de nombres. Tal swap siempre se encuentran juntos con std::swap introducido por un idiomáticas using std::swap; para utilizar ADL bajo la ADL reglas, y luego vendría la ambigüedad de swap donde swap plantilla (que instanciar is_nothrow_swappable para obtener la correcta noexcept-specification) es llamado. Combinado con reglas de búsqueda de 2 fases, el orden de las declaraciones no cuenta, una vez que se incluye el encabezado de la biblioteca que contiene la plantilla swap. Por lo tanto, a menos que sobrecargue todos mis tipos de biblioteca con función especializada swap (para suprimir cualquier plantilla genérica candidata swap coincidiendo con la resolución de sobrecarga después de ADL), no puedo declarar la plantilla. Irónicamente, la plantilla swap declarada en mi espacio de nombres es exactamente para utilizar ADL (consideremos boost::swap) y es una de las más significativas cliente directo de is_nothrow_swappable en mi biblioteca (POR cierto, boost::swap no respeta la especificación de excepción). Esto venció perfectamente 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
}

Try https://wandbox.org/permlink/4pcqdx0yYnhhrASi y gire USE_MY_SWAP_TEMPLATE a true para ver la ambigüedad.

 2
Author: FrankHB,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-07-16 07:09:16