c++ - recursividad - ¿Por qué mis protectores de inclusión no previenen la inclusión recursiva y las definiciones de símbolos múltiples?
recursividad arreglos c++ (1)
Dos preguntas comunes sobre incluyen guardias :
PRIMERA PREGUNTA:
¿Por qué no se incluyen guardias que protegen mis archivos de cabecera de la inclusión mutua recursiva ? Sigo recibiendo errores sobre símbolos no existentes que obviamente existen 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
"main.cpp"
#include "a.h" int main() { ... }
¿Por qué obtengo errores compilando "main.cpp"? ¿Qué debo hacer para resolver mi problema?
SEGUNDA PREGUNTA:
¿Por qué no se incluyen guardias que impiden definiciones múltiples ? Por ejemplo, cuando mi proyecto contiene dos archivos que incluyen el mismo encabezado, a veces el enlazador se queja de que algún símbolo se haya definido varias veces. Por ejemplo:
"header.h"
#ifndef HEADER_H #define HEADER_H int f() { return 0; } #endif // HEADER_H
"fuente1.cpp"
#include "header.h" ...
"source2.cpp"
#include "header.h" ...
¿Por qué está pasando esto? ¿Qué debo hacer para resolver mi problema?
PRIMERA PREGUNTA:
¿Por qué no se incluyen guardias que protegen mis archivos de cabecera de la inclusión mutua recursiva ?
Ellos son .
Con lo que no están ayudando es con las dependencias entre las definiciones de estructuras de datos en encabezados que se incluyen mutuamente . Para ver lo que esto significa, comencemos con un escenario básico y veamos por qué incluir guardias ayuda con las inclusiones mutuas.
Supongamos que sus archivos de cabecera ah
y bh
se incluyen mutuamente tienen contenido trivial, es decir, las elipses en las secciones de código del texto de la pregunta se reemplazan por la cadena vacía. En esta situación, su main.cpp
compilará felizmente. ¡Y esto es solo gracias a tus guardias de inclusión!
Si no está convencido, intente 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á que el compilador informará un error 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 de la Norma C ++ 11:
Una directiva de preprocesamiento de #include puede aparecer en un archivo de origen 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á con la directiva#include "ah"
. Esta directiva le dice al preprocesador que procese el archivo de cabeceraah
, tome el resultado de ese procesamiento y reemplace la cadena#include "ah"
con ese resultado; - Al procesar
ah
, el preprocesador cumplirá con la directiva#include "bh"
, y se aplica el mismo mecanismo: el preprocesador procesará el archivo de encabezadobh
, tomará el resultado de su procesamiento y reemplazará la directiva#include
con ese resultado; - Al procesar
bh
, la directiva#include "ah"
le indicará al preprocesador que proceseah
y reemplazará esa directiva con el resultado; - El preprocesador comenzará a analizar
ah
nuevamente, cumplirá con la directiva#include "bh"
nuevamente, y esto configurará un proceso recursivo potencialmente infinito. Al alcanzar el nivel crítico de anidación, el compilador informará un error.
Sin embargo, cuando se incluyen guardias presentes , no se establecerá ninguna recursión infinita en el paso 4. Veamos por qué:
- ( igual que antes ) Al analizar
main.cpp
, el preprocesador cumplirá con la directiva#include "ah"
. Esto le dice al preprocesador que procese el archivo de cabeceraah
, tome el resultado de ese procesamiento y reemplace la cadena#include "ah"
con ese resultado; - Mientras se procesa
ah
, el preprocesador cumplirá con la directiva#ifndef A_H
. Como la macroA_H
aún no se ha definido, seguirá procesando el siguiente texto. La directiva subsiguiente (#defines A_H
define#defines A_H
) define la macroA_H
. Entonces, el preprocesador cumplirá con la directiva#include "bh"
: el preprocesador ahora procesará el archivo de encabezadobh
, tomará el resultado de su procesamiento y reemplazará la directiva#include
con ese resultado; - Al procesar
bh
, el preprocesador cumplirá con la directiva#ifndef B_H
. Como la macroB_H
aún no se ha definido, seguirá procesando el siguiente texto. La directiva subsiguiente (#defines B_H
define#defines B_H
) define la macroB_H
. Entonces, la directiva#include "ah"
le dirá al preprocesador que proceseah
y reemplace la directiva#include
enbh
con el resultado de preprocesamientoah
; - El compilador comenzará a preprocesarse de nuevo, y cumplirá nuevamente la directiva
#ifndef A_H
. Sin embargo, durante el preprocesamiento previo, se ha definido la macroA_H
. Por lo tanto, el compilador omitirá el siguiente texto esta vez hasta que se encuentre la directiva correspondiente#endif
, y el resultado de este procesamiento sea la cadena vacía (suponiendo que nada siga a la directiva#endif
, por supuesto). Por lo tanto, el preprocesador reemplazará la directiva#include "ah"
enbh
con la cadena vacía, y rastreará la ejecución hasta que reemplace la directiva#include
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 mutuamente:
//================================================
// 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()
{
...
}
Teniendo en cuenta los encabezados anteriores, main.cpp
no compilará.
¿Por qué está pasando esto?
Para ver qué sucede, basta con pasar de nuevo a 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 (basta con leerlos para convencerse). Sin embargo, algo diferente ocurre al final del paso 4: después de reemplazar la directiva #include "ah"
en bh
con la cadena vacía, el preprocesador comenzará a analizar el contenido de bh
y, en particular, la definición de B
Desafortunadamente, la definición de B
menciona la clase A
, que nunca antes se había cumplido exactamente debido a las 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 cortésmente lo señalará.
¿Qué debo hacer para resolver mi problema?
Usted necesita declaraciones futuras .
De hecho, la definición de clase A
no es necesaria para definir la clase B
, porque un puntero a A
se declara como una variable miembro, y no como un objeto de tipo A
Como los punteros tienen un tamaño fijo, el compilador no necesitará conocer el diseño exacto de A
ni calcular su tamaño para definir correctamente la clase B
Por lo tanto, basta con reenviar-declarar la clase A
en bh
y hacer que el compilador tenga conocimiento 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
Su main.cpp
ahora ciertamente compilará. Un par de observaciones:
- No solo romper la inclusión mutua reemplazando la directiva
#include
con una declaración directa enbh
fue suficiente para expresar de manera efectiva la dependencia deB
enA
: usar declaraciones avanzadas siempre que sea posible / práctico también se considera una buena práctica de programación , porque ayuda evitando inclusiones innecesarias, reduciendo así el tiempo de compilación general. Sin embargo, después de eliminar la inclusión mutua,main.cpp
tendrá que modificarse para incluir#include
ah
ybh
(si esto último se necesita), porquebh
no es más indirectamente#include
d aah
; - Mientras que una declaración directa de clase
A
es suficiente para que el compilador declare punteros a esa clase (o para usarla en cualquier otro contexto donde los tipos incompletos son aceptables), desmarca punteros aA
(por ejemplo, para invocar una función miembro) o calcula su tamaño son operaciones ilegales en tipos incompletos: si eso es necesario, la definición completa deA
debe estar disponible para el compilador, lo que significa que debe incluirse el archivo de encabezado que la define. Esta es la razón por la que las definiciones de clases y la implementación de sus funciones miembro usualmente se dividen en un archivo de cabecera 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
por otros archivos en el proyecto, puede#include
manera segura todos los encabezados necesarios para hacer que las definiciones sean visibles. 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 de reenvío siempre que sea posible / práctico.
SEGUNDA PREGUNTA:
¿Por qué no se incluyen guardias que impiden definiciones múltiples ?
Ellos son .
De lo que no te están protegiendo es de múltiples definiciones en unidades de traducción separadas . Esto también se explica en este Q & A en .
Demasiado ver eso, intente eliminar los guardias de inclusión y compilar la siguiente versión modificada de source1.cpp
(o source2.cpp
, por 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()
siendo redefinido. Eso es obvio: ¡su definición se incluye dos veces! Sin embargo, el source1.cpp
anterior se compilará sin problemas cuando header.h
contiene los guardias de inclusión adecuados . Eso es esperado.
Aún así, incluso cuando los guardias de inclusión están presentes y el compilador dejará de molestarlo con un mensaje de error, el enlazador insistirá en el hecho de que se encuentran varias 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 archivo .cpp
(el término técnico en este contexto es unidad de traducción ) en su proyecto se compila por separado e independientemente . Al analizar un archivo .cpp
, el preprocesador procesará todas las directivas #include
y ampliará todas las macrovocaciones que encuentre, y el resultado de este procesamiento de texto puro se entregará en una entrada al compilador para traducirlo en código objeto. Una vez que el compilador finaliza la producción del código de objeto para una unidad de traducción, se procederá con la siguiente, y se olvidarán todas las definiciones macro que se hayan encontrado al procesar la unidad de traducción anterior.
De hecho, compilar un proyecto con n
unidades de traducción (archivos .cpp
) es como ejecutar el mismo programa (el compilador) n
veces, cada vez con una entrada diferente: diferentes ejecuciones del mismo programa no compartirán el estado del anterior ejecución (es) 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 piensas por un momento, te darás cuenta fácilmente de que esto es realmente un comportamiento deseable).
Por lo tanto, aunque los guardias de inclusión lo ayudan a evitar inclusiones recursivas recíprocas e inclusiones redundantes del mismo encabezado en una unidad de traducción, no pueden detectar si la misma definición se incluye en una unidad de traducción diferente .
Sin embargo, al fusionar el código de objeto generado a partir de la compilación de todos los archivos .cpp
de su proyecto, el vinculador verá que el mismo símbolo se define más de una vez, y como esto infringe la Regla de una sola definición . Según el Párrafo 3.2 / 3 de la Norma C ++ 11:
Cada programa debe contener exactamente una definición de cada función o variable no en línea que se utiliza en ese programa; no se requiere diagnóstico. La definición puede aparecer explícitamente en el programa, se puede encontrar en la biblioteca estándar o definida por el usuario, o (cuando corresponda) está implícitamente definida (ver 12.1, 12.4 y 12.8). Se debe definir una función en línea en cada unidad de traducción en la que se utiliza .
Por lo tanto, el enlazador emitirá un error y se negará a generar el ejecutable de su programa.
¿Qué debo hacer para resolver mi problema?
Si desea mantener la definición de su función en un archivo de encabezado #include
d por varias unidades de traducción (observe que no surgirá ningún problema si su encabezado es #include
d solo por una unidad de traducción), debe usar la palabra clave en inline
.
De lo contrario, debe mantener solo la declaración de su función en header.h
, poniendo su definición (cuerpo) en un archivo .cpp
separado solamente (este es el enfoque clásico).
La palabra clave en inline
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 a función normal. Aunque el compilador no tiene que cumplir su solicitud, la palabra clave en inline
sí logra decirle al vinculador que tolere múltiples definiciones de símbolos. De acuerdo con el párrafo 3.2 / 5 de la Norma C ++ 11:
Puede haber más de una definición de 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 que no se especifican algunos parámetros de plantilla (14.7, 14.5.5) en un programa siempre que cada la definición aparece en una unidad de traducción diferente, y siempre que las definiciones satisfagan los siguientes requisitos [...]
El Párrafo anterior básicamente enumera todas las definiciones que comúnmente se incluyen en los archivos de encabezado , porque se pueden incluir de manera segura en varias unidades de traducción. Todas las otras definiciones con enlace externo, en cambio, pertenecen a los archivos fuente.
El uso de la palabra clave static
lugar de la palabra clave en inline
también resulta en la supresión de los errores del vinculador dando a su función un enlace interno , haciendo 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 resulta en un ejecutable más grande, y el uso de inline
debería ser preferido en general.
Una forma alternativa de lograr el mismo resultado que con la palabra clave static
es poner la función f()
en un espacio de nombre sin nombre . Según el Párrafo 3.5 / 4 de la Norma 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 enlace interno. Todos los demás espacios de nombres tienen enlaces externos. Un nombre que tiene un ámbito de espacio de nombres que no se le ha dado enlace interno arriba tiene el mismo enlace 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 typedef en la que la clase tiene el nombre typedef para fines de vinculación (7.1.3); o
- una enumeración nombrada (7.2), o una enumeración sin nombre definida en una declaración typedef en la que la enumeración tiene el nombre typedef para fines de vinculación (7.1.3); o
- un enumerador que pertenece a una enumeración con enlace; o
- una plantilla.
Por el mismo motivo mencionado anteriormente, se debe preferir la palabra clave en inline
.