Querido Ndugu,
Hoy los programadores de C++ estamos de enhorabuena. El famoso nuevo estándar de C++, conocido entre los amigos como 0x tiene ya el primer borrador completo. El nombre, 0x, era una especie de promesa de la gente del comité que hace años asumió la responsabilidad de tenerlo listo antes de 2010 (la ‘x’ viene a ser el comodín que sería sustituido por el año de publicación, por lo que el estándar tendría la forma C++ 0x y nunca C++ 1x…).
Hace por lo menos dos años que le vengo siguiendo la pista a las reuniones del comité, a las propuestas y a los documentos generados sobre los estudios de las nuevas características que estaban planteándose añadir, y me alegro de que por fin se acerque el momento de poder utilizar el “auto” especialmente en la declaración de variables con tipos parametrizados, los “conceptos” que mejorarán los mensajes de error dados por el compilador, y otro montón de cosas que iban siendo necesarias en C++.
El borrador tiene nada más y nada menos que 1352 páginas (que espero que no tengan en un único documento en Word
). Es un documento que merece la pena conservar aunque la misma portada reconoce que son conscientes de que está incompleto y contiene errores. Pero merce la pena, digo, conservarlo al menos hasta que haya otra versión de borrador más moderna, porque puede que, igual que el documento del estándar anterior, haya que pagar por la versión definitiva.
No es que me de por leerme el estándar de C++ en mis momentos ociosos, pero alguna vez he recurrido a él cuando hacía cosas un poco conflictivas. Y siempre queda bien resolver la cuestión con la frase concreta del estándar que te resuelve la duda… si alguien te viene diciendo que no le funciona, le puedes soltar que su compilador no cumple el estándar, y que la culpa no es tuya
Siempre he creído que trabajar en un comité de estandarización no debe ser nada fácil. Este borrador lo refleja claramente. Pero por si sois perezosos y ni siquiera abrís el enlace para intentar entender el soporte de las hebras (si pasáis de la primera página de la sección habiéndolo entendido, decidlo
), contaré algo yo sobre esto.
En el desarrollo del estándar hay que intentar fijar unas normas de tal forma que sean tan claras que no dejen lugar a dudas sobre lo que quieren decir, ni dejen cabos sueltos que abra las puertas a futuras incompatibilidades entre los implementadores.
A eso hay que añadir la gran cantidad de conflictos de intereses contra los que algunos estándares tienen que luchar, las sugerencias o ideas que reciben los comités, las presiones de tiempo y un largo etcétera.
El libro “The design and evolution of C++” es una buena fuente para darse cuenta de todos estos aspectos, pues describe la experiencia desde dentro (los ejemplos que aquí describo están sacados del libro). Con él se aprende que gran parte del proceso de estandarización consiste en “encontrar la forma de expresar completa y claramente lo que todo el mundo sabe pero resulta no estar escrito en ningún manual”, en aclarar aspectos oscuros que afectan a un pequeño número de programadores, pero que son vitales para los implementadores de los compiladores que en un momento dado se enfrentan al problema de decidir cómo compilar un ejemplo concreto de código.
El libro dedica unas cuántas páginas a resolver el problema de estandarización del método para determinar con exactitud a qué declaración corresponde un nombre cuando se declara una clase. Por ejemplo, ¿a qué x se refiere la implementación de X::f() en el siguiente código?:
int x;
class X {
int f() { return x; }
int x;
};
Cualquiera que haya programado un poco en C++ contestará (correctamente) que se refiere a X::x. Podríamos incluso ser capaz de deducir una regla (incorrecta, como veremos) del tipo “en la definición de las clases, los símbolos definidos dentro de ella están disponibles también para las declaraciones anteriores a la propia definición del símbolo”.
Entonces, ¿qué ocurre con este código?
class T {
A f();
void g() { A a; /* ... */ }
typedef int A;
}
Con la regla anterior debería compilar, pues A está definido dentro de la clase T que lo utiliza… pero no. No compila. Y no compila porque en realidad, el primer ejemplo compilaba por otra regla distinta, una cuyo nombre técnico es “regla de reescritura” y que viene a decir que las funciones miembro que se declaran inline deben ser analizadas como si estuvieran definidas inmediatamente después del final de la declaración de la clase. Con esta regla, la implementación de X::f() del primer ejemplo se analiza “después” de analizar toda la clase, y en particular, su atributo X::x.
Y según esta regla, este segundo ejemplo no compila cuando analiza la línea A f(), ya que el tipo A no está definido, pero no da error en la función inline ya que, aplicando la regla de reescritura, al final de la clase A ya está definido.
La regla de reescritura, no obstante, introduce un nuevo problema, a saber: ¿a qué T se refiere Y::f() en el siguiente trozo de código?
typedef char *T;
class Y {
T f() { T a = 0; return a; }
typedef int T;
};
El problema surge debido a que la implementación anterior utilizando la regla de reescritura es equivalente a (o mejor dicho, se compila como):
typedef char *T;
class Y {
T f();
typedef int T;
};
inline T Y::f() { T a = 0; return a; }
De tal forma que en la definición de Y::f() (dentro de la clase) ésta devuelve un char *, pero en la declaración/implementación posterior, pasa a devolver un entero.
Pues bien, para evitar esto, se introduce una regla más, conocida como la regla de la redefinición del tipo, que dice que un tipo no puede ser redefinido en una clase después de su primer uso en ella. Por lo tanto, el error que da el compilador es algo como “error en la declaración typedef int Y::T, por cambio de significado” (es curioso comprobar que esta regla no se tiene en cuenta en Visual Studio, al menos en las versiones 2003 ni 2005…).
Volviendo al proceso de estandarización, estas dos reglas ya estaban claras cuando el proceso de estandarización de C++ comenzó. En principio, parecían suficientes para cubrir todos los casos pero ahí es donde entra el trabajo de la gente del comité que se dedica a busca casos esotéricos que no sean cubiertos por las reglas. Son fragmentos de código que uno no se encuentra en su quehacer diario pero que, cuando alguien se enfrenta al problema de programar un compilador, debe resolver. Si se utilizan únicamente las dos reglas anteriores, no está claro qué se debe hacer.
El ejemplo que aparece en el libro y que refleja que el trabajo de una de estas personas puede llegar a ser perjudicial para su salud mental es el siguiente (tengo que reconocer que me costó bastante entender qué quería decir…):
typedef int P();
typedef int Q();
class X {
static P(Q); // equivalente a static P Q; es decir
// la definición del símbolo Q del tipo
// P, es decir, función que devuelve un int
// Q por tanto ya no es el typedef
static Q(P); // definición de Q como una función que
// recibe como parámetro a P y que devuelve
// un entero (int implícito heredado de C).
};
Si obviamos que finalmente eliminaron la posibilidad de omitir el int en el tipo devuelto por una función, el código anterior era válido; “simplemente” declara dos funciones Q con tipos distintos en los parámetros. Sin embargo, si le damos la vuelta a sus declaraciones, estaríamos definiendo dos funciones P. Si quitamos alguno de los typedef tenemos otros significado…
Un ejemplo más sencillo que tampoco era cubierto por las reglas anteriores es:
int b;
class Z {
static int a[sizeof(b)];
static int b[sizeof(a)];
};
En este caso, también debería resultar en un error pues b cambia de significado después de su primer uso (aunque no cambia en realidad el significado de ningún tipo).
Espero que estas secciones de código y sus explicaciones hayan sido suficientes para ejemplificar lo duro que debe ser trabajar en uno de estos comités.
Por terminar el post, pongo aquí las reglas que finalmente adoptó el comité en Marzo de 1993:
- El ámbito de un nombre declarado en una clase es no sólo el texto que sigue a la declaración de ese nombre, sino también el cuerpo de todas las funciones, argumentos por defecto e inicializadores de constructores en esa clase (y sus clases anidadas). Obviamente se excluye en la propia definición.
- Un nombre
N utilizado en la declaración de la clase S debe evaluarse de igual manera en el contexto que aparece y en el ámbito completo de S, entendiendo como ámbito completo la clase S, sus clases base y todas las clases que la encierran.
- Si reordenando la declaración de miembros conduce a un programa válido bajo las reglas dos reglas anteriores, el comportamiento del programa no está definido.