La forma convencional de lograr eso sería colocar el código fuente de cada módulo en un directorio separado. Cada directorio puede contener todos los archivos de origen y encabezado para el módulo.
El encabezado público para cada módulo se puede colocar en un directorio de encabezados común y separado. Probablemente usaría un enlace simbólico desde el directorio común al directorio de módulos correspondiente para cada encabezado.
Las reglas de compilación simplemente indican que ningún módulo puede incluir encabezados de otros módulos a excepción de los encabezados en el directorio común. Esto logra el resultado de que ningún módulo puede incluir encabezados de otro módulo, a excepción del encabezado público (reforzando así las barreras privadas).
La prevención de dependencias cíclicas automáticamente no es trivial. El problema es que solo puede establecer que existe una dependencia cíclica al examinar varios archivos de origen a la vez, y el compilador solo mira uno a la vez.
Considere un par de módulos, ModuleA y ModuleB, y un programa, Program1, que utiliza ambos módulos.
base/include
ModuleA.h
ModuleB.h
base/ModuleA
ModuleA.h
ModuleA1.c
ModuleA2.c
base/ModuleB
ModuleB.h
ModuleB1.c
ModuleB2.c
base/Program1
Program1.c
Al compilar Program1.c, es perfectamente legítimo que incluyen tanto ModuleA.h y ModuleB.h si se hace uso de los servicios de ambos módulos. Por lo tanto, ModuleA.h no puede presentar una queja si el MóduloB.h está incluido en la misma unidad de traducción (TU), y tampoco el MóduloB.h puede presentar una queja si ModuleA.h está incluido en la misma TU.
Supongamos que es legítimo que ModuleA use las instalaciones de ModuleB. Por lo tanto, al compilar ModuleA1.c o ModuleA2.c, no puede haber ningún problema al tener ambos ModuleA.h y ModuleB.h incluidos.
Sin embargo, para evitar dependencias cíclicas, debe poder prohibir que el código en ModuleB1.c y ModuleB2.c use ModuleA.h.
Por lo que puedo ver, la única manera de hacerlo es mediante una técnica que requiere un encabezado privado para el Módulo B que dice "ModuleA ya está incluido" aunque no lo esté, y esto está incluido antes de ModuleA.h está siempre incluido.
El esqueleto de ModuleA.h será el formato estándar (y ModuleB.h habrá similar):
#ifndef MODULEA_H_INCLUDED
#define MODULEA_H_INCLUDED
...contents of ModuleA.h...
#endif
Ahora, si el código en ModuleB1.c contiene:
#define MODULEA_H_INCLUDED
#include "ModuleB.h"
...if ModuleA.h is also included, it will declare nothing...
...so anything that depends on its contents will fail to compile...
Esto está lejos de ser automático.
Puede hacer un análisis de los archivos incluidos y exigir que exista un tipo topológico sin bucles de las dependencias. Solía haber un programa tsort
en sistemas UNIX (y un programa complementario, lorder
) que juntos proporcionaban los servicios necesarios para que se pudiera crear una biblioteca estática (.a
) que contuviera los archivos del objeto en un orden que no requiriera volver a escanear el archivo. El programa ranlib
y, finalmente, ar
y ld
asumieron las tareas de gestión del reexploración de una sola biblioteca, por lo que el lorder
es especialmente redundante. Pero tsort
tiene usos más generales; está disponible en algunos sistemas (MacOS X, por ejemplo, RHEL 5 Linux también).
Por lo tanto, utilizando el seguimiento de dependencia de GCC más tsort
, debe poder verificar si hay ciclos entre módulos. Pero eso debería manejarse con cuidado.
Puede haber algún IDE u otro conjunto de herramientas que maneje esto automáticamente. Pero normalmente los programadores pueden ser lo suficientemente disciplinados como para evitar problemas, siempre y cuando los requisitos y las dependencias entre módulos estén cuidadosamente documentados.
+1 Gran respuesta, en particular, el último párrafo. – mouviciel
Jonathan, ¿cuál sería la herramienta de elección para implementar su solución sugerida? Hacer, CMake, Rake u otra cosa por completo? – thegreendroid
No tengo puntos de vista fuertes sobre los sistemas más modernos; He visto exaltar a CMake, pero probablemente puedas usar cualquiera de ellos. Puede depender en parte de los idiomas con los que esté más familiarizado. Si usas Ruby, Rake tiene sentido, por ejemplo. Todavía tiendo a utilizar el viejo y sencillo Make, pero he estado haciendo eso durante ... bastante tiempo ... así que me siento cómodo haciéndolo. Es algo así como una solución de mínimo denominador común, con problemas de portabilidad. A veces uso Autoconf para manejar los problemas de portabilidad, pero eso tiene su propio conjunto de complejidades (y aumenta dramáticamente el tamaño de los pequeños proyectos). –