Organizar codigo en CPP

Esta es una de las maravillas literarias para programadores que he encontrado en la web y no quería dejar de transmitirla, el post original(en ingles) esta aquí y su traducción aca:

http://razonartificial.com/2013/01/organizacion-del-codigo-fuente-en-cpp/

Introducción

Si bien muchos programas simples caben en un solo archivo C o Cpp, cualquier proyecto serio va a necesitar dividir el código en varios ficheros con el fin de ser manejable. Sin embargo, muchos principiantes no se dan cuenta hasta que punto esto es importante (Sobre todo por que muchos lo han intentado y les ha dado más problemas que soluciones y decidieron que no merecía la pena el esfuerzo). En este artículo intentaré explicar porqué hacerlo y cómo hacerlo correctamente. Cuando sea necesario, doy une breve explicación de como trabajan los compiladores y enlazadores para ayudar a entender porque se tienen que hacer las cosas de una manera determinada.

Terminología

En este artículo voy a llamar a los ficheros estándar de C y C++ (normalmente con extensión .c o .cpp) “ficheros fuentes”. Esto será para distinguirlos de los “archivos de cabecera” (normalmente con extensión .h o .hpp). Esta terminología es la usada por Visual C++ y la mayoría de los libros. Ten en cuenta que esta diferencia es puramente conceptual, ambos son archivos de texto con código en el interior. Sin embargo, como se muestra en el resto del artículo, la diferencia de como se trata a unos archivos u otros es muy importante.

¿Por qué dividir el código en varios archivos?

La primera pregunta que los programadores novatos se hacen cuando ven un directorio lleno de archivos de código es ¿Por qué no un solo archivo? No entienden el sentido de separar el código en varios ficheros.

Dividir cualquier proyecto de tamaño razonable tiene algunas ventajas, siendo las más significativas las siguientes:

  • Acelerar la compilación. La mayoría de compiladores trabajan en un solo archivo a la vez. Así que si tenemos 10.000 líneas de código en un solo archivo y cambiamos una línea para compilar nuestro programa necesitamos volver a procesar y compilar 10.000 líneas de código. Sin embargo, si 10.000 líneas de código se distribuyen uniformemente en 10 archivos, si cambiamos una línea solo tendremos que volver a compilar 1000 líneas de código, ahorrando tener que compilar las 9000 líneas restantes del resto de ficheros.
  • Aumenta la organización. Dividir el código en una línea lógica hará el trabajo más fácil para ti (O cualquier programador del proyecto) para encontrar variables, clases, estructuras, funciones, etc. Imagina que usas la función de búsqueda de tu editor para encontrar una parte del código, si tienes un fichero llamado juego.cpp tendrás que buscar a lo largo de todo el código, sin embargo, si tienes tu juego dividido en 4 ficheros del tipo graphics.cpp,main.cppaudio.cpp e input.cpp, si quieres buscar algo relacionado con el sonido sabrás donde buscar reduciendo en 3/4 tu búsqueda.
  • Facilita reutilizar código. Si el código está cuidadosamente dividido y los archivos son independientes unos de otros te permite reutilizarlos en otros proyecto ahorrándote mucho trabajo en el futuro. Hay muchas más cosas necesarias para poder reutilizar el código, pero sin una buena organización es difícil saber que partes van a trabajar juntas y cuales no. Por lo tanto, poner los subsistemas y clases en ficheros independientes ayuda a portar el código a otros proyectos fácilmente.
  • Compartir código entre proyectos. En principio es el mismo tema que la reutilización, pero hay ocasiones que el mismo archivo se usa en varios proyectos en vez de simplemente copiar y pegar, esto facilita que si se corrigen errores o se cambia algo no se tenga que modificar cada copia del archivo y te aseguras de que ambos proyectos utilizan la última versión del código.
  • Dividir las tareas entre los programadores. En proyectos grandes donde trabajan varios programadores no es factible trabajar todos en el mismo fichero, la separación de código facilita que cada uno pueda dedicarse a una parte sin interferir en las demás. Por supuesto, puede darse el caso de que dos quieran modificar el mismo archivo a la vez, para ello existen el software de control de versiones como SVN, GIT, Mercurial…

