La directiva de preprocesador #include se usa en los lenguajes C y C++ para “incluir” las declaraciones de otro fichero en la compilación. Esta directiva no tiene más misterio para proyectos pequeños. En cambio, puede ayudar aprovechar bien esta directiva en proyectos con un gran número de subdirectorios.

El efecto de #include

Cuando el preprocesador encuentra una línea #include "fichero", entonces reemplaza esta línea por el fichero incluido. Así procede con todas las directivas de inclusión – también en aquellas anidadas en los fichero ya a su vez incluidos. Es decir, existe un sólo fichero grande tras la precompilación.

No obstante, esta unión de varios ficheros no tiene lugar físicamente. Lo que sucede es que se interrumpe la compilación del fichero actual, se compila el fichero incluido y, tras compilarlo, se continúa con el primero. Por eso, el compilador puede decirnos, en qué fichero tuvo lugar un error de compilación.

En cambio, conviene tener esta idea del fichero único en mente, porque a veces ayuda a encontrar errores. Uno muy típico es olvidarse el punto y coma tras la declaración de una clase.

En este caso hay una declaración de clase en el fichero incluido:

class MiClase {}

En el segundo puede haber algo así:

#include "mi_clase.h"
MiClase miInstancia;

En este ejemplo, el compilador se quejará de que aquí no se puede definir un nuevo tipo en la línea de MiClase miInstancia aunque esta línea es correcta. El error verdadero es la falta del ; en el fichero incluido. Lo que el compilador realmente ve es

class MiClase {} MiClase miInstancia;

No obstante, el programador no lo ve, porque el código está distribuido sobre dos fichero y el error se produce en el correcto.

La precompilación sólo modifica el código a nivel textual. No entiende del sintaxis del lenguaje. Por eso es posible distribuir el código de forma arbitraria. Por ejemplo, el siguiente ejemplo compilaría.

Fichero incluido

{
	int

Fichero principal

void main(void)
#include "fichero_incluido"
	a;
}

Esto es así porque el compilador ve el conunto

void main(void)
{
	int
	a;
}

La posición del #include

Lo habitual es posicionar las inclusiones al inicio de cada fichero. Esto tiene sentido, porque se suele requerir declaraciones básices antes de declarar clases más complejas. Mi lugar preferido en los ficheros de cabecera es tras el guardián de inclusión múltiple.

// Guardian de inclusión múltiple
#ifndef FICHERO_YA_INCLUIDO
#define FICHERO_YA_INCLUIDO

#include "declaraciones_basicas.h"

#endif

Así se evita que un compilador poco sofisticado abre otra vez el mismo conjunto de ficheros cuando se inluye un fichero de cabecera dos o más veces.

En los ficheros de definición (los .c o .cpp), los guardianes de inclusión múltiple no hacen falta. No obstante, puede haber una restricción importante cuando se usan cabeceras precompiladas. En este caso, todos los ficheros fuente deben incluir primero el mismo fichero de cabecera – que es aquello que define la cabecera precompilada. El compilador de C++ de Borland permite varios ficheros de cabecera para la definición de una cabecera precompilada. Estas inclusiones deben ser lor primeros ficheros incluidos y se deben incluir en el mismo orden.

Puede darse el caso de no poner las inclusiones en el inicio de un fichero. Esto es frecuente en los fichero que se podrían denominar “programados en directivas de precompilación”. Normalmente se trata de ficheros de cabeceras con definiciones muy básicas como ajustes a la plataforma empleada. Por ejemplo, en medio de un fichero “definiciones_basicas.h” puede haber unas líneas

#if PLATAFORMA_ES_LINUX
#include "funcionalidad_gratis.h"
#elif PLATAFORMA_ES_MICROSOFT_WINDOWS
#include "funcionalidad_cara.h"
#elif PLATAFORMA_ES_APPLE
#include "funcionalidad_muy_cara.h"
#else
#error Esta plataforma no está soportada
#endif

La diferencia entre “” y <>

