É uma boa prática contar com a inclusão transitória de cabeçalhos?

37

Estou limpando as inclusões em um projeto C ++ em que estou trabalhando e fico pensando se devo incluir explicitamente todos os cabeçalhos usados ​​diretamente em um arquivo específico ou se devo incluir apenas o mínimo necessário.

Aqui está um exemplo Entity.hpp:

#include "RenderObject.hpp"
#include "Texture.hpp"

struct Entity {
    Texture texture;
    RenderObject render();
}

(Vamos supor que uma declaração de encaminhamento para RenderObjectnão seja uma opção.)

Agora, eu sei que isso RenderObject.hppinclui Texture.hpp- eu sei disso porque cada RenderObjectum tem um Texturemembro. Ainda assim, eu incluir explicitamente Texture.hppno Entity.hpp, porque eu não tenho certeza se é uma boa idéia para contar com ele ser incluído no RenderObject.hpp.

Então: é uma boa prática ou não?

futlib
fonte
19
Onde estão os protetores de inclusão no seu exemplo? Você acabou de esquecê-los acidentalmente, espero?
Doc Brown
3
Um problema que ocorre quando você não inclui todos os arquivos usados ​​é que, às vezes, a ordem em que você inclui os arquivos se torna importante. Isso é realmente irritante apenas no único caso em que isso acontece, mas às vezes bolas de neve e você realmente desejam que a pessoa que escreveu o código assim seja levada à frente de um esquadrão de tiro.
Dunk
É por isso que existem #ifndef _RENDER_H #define _RENDER_H ... #endif.
Sampathsris #
@ Dunk Acho que você não entendeu o problema. Com qualquer uma das sugestões dele, isso não deveria acontecer.
Mooing Duck
11
@DocBrown, #pragma onceresolve isso, não?
Pacerier

Respostas:

65

Você sempre deve incluir todos os cabeçalhos que definem quaisquer objetos usados ​​em um arquivo .cpp nesse arquivo, independentemente do que você sabe sobre o que há nesses arquivos. Você deve incluir guardas em todos os arquivos de cabeçalho para garantir que a inclusão de cabeçalhos várias vezes não importe.

As razões:

  • Isso deixa claro para os desenvolvedores que leem a fonte exatamente o que o arquivo de origem em questão exige. Aqui, alguém olhando as primeiras linhas do arquivo pode ver que você está lidando com Textureobjetos nesse arquivo.
  • Isso evita problemas nos quais cabeçalhos refatorados causam problemas de compilação quando eles não precisam mais de cabeçalhos específicos. Por exemplo, suponha que você perceba que RenderObject.hpprealmente não precisa de Texture.hppsi.

Um corolário é que você nunca deve incluir um cabeçalho em outro cabeçalho, a menos que seja explicitamente necessário nesse arquivo.

Gort the Robot
fonte
10
Concorde com o corolário - com a condição de que SEMPRE inclua o outro cabeçalho se precisar!
Andrew
11
Não gosto da prática de incluir diretamente cabeçalhos para todas as classes individuais. Sou a favor de cabeçalhos cumulativos. Ou seja, acho que o arquivo de alto nível deve fazer referência a um "módulo" de algum tipo que está usando, mas não precisa incluir diretamente todas as partes individuais.
EdA-qa mort-ora-y
8
Isso leva a cabeçalhos grandes e monolíticos que todo arquivo inclui, mesmo quando eles precisam apenas de um pouco do que está nele, o que leva a longos tempos de compilação e dificulta a refatoração.
Gort the Robot
6
O Google criou uma ferramenta para ajudá-lo a aplicar exatamente esse conselho, chamado incluir o que você usa .
Matthew G.
3
O principal problema de tempo de compilação com cabeçalhos monolíticos grandes não é o momento de compilar o próprio código do cabeçalho, mas ter que compilar todos os arquivos cpp em seu aplicativo sempre que o cabeçalho for alterado. Cabeçalhos pré-compilados não ajudam nisso.
Gort the Robot
23

