¿Por qué mis protectores de inclusión no impiden la inclusión recursiva y las definiciones de símbolos múltiples?
Dos preguntas comunes sobre incluir guardias :
PRIMERA PREGUNTA:
¿Por qué no hay protectores de inclusión que protejan mis archivos de encabezado contra la inclusión mutua y recursiva ? Sigo recibiendo errores sobre símbolos inexistentes que obviamente están ahí o incluso errores de sintaxis más extraños cada vez que escribo algo como lo siguiente:
"ah"
#ifndef A_H #define A_H #include "b.h" ... #endif // A_H
"bh"
#ifndef B_H #define B_H #include "a.h" ... #endif // B_H
"principal.cpp"
#include "a.h" int main() { ... }
¿Por qué recibo errores al compilar "main.cpp"? ¿Qué necesito hacer para solucionar mi problema?
SEGUNDA PREGUNTA:
¿Por qué no se incluyen guardias que impidan definiciones múltiples ? Por ejemplo, cuando mi proyecto contiene dos archivos que incluyen el mismo encabezado, a veces el vinculador se queja de que algún símbolo se define varias veces. Por ejemplo:
"encabezado.h"
#ifndef HEADER_H #define HEADER_H int f() { return 0; } #endif // HEADER_H
"fuente1.cpp"
#include "header.h" ...
"fuente2.cpp"
#include "header.h" ...
¿Por qué está pasando esto? ¿Qué necesito hacer para solucionar mi problema?
PRIMERA PREGUNTA:
¿Por qué no hay protectores de inclusión que protejan mis archivos de encabezado contra la inclusión mutua y recursiva ?
Ellos son .
En lo que no ayudan es en las dependencias entre las definiciones de estructuras de datos en encabezados que se incluyen entre sí . Para ver lo que esto significa, comencemos con un escenario básico y veamos por qué los guardias de inclusión ayudan con las inclusiones mutuas.
Supongamos que sus archivos de encabezado a.h
y que se incluyen mutuamente b.h
tienen contenido trivial, es decir, las elipses en las secciones de código del texto de la pregunta se reemplazan con la cadena vacía. En esta situación, main.cpp
compilarás felizmente. ¡Y esto es sólo gracias a tus guardias incluidos!
Si no estás convencido, intenta eliminarlos:
//================================================
// a.h
#include "b.h"
//================================================
// b.h
#include "a.h"
//================================================
// main.cpp
//
// Good luck getting this to compile...
#include "a.h"
int main()
{
...
}
Notarás que el compilador informará una falla cuando alcance el límite de profundidad de inclusión. Este límite es específico de la implementación. Según el párrafo 16.2/6 del estándar C++11:
Una directiva de preprocesamiento #include puede aparecer en un archivo fuente que se ha leído debido a una directiva #include en otro archivo, hasta un límite de anidamiento definido por la implementación .
Entonces, ¿qué está pasando ?
- Al analizar
main.cpp
, el preprocesador cumplirá la directiva#include "a.h"
. Esta directiva le dice al preprocesador que procese el archivo de encabezadoa.h
, tome el resultado de ese procesamiento y reemplace la cadena#include "a.h"
con ese resultado; - Mientras procesa
a.h
, el preprocesador cumplirá la directiva#include "b.h"
y se aplica el mismo mecanismo: el preprocesador procesará el archivo de encabezadob.h
, tomará el resultado de su procesamiento y reemplazará la#include
directiva con ese resultado; - Al procesar
b.h
, la directiva#include "a.h"
le indicará al preprocesador que procesea.h
y reemplace esa directiva con el resultado; - El preprocesador comenzará a analizar
a.h
nuevamente, cumplirá#include "b.h"
nuevamente con la directiva y esto configurará un proceso recursivo potencialmente infinito. Al alcanzar el nivel de anidamiento crítico, el compilador informará un error.
Sin embargo, cuando están presentes los guardias de inclusión , no se configurará ninguna recursividad infinita en el paso 4. Veamos por qué:
- ( igual que antes ) Al analizar
main.cpp
, el preprocesador cumplirá la directiva#include "a.h"
. Esto le dice al preprocesador que procese el archivo de encabezadoa.h
, tome el resultado de ese procesamiento y reemplace la cadena#include "a.h"
con ese resultado; - Durante el procesamiento
a.h
, el preprocesador cumplirá la directiva#ifndef A_H
. Como la macroA_H
aún no se ha definido, seguirá procesando el siguiente texto. La siguiente directiva (#defines A_H
) define la macroA_H
. Luego, el preprocesador cumplirá la directiva#include "b.h"
: el preprocesador ahora procesará el archivo de encabezadob.h
, tomará el resultado de su procesamiento y reemplazará la#include
directiva con ese resultado; - Al procesar
b.h
, el preprocesador cumplirá la directiva#ifndef B_H
. Como la macroB_H
aún no se ha definido, seguirá procesando el siguiente texto. La siguiente directiva (#defines B_H
) define la macroB_H
. Luego, la directiva#include "a.h"
le indicará al preprocesador que procesea.h
y reemplace la#include
directivab.h
con el resultado del preprocesamientoa.h
; - El compilador comenzará el preprocesamiento
a.h
nuevamente y cumplirá#ifndef A_H
nuevamente la directiva. Sin embargo, durante el preprocesamiento anterior,A_H
se definió la macro. Por lo tanto, esta vez el compilador omitirá el siguiente texto hasta que#endif
se encuentre la directiva coincidente y el resultado de este procesamiento sea la cadena vacía (suponiendo que nada siga la#endif
directiva, por supuesto). Por lo tanto, el preprocesador reemplazará la#include "a.h"
directivab.h
con la cadena vacía y rastreará la ejecución hasta que reemplace la#include
directiva original enmain.cpp
.
Por lo tanto, incluir guardias protege contra la inclusión mutua . Sin embargo, no pueden ayudar con las dependencias entre las definiciones de sus clases en archivos que se incluyen entre sí:
//================================================
// a.h
#ifndef A_H
#define A_H
#include "b.h"
struct A
{
};
#endif // A_H
//================================================
// b.h
#ifndef B_H
#define B_H
#include "a.h"
struct B
{
A* pA;
};
#endif // B_H
//================================================
// main.cpp
//
// Good luck getting this to compile...
#include "a.h"
int main()
{
...
}
Dados los encabezados anteriores, main.cpp
no se compilará.
¿Por qué está pasando esto?
Para ver qué está pasando, basta con volver a realizar los pasos 1 a 4.
Es fácil ver que los primeros tres pasos y la mayor parte del cuarto paso no se ven afectados por este cambio (simplemente léalos para convencerse). Sin embargo, sucede algo diferente al final del paso 4: después de reemplazar la #include "a.h"
directiva b.h
con la cadena vacía, el preprocesador comenzará a analizar el contenido b.h
y, en particular, la definición de B
. Desafortunadamente, la definición de B
menciona class A
, que nunca antes se había cumplido exactamente debido a los guardias de inclusión.
Declarar una variable miembro de un tipo que no ha sido declarado previamente es, por supuesto, un error, y el compilador lo señalará cortésmente.
¿Qué necesito hacer para solucionar mi problema?
Necesita declaraciones anticipadas .
De hecho, la definición de clase A
no es necesaria para definir la clase B
, porque un puntero se A
declara como una variable miembro y no como un objeto de tipo A
. Dado que los punteros tienen un tamaño fijo, el compilador no necesitará conocer el diseño exacto A
ni calcular su tamaño para definir correctamente la clase B
. Por lo tanto, es suficiente declararA
la clase b.h
y hacer que el compilador sea consciente de su existencia:
//================================================
// b.h
#ifndef B_H
#define B_H
// Forward declaration of A: no need to #include "a.h"
struct A;
struct B
{
A* pA;
};
#endif // B_H
main.cpp
Ahora seguramente compilarás . Un par de comentarios:
- No solo romper la inclusión mutua reemplazando la
#include
directiva con una declaración directab.h
fue suficiente para expresar efectivamente la dependencia deB
:A
usar declaraciones directas siempre que sea posible/práctico también se considera una buena práctica de programación , porque ayuda a evitar inclusiones innecesarias, por lo tanto reduciendo el tiempo total de compilación. Sin embargo, después de eliminar la inclusión mutua,main.cpp
tendrá que modificarse a#include
ambosa.h
yb.h
(si es que este último es necesario), porqueb.h
ya no se realiza indirectamente#include
mediantea.h
; - Si bien una declaración directa de una clase
A
es suficiente para que el compilador declare punteros a esa clase (o la use en cualquier otro contexto donde los tipos incompletos sean aceptables), desreferenciar punteros aA
(por ejemplo, para invocar una función miembro) o calcular su tamaño es suficiente. operaciones ilegales en tipos incompletos: si eso es necesario, la definición completa deA
debe estar disponible para el compilador, lo que significa que se debe incluir el archivo de encabezado que lo define. Esta es la razón por la que las definiciones de clase y la implementación de sus funciones miembro generalmente se dividen en un archivo de encabezado y un archivo de implementación para esa clase (las plantillas de clase son una excepción a esta regla): archivos de implementación, que nunca son#include
analizados por otros archivos en el proyecto. , puede incluir de forma segura#include
todos los encabezados necesarios para hacer visibles las definiciones. Los archivos de encabezado, por otro lado, no incluirán#include
otros archivos de encabezado a menos que realmente necesiten hacerlo (por ejemplo, para hacer visible la definición de una clase base ), y usarán declaraciones directas siempre que sea posible/práctico.
SEGUNDA PREGUNTA:
¿Por qué no se incluyen guardias que impidan definiciones múltiples ?
Ellos son .
De lo que no le protegen es de definiciones múltiples en unidades de traducción separadas . Esto también se explica en estas preguntas y respuestas sobre StackOverflow.
Para ver eso, intente eliminar las protecciones de inclusión y compilar la siguiente versión modificada de source1.cpp
(o source2.cpp
, para lo que importa):
//================================================
// source1.cpp
//
// Good luck getting this to compile...
#include "header.h"
#include "header.h"
int main()
{
...
}
El compilador seguramente se quejará aquí de f()
haber sido redefinido. Eso es obvio: ¡su definición se incluye dos veces! Sin embargo, lo anterior source1.cpp
se compilará sin problemas cuando header.h
contenga los include guards adecuados . Eso es lo esperado.
Aún así, incluso cuando las protecciones de inclusión estén presentes y el compilador dejará de molestarlo con mensajes de error, el vinculador insistirá en el hecho de que se encuentran múltiples definiciones al fusionar el código objeto obtenido de la compilación de source1.cpp
y source2.cpp
y se negará a generar su ejecutable.
¿Por qué está pasando esto?
Básicamente, cada .cpp
archivo (el término técnico en este contexto es unidad de traducción ) de su proyecto se compila por separado e independientemente . Al analizar un .cpp
archivo, el preprocesador procesará todas las #include
directivas y expandirá todas las invocaciones de macros que encuentre, y el resultado de este procesamiento de texto puro se entregará al compilador para traducirlo a código objeto. Una vez que el compilador termina de producir el código objeto para una unidad de traducción, continuará con la siguiente y se olvidarán todas las definiciones de macro que se encontraron al procesar la unidad de traducción anterior.
De hecho, compilar un proyecto con n
unidades de traducción ( .cpp
archivos) es como ejecutar el mismo programa (el compilador) n
varias veces, cada vez con una entrada diferente: diferentes ejecuciones del mismo programa no compartirán el estado de las ejecuciones anteriores del programa. ) . Por lo tanto, cada traducción se realiza de forma independiente y los símbolos del preprocesador encontrados al compilar una unidad de traducción no se recordarán al compilar otras unidades de traducción (si lo piensa por un momento, se dará cuenta fácilmente de que este es en realidad un comportamiento deseable).
Por lo tanto, aunque los protectores de inclusión le ayudan a evitar inclusiones mutuas recursivas e inclusiones redundantes del mismo encabezado en una unidad de traducción, no pueden detectar si la misma definición está incluida en una unidad de traducción diferente .
Sin embargo, al fusionar el código objeto generado a partir de la compilación de todos los .cpp
archivos de su proyecto, el vinculador verá que el mismo símbolo está definido más de una vez, y esto viola la regla de una definición . Según el párrafo 3.2/3 del estándar C++11:
Cada programa contendrá exactamente una definición de cada función o variable no en línea que se utilice odr en ese programa; no se requiere diagnóstico. La definición puede aparecer explícitamente en el programa, puede encontrarse en la biblioteca estándar o definida por el usuario, o (cuando sea apropiado) está definida implícitamente (ver 12.1, 12.4 y 12.8). Se definirá una función en línea en cada unidad de traducción en la que se utilice odr .
Por lo tanto, el vinculador emitirá un error y se negará a generar el ejecutable de su programa.
¿Qué necesito hacer para solucionar mi problema?
Si desea mantener la definición de su función en un archivo de encabezado que está #include
dividido por múltiples unidades de traducción (tenga en cuenta que no surgirá ningún problema si su encabezado está #include
dividido por solo una unidad de traducción), debe usar la inline
palabra clave.
De lo contrario, necesita mantener solo la declaración de su función header.h
, colocando su definición (cuerpo) en un solo archivo separado .cpp
(este es el enfoque clásico).
La inline
palabra clave representa una solicitud no vinculante al compilador para alinear el cuerpo de la función directamente en el sitio de la llamada, en lugar de configurar un marco de pila para una llamada de función normal. Aunque el compilador no tiene que cumplir con su solicitud, la inline
palabra clave logra indicarle al vinculador que tolere múltiples definiciones de símbolos. Según el párrafo 3.2/5 del estándar C++11:
Puede haber más de una definición de un tipo de clase (Cláusula 9), tipo de enumeración (7.2), función en línea con enlace externo (7.1.2), plantilla de clase (Cláusula 14), plantilla de función no estática (14.5.6). , miembro de datos estáticos de una plantilla de clase (14.5.1.3), función miembro de una plantilla de clase (14.5.1.1) o especialización de plantilla para la cual algunos parámetros de plantilla no están especificados (14.7, 14.5.5) en un programa siempre que cada definición aparece en una unidad de traducción diferente, y siempre que las definiciones cumplan los siguientes requisitos [...]
El párrafo anterior básicamente enumera todas las definiciones que comúnmente se colocan en archivos de encabezado , porque se pueden incluir de manera segura en múltiples unidades de traducción. Todas las demás definiciones con enlace externo, en cambio, pertenecen a los archivos fuente.
El uso de la static
palabra clave en lugar de la inline
palabra clave también da como resultado la supresión de errores del vinculador al darle a su función un vínculo interno , lo que hace que cada unidad de traducción contenga una copia privada de esa función (y de sus variables estáticas locales). Sin embargo, esto eventualmente da como resultado un ejecutable más grande y, inline
en general, se debe preferir el uso de.
Una forma alternativa de lograr el mismo resultado que con la static
palabra clave es colocar la función f()
en un espacio de nombres sin nombre . Según el párrafo 3.5/4 del estándar C++11:
Un espacio de nombres sin nombre o un espacio de nombres declarado directa o indirectamente dentro de un espacio de nombres sin nombre tiene un vínculo interno. Todos los demás espacios de nombres tienen enlaces externos. Un nombre que tiene un alcance de espacio de nombres al que no se le ha proporcionado un vínculo interno arriba tiene el mismo vínculo que el espacio de nombres adjunto si es el nombre de:
- una variable; o
- Una función ; o
— una clase con nombre (Cláusula 9), o una clase sin nombre definida en una declaración de typedef en la que la clase tiene el nombre de typedef para fines de vinculación (7.1.3); o
— una enumeración con nombre (7.2), o una enumeración sin nombre definida en una declaración de typedef en la que la enumeración tiene el nombre de typedef para fines de vinculación (7.1.3); o
— un enumerador que pertenece a una enumeración con vinculación; o
- una plantilla.
For the same reason mentioned above, the inline
keyword should be preferred.