La directiva #include existe en dos versiones. En una se pone el nombre de fichero entre comillas, en la otra entre paréntesis angulares (el signo menor y mayor como “comillas”).

#include "fichero_con_comillas.h"
#include <fichero_entre_menor_y_mayor.h>

La versión con los paréntesis angulares busca los ficheros en todos los directorios que se han especificado en la llamada al compilador – normalmente con la opción “-I”. Estos directorios se suelen rastrear por el fichero incluido en el orden en que aparecen en la línea de comando.

Cuando se incluye un fichero entre comillas, entonces el compilador busca este fichero primero en el mismo directorio que el fichero actualemente compilado y después en los demás directorios. Es decir, la versión con comillas se diferencia de la versión con paréntesis angulares únicamente por buscar primero en el directorio del fichero compilado. Tras no encontrarlo ahí actúa igual.

Esto muchas veces no es ninguna diferencia, ya que se suelen especificar todos los directorios en la línea de comando del compilador. Así no se suele dar el caso que se puede incluir un fichero con comillas pero no con paréntesis angulares.

Más significativo es el comportamiento ante ficheros con el mismo nombre en distintos directorios. En este caso la versión con comillas da preferencia sobre el fichero en el mismo directorio y esto suele ser el mejor acertado. Aunque sea preferible nombrar ficheros de forma única en un proyecto, es posible que no se pueda evitar tener dos ficheros con el mismo nombre cuando se incluyen varias bibliotecas de terceros.

De ahí se puede deducir que es imperativo incluir cabeceras de la misma biblioteca con comillas. De esta forma se puede asegurar que las cabeceras de una biblioteca se incluyan entre si aunque haya otros con el mismo nombre en uno de los directorios especificados en la línea de comandos.

Además, incluir con comillas puede dar al programador un significado adicional: que este fichero está bajo la custodia de mi equipo de desarrollo. Las cabeceras incluidas con paréntesis angulares son de bibliotecas de terceros. Los primeros ficheros puedo modificar si hace falta, los segundos no.

El orden de las inclusiones

El orden de las directivas #include no importa cuando todos los identificadores del programa son únicos. No obstante, a veces no lo son y conviene generar el error “este identificador ya existe” en nuestro código y no en el código de una biblioteca estándar.

Esto se consigue incluyendo primero las caberas de terceros. Si aparece un error de identificador doble, entonces aparece en la segunda definición – que es la nuestra – y ahí podemos cambiar el nombre del objeto sin problema.

En proyectos realmente grandes puede haber varias bibliotecas de distintas dependencias. Por la misma razón de generar los errores de identificadores dobles en el código más fácilmente modificable, conviene incluir las bibliotecas más básicas primero. Dentro del mismo nivel podemos ordenar los ficheros incluidos de forma alfabética. Esto ayuda a encontrar inclusiones que faltan o sobran.

El siguiente código muestra una secuencia de inclusiones para un fichero “definicion_de_mi_clase.cpp”.

// Primero se debe incluir la cabecera de precompilación
#include "cabecera_de_precompilacion.h"

// Segundo, incluir la cabecera correspondiente
// a este fichero de implementación.
// Esto deja más claro, que es la
// clase que se implementa aquí.
#include "definicion_de_mi_clase.h"

// A continuación inclusiones de la biblioteca estándar.
// Se usan paréntesis angulares.
#include <vector>

// Inclusiones de otras bibliotecas de terceros
#include <wx.h>
#include <gl.h>

// Inclusiones de subbibliotecas básicas de
// mi proyecto con comillas
#include "mis_definiciones_basicas.h"

// Luego las demás inclusiones de mi proyecto
#include "clases_auxiliares.h"
#include "más_definiciones_para_mi_clase.h"

Usar rutas relativas

Una forma de evitar nombres de fichero dobles es incluir ficheros con rutas relativas.

#include "definiciones/tipos_básicos.h"
#include "funcionalidad/tipos_básicos.h"

