Por que meus guardas de inclusão não estão impedindo a inclusão recursiva e várias definições de símbolos?

Duas perguntas comuns sobre incluem guardas :

  1. PRIMEIRA PERGUNTA:

    Por que os protetores não estão protegendo meus arquivos de header da inclusão mútua e recursiva ? Eu continuo recebendo erros sobre símbolos inexistentes que estão obviamente lá ou até mesmo erros de syntax mais estranhos toda vez que eu escrevo algo como o seguinte:

    “ah”

    #ifndef A_H #define A_H #include "bh" ... #endif // A_H 

    “bh”

     #ifndef B_H #define B_H #include "ah" ... #endif // B_H 

    “main.cpp”

     #include "ah" int main() { ... } 

    Por que recebo erros ao compilar “main.cpp”? O que preciso fazer para resolver meu problema?


  1. SEGUNDA QUESTÃO:

    Por que os guardas não estão impedindo várias definições ? Por exemplo, quando meu projeto contém dois arquivos que incluem o mesmo header, às vezes o vinculador reclama que algum símbolo foi definido várias vezes. Por exemplo:

    “header.h”

     #ifndef HEADER_H #define HEADER_H int f() { return 0; } #endif // HEADER_H 

    “source1.cpp”

     #include "header.h" ... 

    “source2.cpp”

     #include "header.h" ... 

    Por que isso está acontecendo? O que preciso fazer para resolver meu problema?

PRIMEIRA PERGUNTA:

Por que os protetores não estão protegendo meus arquivos de header da inclusão mútua e recursiva ?

Eles são .

O que eles não estão ajudando são as dependencies entre as definições de estruturas de dados em headers que incluem mutuamente . Para ver o que isso significa, vamos começar com um cenário básico e ver por que include guardas ajudam com inclusões mútuas.

Suponha que seus arquivos de header mutuamente incluindo ah e bh tenham conteúdo trivial, isto é, as reticências nas seções de código do texto da pergunta são substituídas pela string vazia. Nessa situação, seu main.cpp será compilado com alegria. E isso é só graças aos seus guardas incluem!

Se você não estiver convencido, tente removê-los:

 //================================================ // ah #include "bh" //================================================ // bh #include "ah" //================================================ // main.cpp // // Good luck getting this to compile... #include "ah" int main() { ... } 

Você notará que o compilador relatará uma falha quando atingir o limite de profundidade de inclusão. Esse limite é específico da implementação. De acordo com o parágrafo 16.2 / 6 da norma C ++ 11:

Uma diretiva de pré-processamento #include pode aparecer em um arquivo de origem que foi lido devido a uma diretiva #include em outro arquivo, até um limite de aninhamento definido pela implementação .

Então, o que está acontecendo ?

  1. Ao analisar main.cpp , o pré-processador atenderá à diretiva #include "ah" . Essa diretiva informa ao pré-processador para processar o arquivo de header ah , obter o resultado desse processamento e replace a cadeia de caracteres #include "ah" por esse resultado;
  2. Durante o processamento, o pré-processador atenderá à diretiva #include "bh" , e o mesmo mecanismo será aplicado: o pré-processador processará o arquivo de header bh , bh o resultado de seu processamento e replaceá a diretiva #include por esse resultado;
  3. Ao processar bh , a diretiva #include "ah" dirá ao pré-processador para processar ah e replace essa diretiva pelo resultado;
  4. O pré-processador começará a analisar ah novamente, irá atender a diretiva #include "bh" novamente, e isso irá configurar um processo recursivo potencialmente infinito. Ao atingir o nível de aninhamento crítico, o compilador reportará um erro.

Quando guardas de inclusão estão presentes , no entanto, nenhuma recursion infinita será configurada na etapa 4. Vamos ver porque:

  1. ( mesmo que antes ) Ao analisar main.cpp , o pré-processador atenderá à diretiva #include "ah" . Isso informa ao pré-processador para processar o arquivo de header ah , obter o resultado desse processamento e replace a cadeia de caracteres #include "ah" com esse resultado;
  2. Durante o processamento, o pré-processador atenderá à diretiva #ifndef A_H . Como a macro A_H ainda não foi definida, ela continuará processando o texto a seguir. A diretiva subsequente ( #defines A_H ) define a macro A_H . Em seguida, o pré-processador atenderá à diretiva #include "bh" : o pré-processador deve agora processar o arquivo de header bh , obter o resultado de seu processamento e replace a diretiva #include por esse resultado;
  3. Ao processar bh , o pré-processador atenderá à diretiva #ifndef B_H . Como a macro B_H ainda não foi definida, ela continuará processando o texto a seguir. A diretiva subsequente ( #defines B_H ) define a macro B_H . Em seguida, a diretiva #include "ah" dirá ao pré-processador para processar um ah e replace a diretiva #include em bh pelo resultado do pré-processamento ah ;
  4. O compilador iniciará o pré-processamento de novo ah e atenderá novamente a diretiva #ifndef A_H . No entanto, durante o pré-processamento anterior, a macro A_H foi definida. Portanto, o compilador ignorará o texto a seguir desta vez até que a diretiva #endif correspondente seja encontrada e a saída desse processamento seja a cadeia vazia (supondo que nada siga a diretiva #endif , é claro). Portanto, o pré-processador replaceá a diretiva #include "ah" em bh pela string vazia e rastreará a execução até que ela substitua a diretiva #include original em main.cpp .

