Todo lo que un programador de C debería saber sobre el comportamiento indefinido

Hace unos días descubrí un enlace al blog del proyecto LLVM (del que hablaremos en otro momento) sobre el comportamiento indefinido de C, sus ventajas e inconvenientes. Me parece muy interesante de cara a producir código rápido y portable. De hecho, me gustó tanto que voy a colgaros su traducción tras estas líneas. No obstante se trata de una serie de tres artículos así que comienzo por el principio con este primer post que habla sobre las ventajas de no asumir ciertos comportamientos en C y por qué esto supone una ventaja en cuanto a optimización.

Seguir leyendo «Todo lo que un programador de C debería saber sobre el comportamiento indefinido»

z-scheme

Llevo un tiempo desaparecido, lo sé. Pero no ha sido en balde. Me ha dado por aprender Perl (sigo en ello) y he comenzado con un proyecto cuyo desarrollo me gustaría compartir.

La idea lleva rondándome la cabeza durante un tiempo y consiste en desarrollar un esquema de programación en C con el que parezca que manejas objetos clásicos de la POO pero manteniendo la legibilidad de los programas imperativos (ver paradigmas de programación para más información).

Implementarlo pasa por definir un pequeño lenguaje propio que sería consumido por un traductor programado en Perl y encargado de traducirlo a C puro. Mi intención es mantener el código C lo más práctico y fácil de seguir posible. No pretendo reemplazar GObject ni competir con Vala o Genie que son intérpretes de lenguajes  como C# y Python a GObject, respectivamente.

Con práctico me refiero a que el programador debería poder continuar con el desarrollo del programa en su forma C, siguiendo el esquema planteado mientras que con fácil de seguir quiero decir que la depuración no debería resultar compleja puesto que el esquema a de resultar intuitivo.

¿Por qué este nombre? Bueno, Z ya estaba pillado (sí, también es un lenguaje de programación) pero me gusta por su relación en español con la letra C. Así pues le añado la palabra scheme para remarcar el hecho de que no es un lenguaje, sino un esquema de programación.

Y hasta aquí la presentación. En breve, más.

El lenguaje de programación C++: conclusiones sobre las referencias

En los últimos post, referencias y cuándo usar referencias, hemos hablado sobre esta construcción del lenguaje C++.

En C++ una referencia no es algo trivial como ocurre en Java o en C#. En C++ una referencia es otra herramienta junto con las variables y los punteros para gestionar la memoria.

Siguiendo con la idea original de realizar una crítica al libro de Stroustup… ¿qué podemos concluir? ¿se explica mal el texto? ¿resulta confuso o difícil de entender? A mi juicio, no. Todo está bastante claro.

Si bien, el único punto remarcable es el por qué sólo se permiten literales en referencias constantes. Acerca de esto, el texto de Stroustup, en el aparatado 5.5, afirma:

Las referencias a variables y las referencias constantes se diferencian debido a que la introducción de un temporal en el caso de la variable es altamente conducente a errores; una asignación a la variable se convertiría en una asignación al temporal, que desaparecerá próximamente. No existe dicho problema para las referencias a constantes, y las referencias a constantes suelen ser importantes como parámetros de función (…).

En mi opinión era más fácil explicarlo como un problema de incompatibilidad de tipos (los literales son const luego la referencia debe ser const) pero esto no es lo que Stroustup quiere decir con el párrafo anterior. De hecho, yo tardé bastante en entenderlo y no fue hasta mucho después, al final del aparatado 7.2, que me dieron un argumento para entenderlo:

La prohibición de las conversiones para parámetros por referencia no-const (…) evita la posibilidad de errores tontos derivados de la introducción de temporales. Por ejemplo:

void actualiza(float &i);

void g(double d, float r)
{
actualiza(2.0f);    // error: parámtro const
actualiza(r);       // pasa por referencia r
actualiza(d);       // error: se requiere conversión de tipos
}

Si se hubiesen permitido estas llamadas, actualiza() habría actualizado silenciosamente temporales que se borrarían inmediatamente. Habitualmente, esto sería una sorpresa desagradable para el programador.

El caso de actualiza(2.0f) hubiese resultado un tanto extraño.

¿Cómo narices actualizas un literal? Es más, ¿qué significa actualizar un literal?
O____O

Ahí la primera inconsistencia. El temporal se hubiese creado, hubiera tomado el valor 2.0 y luego podría haberse modificado en la función pero… ¿cómo se transmitiría ese cambio fuera de la función? Opino que lo más sencillo hubiera sido aludir a la incompatibilidad de tipos.

En el caso de actualiza(d), se hubiera creado el temporal e inicializado (por copia del valor, no enlace) con el valor de float(d). Luego la referencia i se hubiese enlazado con el temporal pero no con el parámetro d. Los accesos a i hubiesen resultado en accesos al temporal por lo que tras modificarse en el cuerpo de la función y destruirse al cabo de la misma, d habría quedado invariable.

¿Se podría haber solucionado de otra forma? ¿Es la introducción de temporales la mejor forma de lidiar con estas situaciones?