A regra geral é: inclua o que você usa. Se você usar um objeto diretamente, inclua seu arquivo de cabeçalho diretamente. Se você usar um objeto A que use B, mas não use B, inclua apenas Ah

Além disso, enquanto estamos no tópico, você só deve incluir outros arquivos de cabeçalho no arquivo de cabeçalho se realmente precisar dele no cabeçalho. Se você só precisar dele no .cpp, inclua-o apenas lá: esta é a diferença entre uma dependência pública e privada e impedirá que os usuários da sua classe arrastem os cabeçalhos que eles realmente não precisam.

Nir Friedman
fonte
10

Fico me perguntando se devo incluir explicitamente todos os cabeçalhos usados ​​diretamente em um arquivo específico

Sim.

Você nunca sabe quando esses outros cabeçalhos podem mudar. Faz todo o sentido do mundo incluir, em cada unidade de tradução, os cabeçalhos que você conhece que a unidade de tradução precisa.

Temos guardas de cabeçalho para garantir que a inclusão dupla não seja prejudicial.

Corridas de leveza com Monica
fonte
3

As opiniões divergem sobre isso, mas sou de opinião que todo arquivo (seja arquivo de origem c / cpp ou arquivo de cabeçalho h / hpp) deve poder ser compilado ou analisado por si próprio.

Dessa forma, todos os arquivos devem # incluir todos os arquivos de cabeçalho de que precisam - você não deve assumir que um arquivo de cabeçalho já foi incluído anteriormente.

É realmente doloroso se você precisar adicionar um arquivo de cabeçalho e descobrir que ele usa um item definido em outro lugar, sem incluí-lo diretamente ... então você deve procurar (e possivelmente acabar com o errado!)

Por outro lado, (como regra geral) não importa se você # inclui um arquivo que não precisa ...


Como um ponto de estilo pessoal, organizo os arquivos #include em ordem alfabética, divididos em sistema e aplicativo - isso ajuda a reforçar a mensagem "independente e totalmente coerente".

Andrew
fonte
Nota sobre a ordem de inclusões: algumas vezes, a ordem é importante, por exemplo, ao incluir cabeçalhos X11. Isso pode ser devido ao design (que pode, nesse caso, ser considerado um design incorreto); às vezes, devido a problemas de incompatibilidade infelizes.
Hyde
Uma observação sobre a inclusão de cabeçalhos desnecessários, é importante para os tempos de compilação, primeiro diretamente (especialmente se for C ++ com muitos modelos), mas principalmente ao incluir cabeçalhos do mesmo projeto ou de dependência em que o arquivo de inclusão também muda e acionará a recompilação de tudo, inclusive (se você tem dependências de trabalho, se não, então você precisa fazer uma construção limpa o tempo todo ...).
Hyde
2

Depende se essa inclusão transitiva é por necessidade (por exemplo, classe base) ou por causa de um detalhe de implementação (membro privado).

Para esclarecer, a inclusão transitiva é necessária quando a remoção é feita somente após a primeira alteração das interfaces declaradas no cabeçalho intermediário. Como essa já é uma alteração inabalável, qualquer arquivo .cpp que o utilize deve ser verificado de qualquer maneira.

Exemplo: Ah é incluído por Bh que é usado por C.cpp. Se Bh usou Ah para alguns detalhes de implementação, C.cpp não deve assumir que Bh continuará a fazê-lo. Mas se Bh usa Ah para uma classe base, C.cpp pode assumir que Bh continuará incluindo os cabeçalhos relevantes para suas classes base.

Você vê aqui a vantagem real de NÃO duplicar inclusões de cabeçalho. Digamos que a classe base usada por Bh realmente não pertença a Ah e seja refatorada no próprio Bh. Bh agora é um cabeçalho independente. Se o C.cpp incluiu redundantemente Ah, agora ele inclui um cabeçalho desnecessário.

MSalters
fonte
2

