¿Por qué mis protectores de inclusión no impiden la inclusión recursiva y las definiciones de símbolos múltiples?

Resuelto Andy Prowl asked hace 11 años • 3 respuestas

Dos preguntas comunes sobre incluir guardias :

  1. 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?


  1. 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?

Andy Prowl avatar Feb 16 '13 18:02 Andy Prowl
Aceptado

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.hy que se incluyen mutuamente b.htienen 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.cppcompilará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 ?

  1. Al analizar main.cpp, el preprocesador cumplirá la directiva #include "a.h". Esta directiva le dice al preprocesador que procese el archivo de encabezado a.h, tome el resultado de ese procesamiento y reemplace la cadena #include "a.h"con ese resultado;
  2. Mientras procesa a.h, el preprocesador cumplirá la directiva #include "b.h"y se aplica el mismo mecanismo: el preprocesador procesará el archivo de encabezado b.h, tomará el resultado de su procesamiento y reemplazará la #includedirectiva con ese resultado;
  3. Al procesar b.h, la directiva #include "a.h"le indicará al preprocesador que procese a.hy reemplace esa directiva con el resultado;
  4. El preprocesador comenzará a analizar a.hnuevamente, 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é:

  1. ( 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 encabezado a.h, tome el resultado de ese procesamiento y reemplace la cadena #include "a.h"con ese resultado;
  2. Durante el procesamiento a.h, el preprocesador cumplirá la directiva #ifndef A_H. Como la macro A_Haún no se ha definido, seguirá procesando el siguiente texto. La siguiente directiva ( #defines A_H) define la macro A_H. Luego, el preprocesador cumplirá la directiva #include "b.h": el preprocesador ahora procesará el archivo de encabezado b.h, tomará el resultado de su procesamiento y reemplazará la #includedirectiva con ese resultado;
  3. Al procesar b.h, el preprocesador cumplirá la directiva #ifndef B_H. Como la macro B_Haún no se ha definido, seguirá procesando el siguiente texto. La siguiente directiva ( #defines B_H) define la macro B_H. Luego, la directiva #include "a.h"le indicará al preprocesador que procese a.hy reemplace la #includedirectiva b.hcon el resultado del preprocesamiento a.h;
  4. El compilador comenzará el preprocesamiento a.hnuevamente y cumplirá #ifndef A_Hnuevamente la directiva. Sin embargo, durante el preprocesamiento anterior, A_Hse definió la macro. Por lo tanto, esta vez el compilador omitirá el siguiente texto hasta que #endifse encuentre la directiva coincidente y el resultado de este procesamiento sea la cadena vacía (suponiendo que nada siga la #endifdirectiva, por supuesto). Por lo tanto, el preprocesador reemplazará la #include "a.h"directiva b.hcon la cadena vacía y rastreará la ejecución hasta que reemplace la #includedirectiva original en main.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.cppno 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.hcon la cadena vacía, el preprocesador comenzará a analizar el contenido b.hy, en particular, la definición de B. Desafortunadamente, la definición de Bmenciona 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 Ano es necesaria para definir la clase B, porque un puntero se Adeclara 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 Ani calcular su tamaño para definir correctamente la clase B. Por lo tanto, es suficiente declararA la clase b.hy 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.cppAhora seguramente compilarás . Un par de comentarios:

  1. No solo romper la inclusión mutua reemplazando la #includedirectiva con una declaración directa b.hfue suficiente para expresar efectivamente la dependencia de B: Ausar 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.cpptendrá que modificarse a #includeambos a.hy b.h(si es que este último es necesario), porque b.hya no se realiza indirectamente #includemediante a.h;
  2. Si bien una declaración directa de una clase Aes 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 a A(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 de Adebe 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 #includeanalizados por otros archivos en el proyecto. , puede incluir de forma segura #includetodos los encabezados necesarios para hacer visibles las definiciones. Los archivos de encabezado, por otro lado, no incluirán #includeotros 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.hcontenga 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.cppy source2.cppy se negará a generar su ejecutable.

¿Por qué está pasando esto?

Básicamente, cada .cpparchivo (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 .cpparchivo, el preprocesador procesará todas las #includedirectivas 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 nunidades de traducción ( .cpparchivos) es como ejecutar el mismo programa (el compilador) nvarias 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 .cpparchivos 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á #includedividido por múltiples unidades de traducción (tenga en cuenta que no surgirá ningún problema si su encabezado está #includedividido por solo una unidad de traducción), debe usar la inlinepalabra 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 inlinepalabra 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 inlinepalabra 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 staticpalabra clave en lugar de la inlinepalabra 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, inlineen general, se debe preferir el uso de.

Una forma alternativa de lograr el mismo resultado que con la staticpalabra 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.

Andy Prowl avatar Feb 16 '2013 11:02 Andy Prowl