No lo sé, pero sí sé que la explicación por incompatibilidad de tipos era consistente y mucho más sencilla.

El lenguaje de programación C++: ¿cuándo usar referencias?

Seguimos con referencias. Ya sabemos qué son pero ¿cuándo usar las referencias?

La respuesta corta es siempre… que puedas.

La respuesta larga tiene que ver con el uso de punteros y se podría replantear de la siguiente manera ¿cuándo debo usar un puntero y cuándo una referencia?

Veamos algunos casos típicos y recomendaciones:

Seguir leyendo «El lenguaje de programación C++: ¿cuándo usar referencias?»

El lenguaje de programación C++: referencias

Continuamos con la serie de artículos dedicados a darle caña a C++. Hablamos hoy de las referencias y de su significado.

Una definición genérica e intuitiva de referencia podría ser la de una construcción del lenguaje que nos permite acceder a una estructura de datos.

En Java por ejemplo, casi todo lo que entendemos como «declaraciones de variables» son realmente referencias. Sólo son verdaderas variables aquellos identificadores cuyo tipo es primitivo (a saber: boolean, char, byte, short, int, long, float y double).

double g = 9.8; // Aquí g es una variable clásica
g.isInfinite(); // No tine sentido, g no es una estructura de datos, es una variable
Double r = new Double(9.8); // Aquí r es una referencia a un objeto Double. En Java, por convenio, las clases de objetos comienzan por una letra mayúscula
r.isInfinite(); // Esto sí tiene sentido, como referencia, r nos permite acceder al objeto

Referenciar no tiene por qué estar ligado a la programación orientada a objetos. Desreferenciar un puntero a una estructura, en C, puede verse como un acto referencial a la estructura apuntada por el puntero:

struct vector { int x; int y; } v;
struct vector *pv = &v;
(*pv); // La construcción (*pv) es una referencia a v
(*pv).x = 0;
pv->y = 1; // La flecha es una forma abreviada de desreferenciar y acceder a un campo. Más azúcar sintáctico.

Hasta aquí parece fácil. Veamos por qué la cosa se complica en C++.

Seguir leyendo «El lenguaje de programación C++: referencias»

Tamaño de los vectores en C

En el post sobre copia de vectores, en los comentarios, se dice que la macro debería comprobar los tamaños pero… ¿es esto posible?

La respuesta es y es gracias al operador en tiempo de compilación sizeof.

Este operador devuelve el tamaño de un objeto o tipo en bytes. Así que podemos construir una macro muy sencilla que nos de el número de elementos de un vector:

#define vsize(v) (sizeof((v))/sizeof((v)[0]))

Ahora bien, muy importante, esta macro no funcionará con punteros. El tamaño de un puntero es el tamaño de la palabra (conjunto de bytes) usado para direccionar una posición de memoria: en máquinas de 32bits es de 4bytes y en máquinas de 64bits, de 8bytes. Así que nuestra macro no se comportará correctamente.

Con esto en mente se podría reescribir la macro de copia para que tuviese en cuenta el tamaño. La tenéis aquí y actualizada en la entrada original:

#define vsize(v) (sizeof((v))/sizeof((v)[0]))
#define arraycp(type, target, source, offset, count)\
  {\
      int i, ts, ss, c;\
      type *t; ts = vsize(target);\
      type *s; ss = vsize(source) - offset;\
      c = (count);\
      for(i=0, t=(target), s=(source)+(offset);\
          i<c && i<ts && i<ss;\
          ++i, *t++=*s++)\
        ;\
  }

¿Comentarios, mejoras?

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

Seguir leyendo «El lenguaje de programación C++: Constantes de cadena»

La verdad sobre vectores y punteros

Para poder seguir dándole caña al libro de Stroustup tengo que contaros o recordaros antes la verdad sobre vectores y punteros en C, que no son lo mismo.

Es común en los cursos de programación dar a entender que el nombre de un vector es un puntero a su primer elemento o que un vector es un tipo especial de variable pero lo primero es casi cierto y lo segundo, falso.

Lo primero que hay que dejar claro es que el identificador de un vector no es una variable; sólo le da nombre a un conjunto de «variables consecutivas» pero no podemos guardar nada en él. Nos lo dicen Kernighan y Ritchie en su manual de C, apartado 5.3 (aquí pa es un puntero a un array y a es un array):

Existe una diferencia entre un nombre de arreglo y un apuntador. que debe tenerse en mente. Un apuntador es una variable. Por esto pa = a y pa++ son legales. Pero un nombre de arreglo no es una variable; construcciones como a = pa y a++ son ilegales.

Por ejemplo, el siguiente fragmento de código no tiene sentido:

int v1[] = {1,0,1};
int v2[] = {2,0,2};
v2 = v1; // no se puede copiar un vector en otro, v1 y v2 son nombres no variables, no guardan nada, sólo designan espacios
v2++ // lo mismo, esta expresión es como escribir v2 = v2 + 1