Pode haver outro caso: você tem Ah, Bh e seu C.cpp, Bh inclui Ah

então em C.cpp, você pode escrever

#include "B.h"
#include "A.h" // < this can be optional as B.h already has all the stuff in A.h

Então, se você não escrever #include "Ah" aqui, o que pode acontecer? no seu C.cpp, são usados ​​A e B (por exemplo, classe). Mais tarde, você alterou seu código C.cpp, remove os itens relacionados ao B, mas deixa o Bh incluído lá.

Se você incluir Ah e Bh e agora, neste momento, as ferramentas que detectam inclusões desnecessárias podem ajudá-lo a apontar que a inclusão de Bh não é mais necessária. Se você incluir apenas Bh como acima, será difícil para ferramentas / humanos detectar a inclusão desnecessária após a alteração do código.

insetos rei
fonte
1

Estou adotando uma abordagem ligeiramente diferente das respostas propostas.

Nos cabeçalhos, sempre inclua apenas um mínimo, apenas o necessário para fazer a compilação passar. Use a declaração de encaminhamento sempre que possível.

Nos arquivos de origem, não é tão importante quanto você inclui. Minhas preferências ainda devem incluir o mínimo para que seja aprovado.

Para pequenos projetos, incluindo cabeçalhos aqui e ali, não fará diferença. Mas para projetos de médio a grande porte, isso pode se tornar um problema. Mesmo que o hardware mais recente seja usado para compilar, a diferença pode ser perceptível. O motivo é que o compilador ainda precisa abrir o cabeçalho incluído e analisá-lo. Portanto, para otimizar a compilação, aplique a técnica acima (inclua o mínimo necessário e use a declaração a frente).

Embora um pouco desatualizado, o Design de software C ++ em larga escala (de John Lakos) explica tudo isso em detalhes.

BЈовић
fonte
11
Não concorde com esta estratégia ... se você incluir um arquivo de cabeçalho em um arquivo de origem, precisará rastrear todas as suas dependências. É melhor incluir diretamente, do que tentar documentar a lista!
Andrew
@ Andrew, existem ferramentas e scripts para verificar o que e quantas vezes estão incluídos.
BЈовић
11
Notei otimização em alguns dos compiladores mais recentes para lidar com isso. Eles reconhecem uma declaração de guarda típica e a processam. Então, ao # incluí-lo novamente, eles podem otimizar completamente o carregamento do arquivo. No entanto, sua recomendação de declarações avançadas é muito sábia para reduzir o número de inclusões. Depois que você começa a usar declarações de encaminhamento, ele se torna um equilíbrio entre o tempo de execução do compilador (aprimorado pelas declarações de encaminhamento) e a facilidade de uso (aprimorada por #includes extras convenientes), que é um equilíbrio que cada empresa define de maneira diferente.
Cort Ammon
11
@CortAmmon Um cabeçalho típico tem incluem guardas, mas o compilador ainda tem para abri-lo, e que é uma operação lenta
BЈовић
4
@ BЈовић: Na verdade, eles não. Tudo o que eles precisam fazer é reconhecer que o arquivo possui proteções de cabeçalho "típicas" e sinalizá-las para que ele seja aberto apenas uma vez. Gcc, por exemplo, tem documentação sobre quando e onde ele se aplica essa otimização: gcc.gnu.org/onlinedocs/cppinternals/Guard-Macros.html
Cort Ammon
-4

A boa prática é não se preocupar com sua estratégia de cabeçalho, desde que seja compilada.

A seção de cabeçalho do seu código é apenas um bloco de linhas que ninguém deve olhar até que você obtenha um erro de compilação facilmente resolvido. Entendo o desejo de um estilo "correto", mas nenhuma das maneiras pode realmente ser descrita como correta. A inclusão de um cabeçalho para cada classe tem mais probabilidade de causar erros irritantes de compilação com base em ordem, mas esses erros de compilação também refletem problemas que uma codificação cuidadosa poderia corrigir (embora, sem dúvida, não valha a pena consertar).

