Volver al inicio

Mecanismo de Búsqueda de Nombres en C++: ADL y Búsqueda de Plantillas en Dos Fases

El artículo explica el complejo mecanismo de búsqueda de nombres (name lookup) en C++. Se cubren términos clave, ADL y búsqueda en dos fases en plantillas. Se proporcionan ejemplos de código y diferencias de implementación entre los compiladores GCC y Clang. Recomendaciones para evitar errores comunes.

¿Por qué los compiladores de C++ se confunden con los nombres? Desglosamos ADL y la búsqueda en dos fases
Advertisement 728x90

# Cómo el compilador de C++ desenreda el embrollo de nombres: ADL, plantillas y búsqueda en dos fases

La búsqueda de nombres es una de las etapas más complejas de la compilación en C++. Debido a la evolución histórica del lenguaje, las reglas de búsqueda se han vuelto enrevesadas, lo que provoca errores inesperados incluso en desarrolladores experimentados. Vamos a desglosar cómo funciona el mecanismo de búsqueda y por qué los distintos compiladores se comportan de forma diferente.

Términos clave: Cómo clasifica los nombres el compilador

El compilador emplea una terminología precisa para analizar los nombres en el código. Comprender estos términos es fundamental para desentrañar la "magia" de la búsqueda.

Identificador no calificado (unqualified-id) — la forma básica de un nombre, por ejemplo speed, foo o my_var. Esto también incluye operadores, destructores y literales definidos por el usuario. Ejemplos:

Google AdInline article slot
// Nekvalifitsirovannye names
vector<int> v;       // 'vector' — unqualified name
sort(v.begin(), v.end()); // 'sort' — unqualified name

Nombre calificado (qualified-id) — un nombre con el operador de resolución de ámbito ::, por ejemplo std::vector o Foo::bar. Aquí Foo actúa como calificador y bar como el nombre terminal.

// Kvalifitsirovannye names
std::vector<int> v;  // 'vector' — qualified name
Foo::bar();          // 'bar' — terminal qualified name

Template-id — un nombre de plantilla con argumentos explícitos, por ejemplo std::vector<int>.

// Template-id
std::vector<int> v1;         // 'vector<int>' — template-id
std::pair<int, float> p;     // 'pair<int, float>' — template-id

Nombre terminal — el nombre final en la cadena que busca el compilador. Por ejemplo, en obj->f() el nombre terminal es f; en ns::Foo::bar() es bar. Esto es de vital importancia en plantillas:

Google AdInline article slot
template<typename T>
void wrapper(T& obj) {
    obj.size();      // 'size' — terminal name
    T::value_type x; // 'value_type' — terminal qualified name
}

ADL: Cuando los argumentos toman el mando

La búsqueda dependiente de argumentos (ADL, por sus siglas en inglés: Argument-Dependent Lookup) es un mecanismo que extiende automáticamente el ámbito de búsqueda de funciones según los tipos de los argumentos. Sin ADL, usar operadores sobrecargados con tipos definidos por el usuario sería muy engorroso.

Considera este ejemplo:

namespace sak {
    struct BigNum { int value; };
    std::ostream& operator<<(std::ostream& os, const BigNum& n) {
        return os << "BigNum(" << n.value << ")";
    }
}

int main() {
    sak::BigNum n(42);
    std::cout << n; // ADL nakhodit operator<< in sak
}

El compilador ve que n es de tipo sak::BigNum y añade el espacio de nombres sak al ámbito de búsqueda de operator<<. Sin ADL, tendrías que escribir sak::operator<<(std::cout, n).

Google AdInline article slot

La ADL fue propuesta por Andrew Koenig en 1995 (documento N0645), aunque la idea ya se utilizaba en AT&T Bell Labs. El estándar la codificó en [basic.lookup.koenig]. Sin embargo, la ADL tiene sus trampas: depender en exceso de ella puede generar conflictos si dos bibliotecas definen funciones con los mismos nombres en espacios de nombres distintos.

Algoritmo de ADL:

  • Se realiza la búsqueda no calificada ordinaria.
  • Para llamadas a funciones, entra en juego la ADL: se añaden al ámbito de búsqueda los espacios de nombres de los tipos de argumentos.
  • Si el nombre está calificado (p. ej., std::sort), no se aplica la ADL.

Búsqueda en dos fases en plantillas: Por qué GCC y Clang discrepan

En las plantillas, la búsqueda de nombres se divide en dos fases:

  • Fase 1 (definición de la plantilla): busca nombres no dependientes (independientes de los parámetros de plantilla).
  • Fase 2 (instanciación): busca nombres dependientes (dependientes de los parámetros de plantilla), donde sí se aplica la ADL.

Históricamente, los compiladores interpretaron estas reglas de forma distinta. GCC, antes de la versión 7, usaba "análisis perezoso": el cuerpo de la plantilla no se analizaba en el momento de la definición y todos los nombres se buscaban durante la instanciación. Esto generaba no conformidad con el estándar.

Ejemplo de divergencia:

void foo(int) {}  // globalnaya foo

template<typename T>
void bar(T x) {
    foo(x);   // zavisimoe name — ischetsya in phase 2 (ADL), vsyo correct
    foo(42);  // NEzavisimoe name — must iskatsya in phase 1
}

namespace myns {
    struct MyType {};
    void foo(MyType) {}
}

bar(myns::MyType{}); // GCC: kompiliruetsya, Clang: oshibka

En Clang, foo(42) se busca en la fase 1 y no encuentra una sobrecarga adecuada (el foo(int) global no es válido para MyType), lo que provoca un error. GCC pospone la búsqueda hasta la instanciación y encuentra foo(int).

El comité de estándares prohibió la búsqueda en clases base dependientes durante la fase 1. Considera este ejemplo:

template<typename T>
struct Base {};

template<>
struct Base<int> {
    void process() {}
};

template<typename T>
struct Derived : Base<T> {
    void run() {
        process(); // Error: poszukiwanie in zavisimoy baze zapreschyon in phase 1
    }
};

Si se permitiera la búsqueda en Base<T> durante la fase 1, el código compilaría para T=int pero no para T=float. Esto rompería la predictibilidad de las plantillas.

Lo esencial: Lecciones clave

Al trabajar con búsqueda de nombres en C++, ten en cuenta lo siguiente:

  • El nombre terminal es el foco del compilador. Identifícalo para saber dónde buscar.
  • La ADL extiende la búsqueda a los espacios de nombres de los argumentos. Ideal para operadores, pero puede causar conflictos.
  • En plantillas, los nombres no dependientes se buscan en la definición; los dependientes, en la instanciación. Usa typename para tipos dependientes.
  • Los compiladores distintos pueden manejar casos límite de forma diferente. Prueba en GCC, Clang y MSVC.
  • Para clases base dependientes, califica explícitamente los nombres con this-> o ámbitos.

— Editorial Team

Advertisement 728x90

Leer después