El lenguaje de programación C++: Constantes de cadena

Ahora sí, sigamos con el manual de C++ de Bjarne Stroustup. Este post se centra sobre un aspecto curioso de los literales (o constantes) de cadena.

Tanto en C como en C++, una constante de cadena es un texto encerrado entre comillas dobles. Como esto:

"v for vendetta"

Internamente, la cadena se representa cono un array de 15 elementos char: 14 carácteres + 1 carácter nulo, el famoso o caracter nulo que C y C++, así como otros lenguajes, añaden al final de la cadena para indicar dónde termina.

Así que una constante de cadena es como un inicializador de un vector y más azúcar sintáctica. Comparad estas dos inicializaciones:

char texto[] = "vendetta"; // parece fácil e intuitivo
char texto2[] = {'v', 'e', 'n', 'd', 'e', 't', 't', 'a', '\0'}; // más rebuscado y bastante menos intuitivo

Constantes de cadena

Ahora, si os acordais de la entrada sobre vectores y punteros, en ella citábamos el siguiente párrafo del manual de C de Kernighan y Ritchie:

Existe una importante diferencia entre estas definiciones:

char amessage[] = “ya es el tiempo”; /* arreglo */
char *pmessage = “ya es el tiempo”; /* apuntador */

amessage es un arreglo, suficientemente grande como para contener la secuencia de caracteres y el que lo inicializa. Se pueden modificar caracteres individuales dentro del arreglo, pero amessage siempre se referira a la misma localidad de almacenamiento. Por otro lado, pmessage es un apuntador inicializado para apuntar a una cadena constante; el apuntador puede modificarse posteriormente para que apunte a algun otro lado, pero el resultado es indefinido si trata de modificar el contenido de la cadena.

Ojo a la negrita. El inicializador es el mismo: «ya es el tiempo»; pero si lo asignamos a un vector obtenemos una fracción de la memoria modificable mientras que si se lo asignamos a un puntero, el resultado depende de la implementación.

¿Por qué?

Bueno, yo no soy ningún experto para responder correctamente aquí pero me temo que es por optimización de almacenamiento. Digo yo que K.&R. dejaron el comportamiento indefinido para los punteros para que la implementación decidiese qué era mejor en estos casos. Por ejemplo:

char *s1 = "Remember, remember, the fifth of novemeber";
char *s2 = "Remember, remember, the fifth of novemeber";
if (s1==s2)
    printf("s1 y s2 apuntan al mismo sitio!"); // depende de la implementación

Depende de la implementación si reservar dos espacios de memoria distintos para cada una de las constantes de cadena por lo que, tras la inicialización, s1 y s2 serían distintos o reservar uno sólo y hacer que s1 y s2 apuntaran al mismo elemento.

Sin embargo, en C++ esto no es así. En C++, un literal de cadena siempre tiene tipo const char[], es decir, es un vector de caracteres constantes. Siempre, tal y como se especifica en el manual de C++ en la sección 5.2.2:

El tipo de un literal de cadena es un «array del número adecuado de caracteres const»,  por lo que «Bohr» es un tipo de const char [5].

Aquí, la palabra clave const avisa al compilador de que no debe consentir expresiones que puedan alterar el contenido de las variables char que forman el vector. De esta manera, expresiones como:

const char v1[] = "rendetta";
v1[0] = 'V'; // ¡ERROR DE COMPILACIÓN! Los elementos del array son constantes

están prohibidas.

Podríamos pensar que una forma de saltarnos la comprobación de const es utilizar la siguiente construcción:

const char v1[] = "rendetta";
char *v2 = v1; // Puntero a caracteres (no a caracteres constantes)
v2[0] = 'V'; // ¡ERROR DE COMPILACIÓN! La implementación se guarda de este tipo de situaciones comprobando la inicializacion de los punteros

pero no funcionará. No sólo const avisa al compilador de que no permita que el contenido de la variable se modifique sino que además tambien se controla que, en una inicialización, no se pierdan protecciones como ocurriría en el ejemplo si el compilador no diera ningún error.

No obstante, y he aquí el objeto de la crítica, la siguiente expresión está permitida:

char *s = "¿la excepción confirma la regla?"; // el texto entre comillas tiene tipo const char[] y sin embargo esto no producirá ningún error

C++ no deja opción a la implementación, los caracteres de un literal de cadena son constantes. Esa porción de memoria debería de estar protegida por la implementación. Y así es. El siguiente código compilará pero elevará una excepción en tiempo de ejecución cuando se ejecute la línea que modifica el contenido del vector.

char *s = "¿la excepción confirma la regla?"; // el texto entre comillas tiene tipo const char[] y sin embargo esto no producirá ningún error
s[5] = 'X'; // ¡ERROR! Pero en tiempo de ejecución, la compilación terminará sin problemas

Mi error tiene la siguiente pinta:

Access violation writing location 0x00c26be5.

En mi opinión esto es un fallo puesto que errores que podrían detectarse en tiempo de compilación pasan a ser ahora motivo de excepción en ejecución (algo considerablemente más grave). Como dice Stroustup también en la sección 5.2.2:

Se puede asignar un literal de cadena a un char*. Esto se permite porque en definiciones anteriores del C y C++, el tipo de un literal de cadena era un char*. Al permitir la asignación de un literal de cadena a un char* se asegura que millones de líneas de código C y C++ sigan siendo válidas.

La verdad, pienso que no era tan problemático forzar la comprobación de todas esas millones de líneas o buscar tales declaraciones y añadir automáticamente un const delante de cada una de ellas. La retrocompatibilidad está bien mientras tu lenguaje extienda al anterior. Mientras las viejas abstracciones sigan encajando en las nuevas. Cuando un lenguaje moderno cambia la definición de un concepto, mantener la compatibilidad resulta un lastre la mayoría de las veces.

K.&R. permitían a la implementación decidir si un literal de cadena podía modificarse; Stroustup dice claramente que no. Entonces, por qué permitir expresiones como las que acabamos de ver. También Stroustup nos dice que cuanto más C sabe un programador, peor programa en C++ luego cabría pensar que lo correcto era forzar las declaraciones de la forma const char *, para que nos diéramos cuenta de las debilidades de C y se nos fueran pegando las nuevas y buenas costumbres.

Y vosotros ¿qué opinais? ¿se os ocurren más motivos por los que los literales de cadena sean constantes?

2 comentarios en “El lenguaje de programación C++: Constantes de cadena

  1. A mí me sigue pasando a veces cuando escribo en C++, por deformación del C imagino. No le encuentro ahora mismo una justificación al por qué de hacer constantes los literales de cadena de manera implícita. Si a alguien se le ocurre, que lo diga.

    1. Buenas, hace ya un par de añitos que pusiste esto, yo la verdad es que ahora me pregunto lo mismo y me preguntaba si lo habría descubierto e incluso si sabe alguna forma de declarar un * char sin que sea constante.
      Gracias.

Deja un comentario