Assim, inclua guardas que protegem contra a inclusão mútua . No entanto, eles não podem ajudar com dependencies entre as definições de suas classs em arquivos que incluem mutuamente:

 //================================================ // ah #ifndef A_H #define A_H #include "bh" struct A { }; #endif // A_H //================================================ // bh #ifndef B_H #define B_H #include "ah" struct B { A* pA; }; #endif // B_H //================================================ // main.cpp // // Good luck getting this to compile... #include "ah" int main() { ... } 

Dados os headers acima, main.cpp não irá compilar.

Por que isso está acontecendo?

Para ver o que está acontecendo, basta passar pelas etapas de 1 a 4 novamente.

É fácil ver que os três primeiros passos e a maior parte do quarto passo não são afetados por essa mudança (basta lê-los para se convencer). No entanto, algo diferente acontece no final da etapa 4: depois de replace a diretiva #include "ah" em bh pela string vazia, o pré-processador começará a analisar o conteúdo de bh e, em particular, a definição de B Infelizmente, a definição de B menciona a class A , que nunca foi cumprida exatamente por causa dos guardas de inclusão!

Declarar uma variável de membro de um tipo que não tenha sido declarado anteriormente é, obviamente, um erro, e o compilador irá educadamente apontar isso.

O que preciso fazer para resolver meu problema?

Você precisa de declarações futuras .

Na verdade, a definição de class A não é necessária para definir a class B , porque um ponteiro para A está sendo declarado como uma variável de membro e não um object do tipo A Como os pointers têm tamanho fixo, o compilador não precisará saber o layout exato de A nem calcular seu tamanho para definir adequadamente a class B Portanto, é suficiente para encaminhar declarar class A em bh e tornar o compilador ciente de sua existência:

 //================================================ // bh #ifndef B_H #define B_H // Forward declaration of A: no need to #include "ah" struct A; struct B { A* pA; }; #endif // B_H 

Seu main.cpp agora irá certamente compilar. Algumas observações:

  1. Não apenas quebrar a inclusão mútua substituindo a diretiva #include por uma declaração antecipada em bh foi suficiente para expressar efetivamente a dependência de B em A : usar declarações de encaminhamento sempre que possível / prático também é considerado uma boa prática de programação , porque ajuda evitando inclusões desnecessárias, reduzindo assim o tempo de compilation geral. No entanto, depois de eliminar a inclusão mútua, main.cpp terá que ser modificado para #include ah e bh (se o último for necessário), porque bh não é mais indiretamente #include ah ;
  2. Embora uma declaração antecipada de class A seja suficiente para o compilador declarar pointers para essa class (ou usá-la em qualquer outro contexto em que tipos incompletos sejam aceitáveis), desreferenciando pointers para A (por exemplo para invocar uma function-membro) ou computando seus size são operações ilegais em tipos incompletos: se isso for necessário, a definição completa de A precisa estar disponível para o compilador, o que significa que o arquivo de header que o define deve ser incluído. É por isso que as definições de class e a implementação de suas funções-membro são geralmente divididas em um arquivo de header e um arquivo de implementação para essa class ( modelos de class são uma exceção a essa regra): arquivos de implementação que nunca são #include por outros arquivos o projeto pode #include com segurança #include todos os headers necessários para tornar as definições visíveis. Os arquivos de header, por outro lado, não includeão #include outros arquivos de header, a menos que realmente precisem fazê-lo (por exemplo, para tornar visível a definição de uma class base ) e usarão declarações de encaminhamento sempre que possível / prático.

SEGUNDA QUESTÃO:

Por que os guardas não estão impedindo várias definições ?

Eles são .

O que eles não estão protegendo você é várias definições em unidades de tradução separadas . Isso também é explicado neste Q & A no StackOverflow.

Veja também, tente remover as proteções de inclusão e compilar a seguinte versão modificada de source1.cpp (ou source2.cpp , para o que é importante):

 //================================================ // source1.cpp // // Good luck getting this to compile... #include "header.h" #include "header.h" int main() { ... } 

O compilador certamente irá reclamar aqui sobre f() sendo redefinido. Isso é óbvio: sua definição está sendo incluída duas vezes! No entanto, o source1.cpp acima será compilado sem problemas quando header.h contiver as proteções de inclusão apropriadas . Isso é esperado.

Ainda assim, mesmo quando as guardas de inclusão estiverem presentes e o compilador parar de incomodar com a mensagem de erro, o vinculador insistirá no fato de que várias definições são encontradas ao mesclar o código de object obtido da compilation de source1.cpp e source2.cpp , e se recusará a gerar seu executável.

Por que isso está acontecendo?

