4.1.11 Espacios de nombres
§1 Introducción:
Como se verá a continuación, los espacios de nombres son en realidad un recurso de C++ para resolver algunos problemas existentes en el C clásico, relativos al manejo de identificadores. Es una de las razones por las que C++ es tan adecuado para la programación de grandes sistemas.
En C, y en algunos otros lenguajes de programación, ocurría con frecuencia que cuando el programa alcanzaba un gran tamaño, empezaban a presentarse problemas de colisión de los nombres (identificadores) asignados a los objetos [3]. Téngase en cuenta que existen aplicaciones con millones de líneas de código con decenas de miles de identificadores, y que además de las librerías externas, pueden comprender centenares de módulos mantenidos por muchos programadores independientes, cada uno de los cuales se ocupa del mantenimiento de unas cuantas miles de líneas de código.
Nota: para minorar estos problemas, el C clásico había establecido implícitamente el concepto de espacio de nombres del Sistema, reservando para este los identificadores que comenzaran con guión bajo "_" ("Underscore").
Los espacios de nombres son posibles porque C++ permite dividir el espacio total de los identificadores de los
objetos del programa (lo que se denomina el Espacio general de nombres
) en subespacios
distintos e independientes. Para ello dispone de una palabra-clave específica: namespace.
En síntesis, el proceso consiste en declarar un espacio de nombres asignándole un identificador y delimitándolo por un bloque entre llaves. Dentro de este cuerpo pueden declararse los objetos correspondientes al mismo. Después, los objetos pueden ser accedidos mediante diversas técnicas; la más directa mediante el operador ::, denominado precisamente por esto "de resolución de ámbito" ( 4.9.19). Ejemplo:
namespace ALPHA {
...
long double LD;
float f(float y) { return y; }
}
namespace BETA {
...
long double LD;
float f(float z) { return z; }
}
ALPHA::LD = 1.1; // Aceso a variable LD del espacio ALPHA
ALPHA::f(1.1); // Aceso a función f de ALPHA
BETA::LD = 1.0; // Aceso a variable LD del espacio BETA
BETA::f(1.1); // Aceso a función f de BETA
BETA::X = 1 // Error: Elemento X
no definido en BETA
En el trozo de código anterior no existe ningún conflicto de nombres con las variables LD o con las funciones
f. Por supuesto, la última línea contiene un error que es avisado por el compilador, ya que intentamos acceder a un miembro
no declarado en dicho espacio.
Nota: a continuación veremos que incluso está permitida la declaración de funciones dentro de tales subespacios , y como las clases y estructuras C++ [2] son en realidad un tipo especial de subespacios con algunas propiedades añadidas (ser cerrados y con capacidad de herencia).
Observe que el mecanismo de subespacios de nombres
no elimina por completo el problema de los identificadores: Aunque marginal, todavía persiste el peligro potencial de la posible
colisión de nombres de subespacios en el espacio global.
§2 Sinopsis
En C/C++, las aplicaciones medianas y grandes se componen de varios ficheros que se compilan separadamente, en lo que se denomina una unidad de compilación ( 1.4.2); después se enlazan para producir un resultado final ( 1.4).
Cada unidad de compilación constituye un espacio, el denominado espacio global de nombres o simplemente espacio global. Por esta razón, a las entidades definidas fuera de cualquier otro subespacio, bloque, o función, se las denomina "globales". En principio los nombres globales de distintas unidades de compilación no deberían presentar colisión entre sí, ya que pertenecen a espacios distintos. Sin embargo hemos señalado ( 1.4.4) que los identificadores tienen una característica, denominada atributo de enlazado, según la cual, los que tienen el denominado enlazado externo, pueden ser visibles desde las demás unidades de compilación (por defecto los nombres de funciones tienen este tipo de enlazado).
Aunque exceptuando los nombres de funciones, esta "visibilidad" no es automática para el resto de identificadores ( 4.1.8d), la organización tradicional de ficheros motivaba que a la hora de enlazar todos los módulos, fuese frecuente el descubrimiento de definiciones de nombres duplicados (sobre todo de funciones). Como solución para los frecuentes problemas de colisión, se añadió a C++ el sistema de espacio de nombres.
Este mecanismo permite compartimentar una aplicación en varios espacios, cada uno de los cuales puede definir y operar dentro de su propio ámbito. El desarrollador es libre de introducir los identificadores que necesite en su subespacio sin preocuparse si tales nombres están siendo usados por alguien más. El ámbito del subespacio es conocido por la aplicación a través de un único identificador. En este sentido, el espacio global o "raíz" [1] es el primer espacio; por esta razón, casi siempre nos referimos a los espacios de nombres como subespacios (del principal).
§3 Espacios de nombres estándar
Hay que tener en cuenta que, incluso sin utilizar explícitamente el recurso de los subespacios, dentro de un mismo ámbito C++ distingue automáticamente cuatro espacios de nombres distintos (que podemos considerar estándar):
- [1] El espacio de nombres de etiquetas ( 4.10.1).
- [2] El espacio de nombres de miembros de clases ( 4.11).
- [3] El espacio de nombres de miembros de estructuras ( 4.5).
- [4] El espacio de nombres de miembros de uniones ( 4.7).
- [5] El espacio de nombres sencillo (que engloba las variables, las funciones, nombres typedef y los enumeradores).
Esta distinción tiene diversas consideraciones prácticas inmediatas. Por ejemplo, miembros de diferentes estructuras pueden
tener los mismos nombres sin conflicto, ya que habitan en subespacios distintos. Por la misma causa, también pueden repetir nombre
con miembros del espacio sencillo, por ejemplo con variables o funciones. Ejemplo:
int x = 1, y = 2, z = 3; // [5]
struct E { int x; int y; int z; } e1 = {1, 2, 3}; // [3]
class C { int x; int y; int z; } c1 = {1, 2, 3}; // [2]
union U { int x; char y; float z; } u1 = { x }; // [4]
func(1, 2, 3);
...
int func(int x, int y, int z) {
if (x == 0) goto x;
return (x + y + z);
x: // [1]
retrun (1 + y + z);
}
En el trozo de código anterior no existe ningún conflicto de nombres con las variables x, y, z del espacio sencillo, de la función func ni de las estructura E; la clase C, o la unión U, ya que por principio todos ellos son espacios distintos para el compilador.
Observe que la unión u1 se inicializa con el valor del int x del espacio sencillo (esto lo deduce el compilador por el contexto). Observe también que dentro de func no existe conflicto entre la variable x y la etiqueta x, ya que ambas pertenecen por principio a espacios de nombres distintos.
§4 El subespacio como ámbito y clasificación
Como hemos señalado, el mecanismo de subespacios de nombres tienen un sentido de ámbito, que permite resolver el problema de colisión de nombres de variables globales, pero además, este mismo sentido permite establecer principios de clasificación (de cualquier tipo) dentro de los elementos de un programa, de formar que puede aportar claridad al código y cierto nivel de seguridad adicional, al permitir ciertas comprobaciones por parte del compilador como veremos más adelante ( 4.1.11c).
Debemos recordar que en programación es importante mantener un principio de clasificación claro y coherente. Tanto para el planteamiento teórico-conceptual de una aplicación (especialmente si es grande), como para la subdivisión concreta de la misma en sus diversas partes; y que en este sentido, el mecanismo de subespacios de nombres es un buen candidato para constituir un primer nivel de clasificación. Este ha sido precisamente el criterio seguido por los propios diseñadores del lenguaje, y la razón por la que las Librerías Estándar de C++ se han desarrollado utilizando un subespacio específico (para usar sus funciones hay que tener en cuenta este detalle 4.1.11c2).
En realidad, cualquier programa C++ de cierto tamaño bien estructurado, debería contar con una adecuada división en subespacios de todos sus elementos (); esta división constituiría un primer nivel de clasificación conceptual entre sus partes, a continuación como subniveles se tendrían las jerarquías de clases y las propias clases.
A excepción de la función main(), que siempre debe pertenecer al subespacio raíz para que pueda ser manejada por el módulo de inicio.
§5 Subespacios, funciones y clases
El mecanismo de subespacios [4] permite incluso la definición de funciones fuera del espacio global de nombres; algo que hasta entonces no estaba permitido ( 4.1.3). Así mismo, veremos que las clases C++ comparten algunas de las propiedades de estos subespacios, tanto en aspectos del acceso como en la posibilidad de incluir funciones.
Para ilustrar la idea anterior, considere que el siguiente ejemplo que compila sin dificultad en C++:
#include <iostream>
using namespace std;
namespace NE {
void fun() { cout << "Soy func del espacio NE" << endl; }
}
class CLASE {
public: static void fun();
class CL {
public: static void fun();
};
};
void CLASE::fun() { cout << "Soy func de clase CLASE" << endl; CL::fun(); }
void CLASE::CL::fun() { cout << "Soy func de clase CL en clase CLASE" << endl; }
void fun() { cout << "Soy func del espacio Global" << endl; }
int main() { // =================
fun();
NE::fun();
CLASE::fun();
return 0;
}
Salida:
Soy func del espacio Global
Soy func del espacio NE
Soy func de clase CLASE
Soy func de clase CL en clase CLASE
[1] El mecanismo C++ de espacio de nombres proporciona incluso la posibilidad de acceder de forma específica a elementos de este espacio raíz ( 4.1.11c).
[2] Las estructuras C++ son en realidad clases con ciertas propiedades específicas ( 4.5a1).
[3] Este problema es conocido por las siglas GNSP "Global Name Space Pollution".
[4] El mecanismo de subespacios de C++, es una adición reciente que no aparecía en las primeras versiones del lenguaje (Stroustrup TC++PL-00). Fue introducido con la versión del Estándar de Julio de 1998, y es un concepto importado de otros lenguajes como Lisp, Módula 2 y Ada.