Disponible la versión 6 de OrganiZATOR
Descubre un nuevo concepto en el manejo de la información.
La mejor ayuda para sobrevivir en la moderna jungla de datos la tienes aquí.

Curso C++

[Home]  [Inicio]  [Índice]


Búsqueda de nombres

§1  Sinopsis

Cuando compilador encuentra un identificador X en un programa, antes de realizar el "parsing" ( 1.4), debe determinar a que entidad representa. Esta operación se denomina búsqueda de nombres ("name-lookup"), y asocia sin ambigüedad cada nombre con una declaración de dicho nombre dentro de su misma unidad de compilación ( 1.4.2). Si al final del proceso no ha aparecido la declaración correspondiente entonces el programa es incorrecto y se lanza un mensaje de error del tipo:  Error: Undefined symbol 'X' in....

Cuando se trata de funciones puede ocurrir que el "name-lookup" asocie un mismo nombre con más de una declaración; es el caso de funciones sobrecargadas, en las que un mismo nombre corresponde a varias definiciones de la "misma" función. En estos casos, después de la búsqueda de nombres tiene lugar un segundo proceso denominado de resolución de sobrecarga ("overload resolutión"), en el que se intenta averiguar a que definición concreta corresponde el identificador ( 4.4.1a).

El proceso de búsqueda de nombres sigue un conjunto relativamente extenso de reglas que cubren todas las posibilidades que pueden presentarse (búsqueda en espacios de nombres; en miembros de clases y en jerarquías de clases; en funciones y bloques anidados, etc). Además existen modificadores que alteran la forma de esta búsqueda; son los denominados especificadores o modificadores de acceso. Por ejemplo, la directiva using ( 4.1.11c), o el especificador friend ( 4.11.2a1). Incluso existe un operador especial para ayudar en la tarea: el operador :: de resolución de ámbito ( 4.9.19).

En C++ es importantísimo entender mínimamente las reglas que rigen este proceso, pues en ocasiones pueden obtenerse resultados desagradables, inesperados y difíciles de entender. Una visión sinóptica del proceso sería como sigue:

El compilador comienza su búsqueda en el propio ámbito donde ha encontrado el identificador (puede ser el interior de un bloque, un espacio de nombres, una función, una clase, el espacio global del fichero, etc). En caso de no encontrarse ahí la definición correspondiente, se mueve al ámbito inmediatamente exterior al de inicio, donde se realiza una nueva búsqueda. El proceso de repite secuencialmente realizándose la búsqueda en ámbitos cada vez más amplios, hasta que se encuentra al menos una instancia del nombre buscado. En cuyo caso se interrumpe la búsqueda y se trabaja solo con las instancias encontradas.

Si se han encontrado varias definiciones que corresponden con el nombre, entonces nos encontramos con un caso de sobrecarga de funciones, y se concreta la decisión utilizando el mecanismo de congruencia de argumentos.

Nota:  En ciertas ocasiones, cuando la definición del identificador de una función u operador no es reconocida en el espacio actual, el compilador realiza una búsqueda de dicho identificador en los subespacios de sus argumentos u operandos (Koenig-lookup 4.1.11c).


La primera consecuencia del sistema utilizado, es que los posibles ámbitos exteriores al que produce la primera ocurrencia dejan de ser investigados, y las posibles definiciones existentes en ellos son ignoradas. Esto puede suceder incluso si se trata de funciones, y después de haberse realizado el "Name-lookup", el mecanismo de resolución de sobrecarga no encuentra una definición concordante con los argumentos utilizados.Incluso podría ocurrir que en alguno de los ámbitos exteriores que no han sido investigados, existiese una definición adecuada [1].

Cuando se trata de funciones-miembro (métodos) en jerarquías de clases, es especialmente importante saber que, si se parte de la subclase más derivada, la primera definición que se encuentre en la escala ascendente de la jerarquía ocultará a todas las demás.