El identificador v2 no puede ser reasignado, por tanto, como decían K. & R. expresiones como v2++ o v2-=1 no son válidas. Este hecho se precisa más adelante en el mismo libro, sección 5.5:

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.

Precisamente del hecho de que los vectores no se puedan reasignar y que los punteros sí, se deriva el que los vectores no sean punteros. No obstante el identificador de un vector sí que es, por definición, la dirección del primer elemento del vector.

Por lo anterior, las siguientes expresiones sí que tienen sentido:

int v[] = {1,0,1};
int *pi, *pi2;
pi = v; // pi apunta al primer elemento del vector v
pi2 = &v[0]; // ahora pi y pi2 apuntan al mismo sitio

Ahora, lo que sí puede tratarse como una variable son cada una de las posiciones del vector mediante la notación de indexación con corchetes:

int v[] = {1,0,1};
v = 0; // ¡ERROR! Esto es ilegal, v no es una variable
v[1] = 10; // Esto es legal, la posición 1 de v se puede tratar como a una variable

Su relación con los punteros se vuelve trivial cuando se conoce que la indexación de un vector es azúcar sintáctica para evitar construir una expresión de la forma:

y = *(vector+1); // referencia al segundo elemento del vector

En vez de esto, ponemos sencillamente:

y = vector[1]; // lo mismo, pero menos engorroso

Es azúcar porque así nos lo dicen K. & R. en la sección 5.3:

Al evaluar a[i]. C convierte inmediatamente a *(a + i); las dos formas son equivalentes.

En síntesis podríamos decir que:

  1. El identificador de un vector no puede aparecer a la izquierda de una asignación, un puntero sí.
  2. El identificador del vector es, por definición, la dirección al primer elemento.
  3. Podemos acceder a los elementos del vector bien con la notación de punteros, bien con la notación de los corchetes. Son equivalentes.

Con esto espero haber resaltado las diferencias y similitudes entre vectores y punteros. El siguiente post sobre el libro de C++ de Stroustup hablará sobre un asunto relacionado con esto y los literales de cadena.

El lenguaje de programación C++: Declaraciones y One Definition Rule

El capítulo 4 del libro El Lenguaje de programación C++ versa sobre los tipos y declaraciones. Como comentaba ayer, ya en este capítulo se presentan algunos problemas al explicarnos características típicas de C++ como la conocida One Definition Rule o Regla de la Definición Única que resulta de especial interés para los más novatos y que al entenderla como se debería ahorra muchos dolores de cabeza derivados de la compilación separada de módulos.

Seguir leyendo «El lenguaje de programación C++: Declaraciones y One Definition Rule»

El lenguaje de programación C++ (versión comentada)

El lenguaje de programación C++ es un libro escrito por el creador de C++, Bjarne Stroustrup, en el que, según puede leerse en su contraportada:

El libro presenta el lenguaje C++ al completo: desde una visión global de sus conceptos y características básicas y su biblioteca estándar hasta todos los aspectos del diseño y desarrollo de tecnicas específicas de programación en C++.

No es que me entere ahora, por supuesto; de hecho, el libro lo adquirí en 2005 pero no ha sido hasta ahora que he decidido reconciliarme con C++ que lo he vuelto a abrir.

¿Y por qué reconciliarme con C++? Bueno, porque, sin lugar a dudas, se trata de un lenguaje importante y muy popular y bastante necesario para entrar en (por ejemplo) la industria del videojuego sin ir más lejos. Además, de utilizar Java, C# y Python le había cogido tirria a su sintáxis (de hecho, sigo pensando que es confusa y ambigua).

Llevo ya un par de capítulos del librito (son más de 1000 páginas) y conforme leo me convenzo más de mi juicio original. Aunque quizá también tenga que ver el cómo el señor Stroustrup nos enseña las distintas características del lenguaje, cometiendo contradicciones a cada página que no son más que montones de «excepciones a la regla» y vestigios de la «compatibilidad con C».

No obstante, para quien conozca C, el libro es un buen manual de programación y a lo largo de esta nueva serie voy a resaltar, comentar y tratar de corregir cada aspecto ambiguo o contradictorio que descubra en el libro por el bien del equilibrio mental de quienes, como yo, decidan seguir el camino de C++. Serán comentarios cortos, dónde se describirá un problema concreto y se tratará de arrojar luz sobre el mismo, para que sirvan de pequeñas lecciones individuales.

A favor del señor Stroustrup, de su lenguaje de programación y de su libro he de decir que cuanto más se sabe de C, peor se tiende a programar en C++ o mejor dicho, peor se aprovecha C++ tal y como él afirma diciendo:

Cuanto más C se sabe, más difícil parece evitar escribir C++ al estilo de C, prediendo por tanto algunos de los beneficioes potenciales de C++.

Como sé que C++ tiene un ejército de defensores, espero que haya movimiento en los comentarios. Una esperanza vana viendo la cantidad de visitas diarias pero oye, por intentarlo…

Bueno, primeras notas en el próximo post.