Basicamente, cada arquivo .cpp (o termo técnico neste contexto é unidade de tradução ) em seu projeto é compilado separadamente e de forma independente . Ao analisar um arquivo .cpp , o pré-processador processará todas as diretivas #include e expandirá todas as invocações de macro que encontrar, e a saída desse processamento de texto puro será fornecida em input ao compilador para convertê-lo em código de object. Uma vez que o compilador tenha terminado de produzir o código object para uma unidade de tradução, ele prosseguirá com o próximo, e todas as definições de macro encontradas durante o processamento da unidade de tradução anterior serão esquecidas.

De fato, compilar um projeto com n unidades de tradução (arquivos .cpp ) é como executar o mesmo programa (o compilador) n vezes, cada vez com uma input diferente: diferentes execuções do mesmo programa não compartilham o estado do anterior execução do programa (s) . Assim, cada tradução é realizada de forma independente e os símbolos do pré-processador encontrados durante a compilation de uma unidade de tradução não serão lembrados ao compilar outras unidades de tradução (se você pensar por um momento, perceberá que esse é realmente um comportamento desejável).

Portanto, embora as proteções o ajudem a evitar inclusões mútuas recursivas e inclusões redundantes do mesmo header em uma unidade de tradução, elas não podem detectar se a mesma definição está incluída em outra unidade de tradução.

No entanto, ao mesclar o código de object gerado a partir da compilation de todos os arquivos .cpp de seu projeto, o vinculador verá que o mesmo símbolo é definido mais de uma vez e que isso viola a regra de uma definição . De acordo com o parágrafo 3.2 / 3 da norma C ++ 11:

Cada programa deve conter exatamente uma definição de cada function ou variável não-inline que é utilizada neste programa; não é necessário diagnóstico. A definição pode aparecer explicitamente no programa, pode ser encontrada na biblioteca padrão ou definida pelo usuário ou (quando apropriado) é definida implicitamente (veja 12.1, 12.4 e 12.8). Uma function inline deve ser definida em cada unidade de tradução na qual é utilizada .

Portanto, o vinculador emitirá um erro e se recusará a gerar o executável do seu programa.

O que preciso fazer para resolver meu problema?

Se você quiser manter sua definição de function em um arquivo de header que é #include d por várias unidades de tradução (observe que não haverá problema se seu header for #include d apenas por uma unidade de tradução), você precisará usar a palavra-chave inline .

Caso contrário, você precisa manter apenas a declaração de sua function em header.h , colocando sua definição (corpo) em um arquivo .cpp separado (essa é a abordagem clássica).

A palavra-chave inline representa uma solicitação não vinculativa para o compilador para inline o corpo da function diretamente no site de chamada, em vez de configurar um quadro de pilha para uma chamada de function regular. Embora o compilador não precise atender à sua solicitação, a palavra-chave inline não consegue dizer ao vinculador para tolerar várias definições de símbolo. Nos termos do ponto 3.2 / 5 da norma C ++ 11:

Pode haver mais de uma definição de um tipo de class (Cláusula 9), tipo de enumeração (7.2), function inline com binding externa (7.1.2), modelo de class (Cláusula 14), modelo de function não estática (14.5.6) , membro de dados estáticos de um modelo de class (14.5.1.3), function de membro de um modelo de class (14.5.1.1) ou especialização de modelo para a qual alguns parâmetros de modelo não são especificados (14.7, 14.5.5) em um programa, desde que cada A definição aparece em uma unidade de tradução diferente, e desde que as definições satisfaçam os seguintes requisitos […]

O Parágrafo acima lista basicamente todas as definições que são comumente colocadas em arquivos de header , porque elas podem ser incluídas com segurança em várias unidades de tradução. Todas as outras definições com binding externa, em vez disso, pertencem aos arquivos de origem.

Usar a palavra-chave static vez da palavra-chave inline também resulta na supressão dos erros do vinculador, fornecendo à sua function a binding interna , fazendo com que cada unidade de tradução mantenha uma cópia privada dessa function (e de suas variables ​​estáticas locais). No entanto, isso eventualmente resulta em um executável maior, e o uso de inline deve ser preferido em geral.

Uma maneira alternativa de obter o mesmo resultado da palavra-chave static é colocar a function f() em um namespace sem nome . De acordo com o parágrafo 3.5 / 4 da norma C ++ 11:

Um namespace sem nome ou um namespace declarado direta ou indiretamente em um namespace sem nome possui uma binding interna. Todos os outros espaços de nomes possuem binding externa. Um nome com escopo de namespace que não recebeu a vinculação interna acima tem a mesma vinculação que o namespace de fechamento se for o nome de:

– uma variável; ou

uma function ; ou

– uma class nomeada (Cláusula 9) ou uma class sem nome definida em uma declaração typedef na qual a class possui o nome typedef para propósitos de binding (7.1.3); ou

– uma enumeração nomeada (7.2) ou uma enumeração sem nome definida em uma declaração typedef na qual a enumeração possui o nome typedef para propósitos de vinculação (7.1.3); ou

– um enumerador pertencente a uma enumeração com linkage; ou

– Uma amostra.

Pelo mesmo motivo mencionado acima, a palavra-chave inline deve ser preferida.