El sumario de reglas para relacionar un identificador con su correspondiente declaración es el siguiente:

  • Se verifica la ambigüedad del nombre. Si no se detecta ambigüedad dentro del ámbito, se inicia la secuencia de acceso.

  • Si no ocurre ningún error de control de acceso, se comprueba el tipo de objeto, función, clase, etc.

  • Si el nombre se ha utilizado fuera de cualquier clase o función, o es precedido por el operador unitario de modificación de ámbito ::, y el nombre no está afectado por el operador binario :: [2] o el de selección de miembro, directo  . o indirecto ->, entonces el nombre debe pertenecer a un objeto, función o enumerador global.

  • Si el nombre n aparece en cualquiera de las formas  C::n (donde x es un objeto de C o una referencia-a-C), o ptr->n (donde ptr es un puntero-a-C), entonces n es el nombre de un miembro de C o el nombre de una superclase de C.

  • Cualquier nombre que no haya sido discutido todavía y que sea utilizado en un método estático, debe ser declarado en el bloque en que aparece, o en un bloque superior, o ser un nombre global.La declaración de un nombre local n oculta declaraciones de n en bloques externos, así como las declaraciones globales de n.  Los nombres en ámbitos diferentes no son sobrecargados.

  • Cualquier nombre no discutido aún y que sea utilizado en un miembro no estático de la clase C, o bien debe ser declarado en el bloque en que aparece (o en el otro superior), o ser un nombre global, o ser un miembro de la clase C,  o de una superclase de C. La declaración de un miembro oculta la declaración del mismo nombre en las superclases.

  • El nombre del argumento de una función en la definición de esta, tiene el ámbito del bloque más exterior de dicha función (el que define el cuerpo de la función). El nombre de un argumento en la declaración de una función (que no incluya definición, solo prototipo) no tiene ningún ámbito (solo el del prototipo). El ámbito de un argumento por defecto es determinado por el punto de declaración de dicho argumento, pero no puede acceder a variables locales o miembros de clase no estáticos. Los argumentos por defecto se evalúan en cada punto de llamada.

  • El inicializador de un constructor se evalúa en el ámbito del bloque más externo del constructor, de forma que puede referirse a los nombres de los argumentos de dicho constructor.

Ejemplo:

#include <iostream>
using namespace std; 

int x = 1;              // L4:

class C {               // L6:
  public:
  static int x;         // L8:
  class Ci {
    public:
    int x;              // L11:
    void C::Ci::func() {
      int x = 1234;     // L13:
      cout << "X = " << x << endl; // L14:
    }
    Ci(int p = 123) : x(p) { }     // L16:
  };
};
int C::x = 12;          // L19:

void main() {    // ===============
  C::Ci ci;      // M1
  ci.func();     // M2
}

Salida:

1234

Comentario:

En L4 el programa define un objeto tipo int y ámbito global que se inicia con un valor 1.A continuación se define una clase ::C también en el ámbito global.Esta clase tiene un único miembro estático C::x  que se inicia en la sentencia L19 con el valor 12.

En el espacio de C se define una clase anidada C::Ci, que tiene solo dos miembros:  una propiedad x  y un método func.

El método C::Ci::func()  define un entero x, que es iniciado con 1234 y a continuación muestra este valor.

La función main se limita a instanciar un objeto ci de la clase anidada Ci y a invocar el método func sobre el objeto.

Al encontrar el compilador el identificador x en la sentencia L14, realiza un "Name-lookup" para encontrar la definición correspondiente empezando en su propio ámbito (que es el de la función). La concordancia ocurre con el identificador x definido en L13, de forma que este es el resultado 1234 obtenido en la salida.

El juego consiste en ir eliminando selectivamente ciertas sentencias para comprobar como reacciona el mecanismo de búsqueda ante cada nueva circunstancia:

  • En primer lugar eliminamos la sentencia L13, compilamos y volvemos a ejecutar;  la nueva salida es 123.  La razón es que al no existir concordancia en el ámbito de func con el identificador x buscado, el compilador se mueve al ámbito inmediato, que es el de la clase Ci (recuerde que una clase actúa como un espacio de nombres). Aquí se encuentra concordancia con el miembro Ci::x que ha sido iniciado por el constructor (L16), de forma que este es el valor obtenido.

  • El segundo paso es eliminar la propiedad x de Ci y el constructor C::Ci::Ci() para que no se produzca error (sentencias L11 y L16). Ahora la nueva salida es 12.  La razón es que al no encontrar tampoco concordancia en el subespacio Ci, el compilador se mueve al espacio contenedor, que es ahora el ámbito de la clase C.  Aquí se produce otra ocurrencia con el miembro C::x. que ha sido iniciado a 12 en L19.Observe que este miembro lo hemos declarado estático para garantizar su existencia incluso en ausencia de ninguna instancia concreta de C ( 4.11.7).

  • Finalmente, como tercer paso, eliminamos también las sentencias L8 y L19 (esta última para evitar el error). El nuevo valor obtenido 1, se debe a que después del recorrido anterior, el compilador debe moverse al espacio global del fichero, donde encuentra la única definición del identificador x que existente ahora en el programa (en L4).

  Inicio.


[1]   En muchos casos esta definición es precisamente la que nosotros pensábamos que se usaría.

[2]  Recordemos que el operador de resolución de ámbito :: ( 4.9.19) puede ser utilizado con uno o dos operandos.