La desventaja de esta forma es, que uno debe saber, en qué directorio se encuentra cada cabecera. No obstante, esto suele ser un problema menor. Sin ruta relativa, uno debería poner un prefijo a cada nombre de fichero para evitar nombres dobles. Estos prefijos son típicamente los nombres de los directorios. Es decir, todos los ficheros en el directorio “definiciones” tienen se llaman “definiciones_xxx”. Al final debo saber el nombre de directorio de todas formas.

Los programadores de C++ han copiado de Java la idea de estructurar los directorios como los espacios de nombre. Así, el uso de la clase NombreDeEspacio::Subespacio::MiClase requiere la inclusión del fichero “nombre_de_espacio/subespacio/mi_clase.h”.

El uso de rutas relativas en las inclusiones puede mejorar bastante el orden y reducir la configuración del compilador. Así basta incluir un solo directorio para obtener acceso a todos los componentes de la biblioteca boost, por ejemplo. En cambio, rutas relativas hacen más complicado recolocar ficheros a otros directorios. Y esto puede pasar a menudo en fases tempranos de un proyecto.

Optimizar la velocidad de compilación

La inclusión de un sólo fichero normalmente no afecta mucho al tiempo, que el compilador requiere para la compilación de un fichero. Pero esto se cambia si el fichero incluido incluye más ficheros que a su vez incluyen aún más ficheros. Gracias a tantas inclusiones anidadas, una sola inclusión, sí, puede cambiar el tiempo de compilación drásticamente.

Una forma de mejorar la velocidad es utilizar cabeceras precompiladas. Estas cabeceras deben estar incluidas por todos los ficheros de definición y, por eso, debe ser una decisión temprana en un proyecto de utilizarlas o no – al menos si uno quiere evitar añadir esta inclusión en muchos ficheros.

Otra forma es evitar la inclusión de ficheros no necesarios. Normalmente una inclusión adicional no perjudica al resultado de la compilación. Simplemente añade declaraciones que finalmente no se usan. Pero la compilación de estas declaraciones cuesta un tiempo que nos queremos ahorrar.

Para proyectos grandes puede convenir no incluir definiciones de tipos que sólo aparecen como punteros o referencias en los ficheros de cabecera. Esto tipos se pueden introducir con una declaración forward. Sólo en el fichero de definición se incluye entonces la cabecera que define este tipo.

Como ejemplo damos una clase, que guarda un puntero a otra clase. En el fichero de cabecera tenemos

// Declaración forward
class OtraClase;

// Declaración de la clase principal
class UnaClase
{
	OtraClase* dame_un_puntero_a_otra_clase();
};

En el fichero de definición tenemos

#include "una_clase.h"
#include "otra_clase.h"

OtraClase* UnaClase::dame_un_puntero_a_otra_clase()
{
	static OtraClase otraInstancia;
	return &otraInstancia;
}

Cabe señalar que una declaración forward debe tener lugar dentro del nombre de espacio correspondiente.

// Esto no es correcto
class NombreDeEspacio::Subespacio::Clase;

// Se debe hacer la declaración forward así
namespace NombreDeEspacio
{
	namespace Subespacio
	{
		class Clase;
	}
}

Conclusión

Como hemos comprobado, hay bastante que se puede tener en cuenta sobre la directiva #include, cuando se trabaja en proyectos grandes.

  • La inclusión genera un sólo fichero grande. Tener esto en mente cuando se produce un error de compilación en la primera línea aparamente correcta tras un #include.
  • Conviene incluir ficheros del propio proyecto con comillas y las cabeceras de terceros con paréntesis angulares para marcar la diferencia entre “propio” y “estándar”.
  • Ordenando las propias inclusiones al final ayuda a corregir errores de doble definición de identificadores.
  • Rutas relativas son una buena opción para proyectos con una clara estructura de directorios.
  • Usar cabeceras precompiladas y evitar incluir ficheros innecesarios – también aprovechando declaraciones forward – puede acelerar el proceso de compilación.

Lectura adicional

About these ads