4.1.11c Acceso a elementos de un subespacio
§1 Sinopsis:
Existen tres formas de acceder a los elementos de un subespacio:
- Una declaración explícita .
- La declaración using .
- La directiva using .
- Búsqueda en el espacio de los argumentos .
Las dos primeras se utilizan para accesos de elementos individuales. La tercera permite un cómodo acceso a todos los elementos de un subespacio. La última es un caso especial, y se refiere a un mecanismo de acceso automático implícito en todas las funcines C++.
Recuérdese que no importa que se añadan subespacios al ámbito local. Los identificadores de ámbito global (que es otro subespacio -raíz-) son todavía accesibles utilizando el operador de acceso a ámbito :: ( 4.9.19). Ver ejemplo .
§2 Acceso explícito
Se puede referenciar o acceder a cualquier miembro de un subespacio utilizando el nombre del mismo junto con el operador de resolución de ámbito :: seguido del nombre del miembro. Por ejemplo, para acceder a un miembro específico del subespacio ALPHA:
namespace ALPHA {
...
long double LD;
float f(float y);
class C {
public: int x;
}
}
ALPHA::LD = 1.0; // Aceso a variable
LD
ALPHA::f(1.1); //
Invocar función f
float ALPHA::f(float y) // definición de función f de ALPHA
{
...
}
ALPHA::C::x = 1 // Acceso a miembro x de C en ALPHA
Esta forma de acceso explícito puede y debe utilizarse siempre para evitar ambigüedades; no importa de que subespacio
se trate (excepto anónimos naturalmente). Puede utilizarse el operador :: para acceder a identificadores de cualquier
subespacio (incluso los que ya se han declarado en el ámbito local). Así pues, cualquier identificador de una
aplicación es accesible si se utiliza un direccionamiento adecuado.
§2.1 Identificador cualificado
Las expresiones de elementos en las que se indica el subespacio al que pertenecen mediante el especificador de ámbito ::, se denominan nombres, etiquetas o identificadores cualificados. Ejemplo:
class CL {
...
UNO::func() // miembro cualificado
funcC(){...}
...
}
ALPHA::LD // identificador cualificado
ALPHA::fun(...) // función cualificada
void func(ALPHA::char c); // argumento cualificado
Esta técnica de acceso y referencia es siempre recomendable para elementos que hayan sido previamente declarados en un
subespacio, pues permite al compilador realizar la comprobación de que se quiere acceder a un miembro del subespacio, y no una
nueva declaración. Por ejemplo, si escribimos:
void ALPHA::ff() { // definición de ff
...
}
el compilador supone que estas sentencias son la definición de la función ff que ha sido previamente declarada en ALPHA, y nos avisará del error si ff no ha sido declarada antes en dicho subespacio. En cambio, si hacemos:
namespace ALPHA {
void ff() {
...
}
}
El compilador puede creer que deseamos ampliar el subespacio, y no puede avisar del error en caso de no existir un prototipo de ff previamente declarado en ALPHA.
Esta técnica la vamos a utilizar repetidamente al definir funciones miembro de clases ( 4.11.2a), que son un tipo especial de subespacios. Por ejemplo:
class ALPHA {
void ff(); // declaración de ff
...
}
...
void ALPHA::ff() { // definición de ff
...
}
§2.2 Acceso explícito al ámbito global
El ámbito global de un fichero es otro subespacio, con la peculiaridad de que generalmente no es necesario referirse a él con un identificador especial. Por ejemplo, si en el cuerpo de una función declaramos x = 3; y no existe una variable x en el ámbito de la función, automáticamente se busca si existe tal identificador en el en el ámbito global. No obstante, existen casos en que es necesario referirse explícitamente a una variable del ámbito global. Esto puede hacerse con el operador :: de resolución de ámbito sin ningún prefijo. Ejemplo:
#include <iostream>
namespace ALPHA {
int x = 1;
}
int x = 2;
// Variable global
int suma(int); // Prototipo de función suma
void main() {
int x = 3; // Variable local
std::cout << "X = " << x << std::endl;
std::cout << "X = " << ::x << std::endl;
std::cout << "X = " << ALPHA::x << std::endl;
std::cout << "X = " << suma(x) << std::endl;
}
int suma(int x) { // Definición de función suma
return (x + ALPHA::x + ::x);
}
Salida:
X = 3
X = 2
X = 1
X = 6
Es buena técnica de programación C++ no abusar de las variables globales. En caso necesario, englobarlas en un subespacio y referenciarlas a través de su identificador correspondiente.
§3 Declaración using
Los miembros de un subespacio pueden ser accedidos individualmente con la declaración using (palabra reservada). La sintaxis es:
using :: identificador;
Ejemplo:
using ALPHA::x;
Cuando se utiliza esta fórmula, el identificador declarado es añadido al subespacio local, lo que quiere decir que, una vez
establecida la declaración, en adelante el identificador declarado es identificado correctamente, como si perteneciera al
subespacio local. Este recurso es muy útil cuando un identificador aparece muchas veces en un contexto, pues se evita tener
que referenciarlo repetidamente mediante un identificador cualificado
.
El ejemplo que sigue muestra el uso de la declaración using con una función declarada en dos subespacios.
#include <iostream>
namespace UNO { // Subespacio
float f(float y) { return y; }
void g(void) { std::cout << "Versión UNO" << std::endl; }
}
namespace DOS { // Subespacio
void g(void) { std::cout << "Versión DOS" << std::endl; }
}
void main(void) {
using UNO::f; // declaración titulada
using DOS::g; // declaración titulada
// en adelante no son necesarios los especificadores para f ni para g
float x = 0; // x pertenece
al subespacio local
x = f(2.1); // se refiere a f del subespacio UNO
g();
// se refiere a g del subespacio DOS
}
La declaración using también puede ser utilizada para el acceso a miembros de subespacios en objetos de clases
derivadas ( 4.11.2b2).
§4 Directiva using
C++ proporciona un medio cómodo de acceder a la totalidad del subespacio mediante la directiva using (palabra reservada). La sintaxis es como sigue:
using namespace <identificador-de-subespacio>;
Ejemplo:
using namespace ALPHA ;
using namespace COMUNICACIONES::COMUNICACIONES_TCPIP;
La directiva using establece que a partir del punto en que se utiliza, todos los identificadores del subespacio
indicado son visibles. El subespacio debe ser declarado antes de utilizar la
directiva. Ejemplo:
using namespace BETA; // Error BETA no declarado todavía
...
namespace BETA {
...
}
El efecto de esta directiva es como si los objetos del subespacio quedaran incluidos en el espacio local
[1]. Aunque como veremos a continuación (
), los elementos homónimos del subespacio local siguen
existiendo y tienen precedencia sobre los intrusos. Ejemplo:
#include <iostream>
using namespace std;
namespace ALPHA {
void f(int i) { cout << "Entero: " << i << endl; } // L.5
};
void f(char c) { cout << "Caracter: " << c << endl; } // L.7
int main() { // ===============
f('a'); // M.1
f(45); // M.2
using namespace ALPHA; // M.3
f('a'); // M.4
f(45); // M.5
return 0;
}
Salida:
Caracter: a
Caracter: -
Caracter: a
Entero: 45
Comentario:
M.1 y M.2 son responsables de las dos primeras salidas. En este momento solo es visible la definición de f proporcionada por L.7, de forma que en la invocación de M.2, el compilador debe realizar un modelado para adecuar el tipo de argumento actual (int 45) con el esperado (char). El resultado de la segunda salida es el carácter ASCII "-", correspondiente a dicho valor numérico ( 2.2.1a).
La directiva de la sentencia M.3 hace que a partir de este punto, sea visible el contenido del subespacio. Así, en la invocación M.5 el compilador encuentra que la versión de f en L.5, concuerda mejor con el argumento actual que la de L.7, así que esta es la definición utilizada. El resultado corresponde a la última salida.
§4.1 La directiva using es transitiva, lo que significa
que si se aplica a un subespacio que a su vez contiene directivas using, también se tienen acceso a todos esos subespacios.
Por ejemplo:
namespace CERO {
using namespace UNO; // UNO ha sido definido previamente
using namespace DOS; // DOS también definido previamente
...
}
...
using namespace CERO; // a partir de aquí son visibles CERO, UNO y DOS
...
Dentro de una clase no puede utilizarse la
directiva using (Accesos a subespacios en clases
4.1.11c1).
§4.2 La directiva using no añade ningún identificador al ámbito local, por tanto,
un identificador definido en más de un subespacio no debe ser un problema hasta que pretenda usarlo. Las declaraciones de ámbito
local tienen precedencia, ocultando todas las demás declaraciones homónimas. Ejemplo:
#include <iostream>
namespace F {
float x = 9;
}
namespace G {
using namespace F;
float y = 2.0;
namespace ANIDADO_G {
float z = 10.01;
}
}
int main() {
using namespace G; /* esta directiva proporciona acceso a todo
lo declarado en "G" (y por tanto en "F")*/
using namespace G::ANIDADO_G; /* esta directiva solo proporciona
acceso al subespacio anidado "INNER_G" */
float x = 19.1; // Este especificador local tiene precedencia
std::cout << "x = " << x << std::endl;
std::cout << "y = " << y << std::endl;
std::cout << "z = " << z << std::endl;
return 0;
}
Salida:
x = 19.1
y = 2
z = 10.01
Nota: el efecto de la directiva using es desde el punto de la declaración hasta el final de la unidad de compilación (lo mismo que la directiva define 4.9.10b), pero con el inconveniente de que el efecto no es reversible (no puede eliminarse). Por esta razón, no es conveniente colocar la directiva using en ficheros de cabecera, ya que de hacerlo así, se corre el riesgo de eliminar inadvertidamente la protección que representa el mecanismo de espacios de nombres.
Además de lo anterior, son muchos los autores que aconsejan no usarla o usarla solo en ocasiones muy puntuales. La razón es que con frecuencia, originan problemas de colisión de nombres ("name clashes"), sobre todo para el principiante. Por ejemplo, si al principio de nuestro código establecemos (como suele ser muy frecuente) la sentencia
using namespace std;
puede ocurrir que con un poco de suerte utilicemos un identificador que haya sido declarado en la citada librería, lo que originaría errores de compilación aparentemente inexplicables. Recientemente, en un foro de Internet alguien solicitaba ayuda porque obtenía una serie de errores de compilación en un programa cuyas primeras líneas eran las siguientes:
#include <iostream>
using namespace std;
template <class T> class pair {
T a, b;
public:
pair (T first, T second) {a=first; b=second;}
T getmax ();
};
...
El resto del código era correcto; el único problema era que el identificador pair ya estaba definido en la librería <iostream>.
§5 Búsqueda en el espacio de los argumentos
Nos referimos a un mecanismo automático de búsqueda que existe implícitamente en todas las funciones C++.
Ocurre con frecuencia, que las funciones están declaradas en subespacios distintos de los objetos que se le pasan como argumentos; en cuyo caso es necesario especificar los subespacios correspondientes al declarar la lista de argumentos formales de la función. Por ejemplo:
void fun1 (ALPHA::CA a, BETA::CB b);
En estas circunstancias, cuando en el cuerpo de la función fun1, aparece una invocación a otra función fun2 no reconocida en el subespacio actual (el de fun1), se realiza una búsqueda automática de dicho identificador en los subespacios de sus argumentos (en nuestro ejemplo se buscaría en ALPHA y BETA).
Recordando que los operadores son en realidad funciones "disfrazadas" en las que los argumentos son los operandos
( 4.9.18), lo anterior
resulta también válido para los operadores; de forma que también se realiza una búsqueda de la definición correspondiente a un
operador en base al tipo de sus operandos. Esta regla resulta muy útil, ya que simplifica la escritura de código; en especial en
los casos de operadores y argumentos de plantillas [3].
Ejemplo: supongamos dos clases en las que hemos definido versiones sobrecargasas de los operadores suma (+) y asignación (=) para los miembros de la clase:
class Vector { ... };
class Complex { ... };
...
void func () {
Vector v1, v2, v3;
Complex c1, c2;
...
v3 = v1 + v2;
c3 = c1 + c2;
}
Las dos últimas expresiones son posibles porque el compilador busca la definición de los operadores + y = en los subespacios Vector y Complex de sus argumentos.
Nota: la búsqueda de nombres de funciones y operadores basada en los argumentos ("argument based lookup") es conocida también por el acrónimo ADL ("argument-dependent lookup") y de Koenig [2] ("Koenig-lookup"), porque fue este miembro del comité el que propuso este esquema de búsqueda.
Ejemplo:
#include <iostream.h>
namespace ALPHA {
class CA { public: int x; };
class CB { public: int y; };
CA Ca1;
// instancia de CA (en ALPHA)
CB Cb1;
// instancia de CB (en ALPHA)
int fun1(CA& c) { return c.x; }
int fun2() { return Cb1.y; }
}
void funG (ALPHA::CA ca); // prototipo (global):
int main (void) { // ========================
ALPHA::Ca1.x = 10;
ALPHA::Cb1.y = 5;
funG(ALPHA::Ca1);
}
void funG (ALPHA::CA ca) { // definición
int x = fun1(ca); // Ok: ALPHA::fun1
int y = fun2(); // Error: fun2 fuera de ámbito
int y = ALPHA::fun2(); // Ok especificador de subespacio necesario
cout << "x = " << x << endl;
cout << "y = " << y << endl;
}
Salida (una vez eliminada la sentencia errónea):
x = 10
x = 5
Temas relacionados:
- Resolución de sobrecarga ( 4.4.1a)
- Búsqueda de nombres ( Name-lookup).
[1] Debido a sus características, la directiva using no debe utilizarse en el diseño de ficheros de cabecera (que a su vez pueden tener otros "includes"), pues polucionarían el espacio global de nombres. En estos casos es preferible utilizar especificadores explícitos de acceso. O como mucho, la declaración using.
[2] Andrew Koenig, miembro del prestigioso AT&T Labs Research y del Comité de Estandarización del C++, ha escrito varios libros sobre la materia, quizás el más conocido "Accelerated C++ (Practical Programming by Example)", del que es coautor junto con Barbara E Moo ( 9.0).
[3] Stroustrup [TC++PL-00] §8.2.6, §11.2.4, Apéndice C §13.8.4.