Todo lo anterior son aspectos de modularidad, un elemento clave tanto en el diseño estructurado como orientado a objetos.

Como hacerlo. Los Fundamentos

Es probable que te estés convenciendo de que hay que dividir el código, ahora la cuestión es cómo hacerlo. Aunque algunas de las decisiones que tome serán razonablemente arbitrarias hay algunas reglas básicas que debería seguir para asegurar el trabajo.

En primer lugar, mirar como dividir el código en secciones. A menudo se separa en subsistemas o “módulos” como el sonido, música, gráficos, manejo de ficheros, etc. Crea nuevos ficheros con nombres significativos para saber a simple vista que contiene, a continuación mueva todo el código que pertenece al módulo a ese archivo. A veces no está claro que pertenece a cada módulo (Algunos te dirían que es por un mal diseño de su software).

Una vez dividido el código en archivos fuentes el siguiente paso es considerar que meter en el archivo de cabecera, en un nivel simple, lo que suele estar en la parte superior del código es un serio candidato a ir al archivo de cabecera, de ahí a que se llamen así.

Los archivos de cabecera suelen incluir algunas o todas de las siguientes cosas:

  • Definición de estructuras y clases.
  • Definición de tipos (typedef).
  • Prototipos de funciones.
  • Variables Globales (ver más adelante).
  • Constantes
  • Macros #define.
  • Directivas #pragma.

(Además, deberías incluir las plantillas y las funciones en línea. Esto debería quedar claro al terminar este artículo).

Por ejemplo, mira cualquier biblioteca estándar que venga con el compilador que uses. Stdlib.h es un buen ejemplo (ver aquí). Te darás cuenta que tiene algunas de las cosas mencionadas en la lista de antes. Del mismo modo puedes ver que las variables globales están precedidas por el modificador extern, esto es importante, pero lo veremos más adelante.

En general será necesario tener un archivo de cabecera para cara archivo de código fuente, si tienes un sound.cpp tendrás un sound.h, si tienes un sprite.cpptendrás un sprite.h. Mantener siempre el mismo orden es importante. Saber lo que va en el archivo de cabecera y lo que va en el archivo fuente.