E sim, você terá esses problemas com base em pedidos assim que começar a entrar em friendterra.

Você pode pensar no problema em dois casos.


Caso 1: você tem um pequeno número de classes interagindo entre si, digamos menos de uma dúzia. Você adiciona, remove e modifica regularmente esses cabeçalhos de maneiras que possam afetar suas dependências um do outro. Este é o caso que o seu exemplo de código sugere.

O conjunto de cabeçalhos é pequeno o suficiente para não ser complicado resolver quaisquer problemas que surjam. Quaisquer problemas difíceis são corrigidos reescrevendo um ou dois cabeçalhos. Preocupar-se com sua estratégia de cabeçalho é resolver problemas que não existem.


Caso 2: você tem dezenas de aulas. Algumas das classes representam a espinha dorsal do seu programa, e reescrever seus cabeçalhos forçaria você a reescrever / recompilar uma grande quantidade de sua base de código. Outras classes usam esse backbone para realizar as coisas. Isso representa uma configuração comercial típica. Os cabeçalhos estão espalhados pelos diretórios e você não consegue se lembrar realisticamente dos nomes de tudo.

Solução: Nesse momento, você precisa pensar em suas classes em grupos lógicos e coletar esses grupos em cabeçalhos que impedem que você tenha que repetir #includevárias vezes. Isso não apenas torna a vida mais simples, como também é uma etapa necessária para tirar proveito dos cabeçalhos pré-compilados .

Você acaba tendo #includeaulas que não precisa, mas quem se importa ?

Nesse caso, seu código seria semelhante a ...

#include <Graphics.hpp>

struct Entity {
    Texture texture;
    RenderObject render();
}
QuestionC
fonte
13
Eu precisei disso porque acredito sinceramente que qualquer sentença na forma de "Boas práticas é não se preocupar com sua estratégia de ____, desde que compilada" leve as pessoas a julgarem mal. Eu descobri que a abordagem leva muito rapidamente à ilegibilidade, e a ilegibilidade é QUASE tão ruim quanto "não funciona". Também encontrei muitas bibliotecas importantes que discordam dos resultados de ambos os casos que você descreve. Como exemplo, o Boost FAZ os cabeçalhos de "coleções" que você recomenda no caso 2, mas eles também são muito importantes em fornecer cabeçalhos de classe por classe para quando você precisar deles.
Cort Ammon
3
Eu testemunhei pessoalmente que "não se preocupe se compilar" se transformar em "nosso aplicativo leva 30 minutos para ser compilado quando você adiciona um valor a uma enumeração, como diabos vamos consertar isso !?"
Gort the Robot
Abordei a questão do tempo de compilação na minha resposta. De fato, minha resposta é uma das únicas duas (nenhuma das quais obteve boa pontuação). Mas, na verdade, isso é tangencial à pergunta do OP; este é um "Devo colocar em camelo meus nomes de variáveis?" digite a pergunta. Sei que minha resposta é impopular, mas nem sempre é uma prática recomendada para tudo, e esse é um desses casos.
QuestionC
Concorde com o nº 2. Quanto às idéias anteriores - espero que a automação atualize o bloco de cabeçalho local - até então defendo uma lista completa.
chux - Restabelece Monica 3/15
A abordagem "incluir tudo e pia da cozinha" pode economizar algum tempo inicialmente - seus arquivos de cabeçalho podem até parecer menores (já que a maioria das coisas é incluída indiretamente de ... em algum lugar). Até você chegar ao ponto em que qualquer alteração em qualquer lugar causa uma recompilação de mais de 30 minutos do seu projeto. E seu preenchimento automático inteligente para IDE traz centenas de sugestões irrelevantes. E você acidentalmente mistura duas classes ou funções estáticas com nomes semelhantes. E você adiciona uma nova estrutura, mas a compilação falha porque você tem uma colisão de namespace com uma classe totalmente não relacionada em algum lugar ...
CharonX