Estos archivos de cabecera se convertirán en la interfaz entre los subsistemas. Al incluir (#include) un archivo de cabecera en otro archivo tendrás acceso a todas las funciones, clases y tipos declarados en ella. Por lo tanto en todos los archivos que uses sprintes probablemente tengas que incluir sprite.h en cada uno de ellos. Recuerda usar #include “sprite.h” y no #include <sprite.h> para que busque en el directorio del proyecto y no en la biblioteca estándar.

Recuerda que, en lo que al compilador se refiere, no existe ninguna diferencia entre un archivo de cabecera y un archivo de código fuente, para el compilador ambos son archivos de texto que contienen código y tratarán igual, la diferencia es puramente conceptual y para los programadores. Los archivos de cabecera deben contener la interfaz, es decir, la definición de lo que contienen los archivos de código fuentes. Estos son los que contienen las funciones, clases y demás. Esto significa que un archivo fuente utiliza otros archivos fuentes gracias a los archivos de cabecera, estos hacen de enlace entre unos y otros.

Dificultades potenciales

Las reglas anteriores son bastante vagas y sólo sirven como punto de partida para organizar el código. En casos sencillos, se puede producir por completo los programas de trabajo siguiendo las directrices. Sin embargo, hay algunos detalles más que han de tenerse en cuenta, y es a menudo estos detalles los que causan a los programadores noveles tanto dolor, cuando comienzan a dividir su código hasta en los archivos de cabecera y los archivos normales.

En mi opinión hay cuatro errores básicos de los que entran en el turbio mundo de los archivos de cabeceras definidos por el usuario:

  • La fuente de los archivos ya no compila ya que no pueden encontrar las funciones o variables que necesitan. (Esto a menudo se manifiesta en forma de algo parecido a un “error C2065: ‘MyStruct’: identificador no declarado”. En Visual C++, aunque esto puede producir cualquier número de mensajes de error diferentes en función de exactamente lo que están tratando de referencia)
  • Dependencias cíclicas. Cuando dos encabezados tienen la necesidad de incluirse entre sí. Por ejemplo la clase sprite incluye un puntero a la criatura que representa y la clase de la criatura necesita un puntero a la clase sprite. No importa como lo hagas ambas deben ser declaradas antes de usarse, pero a la fuerza cuando una se está declarando la otra no ha sido declarada aún.
  • Duplicar las definiciones. Imagina que tienes un archivo llamado map.h que incluye los archivos sprite.h y hero.h, a su vez hero.h incluye sprite.h por lo que sprite.h se incluiría dos veces en map.h, provocando un error.
  • Duplicado las instancias de los objetos dentro del código que compila bien. Este es un error vincular, a menudo difícil de entender.

Entonces, ¿cómo solucionar estos problemas?

Solución problema 1

Afortunadamente, estos problemas son fáciles de arreglar, y aún más fácil de evitar, una vez que las entienda.

El primer error, en un archivo de origen se niega a compilar porque uno de los identificadores fue declarado, es fácil de resolver. Simplemente incluye (#include) el archivo que contiene la definición del identificador que usted necesita. Si sus archivos de cabecera están organizados de forma lógica y llamado así, esto debe ser fácil. Si es necesario utilizar la estructura Sprite, entonces es probable que tenga que incluir “sprite.h” en todos los archivos que lo hace. Un error que los programadores suelen tener es asumir que es un archivo ha sido incluido porque está incluido en otro archivo ya incluido, un ejemplo.

/* Header1.h */
#include "header2.h"
class ClassOne { ... };

/* Header2.h */
class ClassTwo { ... };

/* File1.cpp */
#include "Header1.h"
ClassOne myClassOne_instance;
ClassTwo myClassTwo_instance;

En este caso File1.cpp compila bien ya que incluye a Header1.h e indirectamente (a través de Header1.h) también incluye a Header2.h, pero en el futuro alguien puede decidir que Header1.h no necesita incluir a Header2.h. Esto hará que se rompa File1.cpp la próxima vez que compiles al no tener la declareción de ClassTwo.

La solución está en incluir siempre los archivos necesarios explícitamente en el archivo que son necesarios y no confiar en que otros archivos incluidos ya lo incluyen, esto además ayuda a entender y documentar el código pues se ve que archivos necesita ese fichero en concreto para funcionar.

Solución Problema 2

Las dependencias cíclicas son un problema común en la ingeniería de software. A menudo se necesita que una clase o estuctura conozca la existencia de otra y esta de la primera, esto termina con el siguiente aspecto.

/* Parent.h */
#include "child.h"
class Parent
{
    Child* theChild;
};

/* Child.h */
#include "parent.h"
class Child
{
    Parent* theParent;
};

Dado que uno de ellos tiene que ser compilado en primer lugar, necesita alguna manera de romper el ciclo. En este caso, de hecho es bastante trivial. La estructura de Parent en realidad no necesita saber los detalles de la clase del Child, ya que sólo se almacena un puntero a una. Los punteros son casi lo mismo no importa lo que apuntan, por lo tanto no es necesario que la definición de la estructura o la clase con el fin de almacenar un puntero a una instancia de esa estructura o clase. Así que la línea #include no es necesario. Sin embargo, sólo tiene que sacarla para tener un “identificador no declarado” error cuando se encuentra con la palabra Child, así que tienes que dejar que el compilador saber que Child es una clase a la que desea apuntar. Esto se hace con una declaración adelantada (forward declaration), tomando la forma de una definición de clase o de clase sin un cuerpo. ejemplo:

/* Parent.h */
class Child; /* Forward declaration of Child; */
class Parent
{
    Child* theChild;
};

Observa como sustituimos la línea #include por la declaración adelantada, igualmente podemos hacerlo en el archivo Parent.h, esto, además, ahorra tiempo de compilación ya que es un #include menos que tratar. Como solo estamos utilizando un puntero y no el tipo en sí no necesita la declaración entera. En el 99% de los casos esto se puede aplicar a uno o ambos archivos para romper las dependencias cíclicas.

Por supuesto, en los archivos fuentes, habrá funciones que se aplican a Parent que modifican también a Child. Por lo tanto es probable que haya que incluirParent.h y Child.h tanto en Parent.cpp como en Child.cpp Click para ver la imagen.

Tenga en cuenta que ser capaz de eliminar completamente la dependencia no siempre es posible. Muchas de las clases y las estructuras están compuestas de otras clases y las estructuras, que es una dependencia no se puede evitar. Sin embargo, siempre que esta dependencia es de un solo sentido, el orden de compilación será fijo y no debería haber ningún problema.

Solución Problema 3

Duplicar definiciones en tiempo de compilación significa que un archivo de cabecera terminó incluyéndose más de una vez en un archivo en particular. Esto lleva a que una clase o estructura se define más de una vez, causando error. Lo primero que debes hacer es asegurarte de que se incluyen solo los archivos necesarios para ese código fuente en particular y quitar todo lo que no utilice.

Lamentablemente, esto rara vez es suficiente, ya que algunas cabeceras se incluyen otras cabeceras. Vamos a revisar un ejemplo de las anteriores, con ligeras modificaciones:

/* Header1.h */
#include "header3.h"
class ClassOne { ... };

/* Header2.h */
#include "header3.h"
class ClassTwo { ... };

/* File1.cpp */
#include "Header1.h"
#include "Header2.h"
ClassOne myClassOne_instance;
ClassTwo myClassTwo_instance;

Por alguna razón tanto Header1.h como Header2.h incluyen a Header3.h, quizás ClassOne y ClassTwo se compenen con funciones de Header3.h. La razón no es importante, pero muchas veces sucede casos como estos en los que al final en File1.cpp se acaba incluyendo dos veces el mismo archivo incluso sin haber una #include a él en el mismo archivo, recuerda que la directiva #include lo que hace es, antes de compilar, copiar todo el contenido del archivo incluido en el archivo actual. así que File1.cpp quedaría de la siguiente manera.

A los efectos de la compilación, File1.cpp termina con copias de Header1.h y Header2.h, los cuales incluyen sus propias copias de Header3.h. El archivo resultante, con todas las cabeceras de expansión en línea en su archivo original, se conoce como una unidad de traducción. Debido a esta expansión en línea, todo lo declarado en Header3.h va a aparecer dos veces en esta unidad de traducción, causando un error.

Así que, ¿qué hacer? No se puede hacer sin Header1.h o Header2.h, ya que se necesita para acceder a las estructuras declaradas en su interior lo que necesita alguna forma de asegurar que, no importa qué, Header3.h no va a aparecer dos veces en el File1.cpp cuando se compila.

Si se miraba stdlib.h antes, te habrás dado cuenta las líneas en la parte superior similar a lo siguiente:

#ifndef _INC_STDLIB
#define _INC_STDLIB

Y en la parte inferior del archivo, algo así como:

#endif  /* _INC_STDLIB */

Esto es lo que se conoce como un “inclusion guard”. Viene a decir que si no está definido _INC_STDLIB defínelo, sino, ve a #endif sería similar al código cuando quieres que algo se ejecute solo una vez.

static bool done = false;
if (!done)
{
    /* Do something */
    done = true;
}

Durante la compilación de File1.cpp, la primera vez que pide que se incluya el archivo stdlib.h, llega a la línea #ifndef y continúa porque “_INC_STDLIB” aún no está definida. La siguiente línea define ese símbolo y lleva a cabo la lectura en stdlib.h. Si hay otra “#include” durante la compilación de File1.cpp, leerá el cheque #ifndef y luego salta a #endif al final del archivo. Esto se debe a todo lo que entre el #ifndef y #endif se ejecuta sólo si “_INC_STDLIB” no está definido, y que se definió la primera vez que lo incluye. De esta manera, se garantiza que las definiciones en stdlib.h sólo son cada vez incluye una vez al ponerlos dentro de #ifndef / #endif.

Esto es trivial para aplicar a sus propios proyectos. Al comienzo de cada archivo de cabecera que usted escribe, escribe lo siguiente:

#ifndef INC_FILENAME_H
#define INC_FILENAME_H

Tenga en cuenta que el símbolo (en este caso, “INC_FILENAME_H”) tiene que ser único a través de su proyecto. Es por esto que es una buena idea de incorporar el nombre del archivo en el símbolo. No agregue un guión bajo al principio como stdlib.h tiene como identificadores precedidos por un guión bajo se supone que son reservados para “la aplicación” (es decir, el compilador, las librerías estándar, y así sucesivamente). Luego se agrega el #endif / * INC_FILENAME_H * / al final del archivo. El comentario no es necesario, pero le ayudará a recordar a que pertenece ese #endif.

Solución Problema 4

Cuando el enlazador trata de crear un archivo ejecutable (o biblioteca) de su código lo que hace es meterlo todo en un archivo objeto (.obj o .o), uno por cada archivo de código fuente y los une. El trabajo principal del enlazador es resolver los identificadores (básicamente, las variables o los nombres de funciones) y convertirlas en direcciones máquina en el archivo final. El problema surge cuando el enlazador en cuentra dos intancias o más de ese identificador en los archivos objetos, entonces no se puede determinar cual es el “correcto” para su uso. El identificador debe ser único para evitar cualquier ambigüedad, Así que ¿Cómo es que el compilador no ve que hay un identificador duplicado y el enlazador si lo ve?

Imagina el siguiente código:

/* Header.h */
#ifndef INC_HEADER_H
#define INC_HEADER_H
int my_global;
#endif /* INC_HEADER_H */

/* code1.cpp */
#include "header1.h"
void DoSomething()
{
    ++my_global;
}

/* code2.cpp */
#include "header1.h"
void DoSomethingElse()
{
    --my_global;
}

La primera se compila en dos archivos objeto, probablemente llamado code1.obj y code2.obj. Recuerde que una unidad de traducción contiene una copia completa de todos los encabezados incluidos en el archivo que está compilando. Finalmente, los archivos de objetos se combinan para producir el archivo final.

Aquí hay una representación visual de la forma en que estos archivos (y su contenido) se combinan:

Note que hay dos copias de “my_global” en ese bloque final. Aunque “my_global” fue único para cada unidad de traducción (esto sería garantizada por el uso de los guardias de inclusión), que combina los archivos objeto generados por cada unidad de traducción se traduciría en que haya más de una instancia de my_global en el archivo. Esto se marca como un error, ya que el enlazador no tiene manera de saber si estos dos identificadores son en realidad una misma, o si uno de ellos era mal llamado justa y que se supone en realidad que es de 2 variables independientes. Así que hay que arreglarlo.

La respuesta no consiste en definir las variables o funciones en los archivos de cabecera en lugar de los archivos fuentes, donde estás seguro que se compilan solo una vez. Esto trae un nuevo problema, ¿Cómo hacer las funciones y variables visibles globalmente si no se encuentra en un archivo de cabecera? ¿De qué manera la pueden “ver” otros archivos? La respuesta es declararlas en los archivos de cabecera, pero no definirlas. Esto permite al compilador saber que la función o variable existe, pero delega el acto de de asignarle una dirección al enlazador.

Para hacer esto para una variable, se añade ‘extern‘ la palabra clave antes de su nombre:

extern int my_global;

El especificador ‘extern‘ es como decirle al compilador que esperar hasta el tiempo de enlace para resolver la “conexión”. Y para una función, simplemente hay que poner el prototipo de función:

int SomeFunction(int parameter);

Las funciones se consideran “extern” por defecto por lo que se acostumbra a omitir el ‘extern’ en un prototipo de función.

Por supuesto, estas son sólo las declaraciones de que my_global y SomeFunction existen en alguna parte. En realidad, no los creas. Todavía tiene que hacer esto en uno de los archivos de origen, de lo contrario aparecerá un error de vinculador nuevo cuando se descubre que no puede resolver uno de los identificadores a una dirección real. Así que para este ejemplo, tendría que añadir “int my_global” a cualquiera de Code1.cpp o Code2.cpp, y todo debería funcionar bien. Si se trata de una función, deberá añadir la función como su cuerpo (es decir, el código de la función) en uno de los archivos de origen.

La regla aquí es recordar que los archivos de cabecera define una interfaz, no una implementación. En ellas se indica que las funciones, variables y objetos existen, pero no se hace responsable de su creación. Ellos pueden decir lo que una estructura o clase debe contener, pero en realidad no debe crear instancias de esa estructura o clase. Se puede especificar los parámetros de una función y se lo devuelve, pero no cómo se obtiene el resultado. Y así sucesivamente. Esta es la razón por la que la lista de lo que puede entrar en un fichero de cabecera al principio de este artículo es importante.

Hay dos notables excepciones a no incluir el cuerpo de las funciones en los archivos de cabecera:

La primera excepción es la de funciones de la plantilla. La mayoría de los compiladores y enlazadores no puede manejar plantillas se definen en archivos diferentes a la que se utilizan en, por lo que las plantillas casi siempre es necesario definir en un encabezado para que la definición se puede incluir en todos los archivos que necesita para usarlo. Debido a la forma de plantillas se crea una instancia en el código, esto no conduce a los mismos errores que se podrían obtener mediante la definición de una función normal en un encabezado. Esto es así porque las plantillas no se compilan en el lugar de la definición, sino que se compilan, ya que son utilizados por el código en otros lugares.

La segunda excepción es las funciones en línea, mencionó brevemente antes. Una función en línea se ha compilado en el código, en vez de llamadas de la forma habitual. Esto significa que cualquier unidad de traducción que ‘llama “el código de una función en línea tiene que ser capaz de ver el funcionamiento interno (es decir, la puesta en práctica) de esa función con el fin de insertar el código de esa función directamente. Esto significa que un prototipo de función sencilla no es suficiente para llamar a la función en línea, lo que significa que donde quiera que normalmente sólo tiene que utilizar un prototipo de función, se necesita el cuerpo de la función general de una función en línea. Al igual que con las plantillas, esto no causa errores de vinculador como la función en línea no es realmente compilado en el lugar de la definición, pero se inserta en el lugar de la llamada.

Otras Consideraciones

Por lo tanto, el código está muy bien dividido en varios archivos, que le da todos los beneficios mencionados en el inicio como la velocidad de compilación de una mayor y mejor organización. ¿Hay algo más que necesitas saber?

En primer lugar, si estás usando la biblioteca estándar de C++ (STL) o cualquier otra biblioteca que utilice espacios de nombres, no utilice la se sentencia “using” en los archivos de cabecera pues reducirás la utilidad de los espacios de nombres casi por completo, es mejor usar el identificador del espacio de nombre en cada función u objeto que uses, así tienes la ventaja de saber a que biblioteca pertenece ese código y no tendrás problemas de superponer nombres de funciones propias que te hagan rectificar el código.

En segundo lugar, el uso de macros debe ser cuidadosamente controlado. Los programadores de C tienen que confiar en las macros de un montón de funcionalidades, pero los programadores de C++ debe evitarlos siempre que sea posible. Si desea una constante en C + +, utilice la palabra clave ‘const’. Si desea una función en línea en C++, utilice la palabra clave ‘inline’. Si desea una función que opera sobre los diferentes tipos utilice plantillas o sobrecarga. Pero si necesitas utilizar una macro, por alguna razón, y lo coloca en un fichero de cabecera, trate de no escribir macros que podrían interferir con el código en los archivos que se incluyen. Cuando se producen errores extraños de compilación, no quiero tener que buscar a través de todos los archivos de cabecera para ver si alguien utiliza un # define para cambiar inadvertidamente su función o sus parámetros a otra cosa. Así que siempre que sea posible, mantener las macros de cabeceras a menos que usted puede estar seguro de que no les importa que afecta a todo en el proyecto.