É uma boa prática colocar definições de C ++ em arquivos de cabeçalho?

196

Meu estilo pessoal com C ++ sempre coloca declarações de classe em um arquivo de inclusão e definições em um arquivo .cpp, como estipulado na resposta de Loki para arquivos de cabeçalho em C ++, separação de código . É certo que parte da razão de eu gostar desse estilo provavelmente tem a ver com todos os anos que passei codificando o Modula-2 e o Ada, ambos com um esquema semelhante com arquivos de especificação e arquivos corporais.

Eu tenho um colega de trabalho, muito mais experiente em C ++ do que eu, que insiste em que todas as declarações de C ++ devem, sempre que possível, incluir as definições existentes no arquivo de cabeçalho. Ele não está dizendo que esse é um estilo alternativo válido, ou mesmo um estilo um pouco melhor, mas esse é o novo estilo universalmente aceito que todo mundo está usando no C ++.

Eu não sou tão flexível quanto costumava ser, então não estou realmente ansiosa para me meter nesse vagão dele até ver mais algumas pessoas lá em cima com ele. Então, quão comum é esse idioma realmente?

Só para dar alguma estrutura às respostas: agora é o Caminho , muito comum, um tanto comum, incomum ou maluco?

TED
fonte
funções de uma linha (getters e setters) no cabeçalho são comuns. Mais do que receberia um segundo olhar interrogativo. Talvez para a definição completa de uma classe pequena que é usada apenas por outra no mesmo cabeçalho?
276 Martin
Eu sempre coloquei todas as minhas definições de classe nos cabeçalhos até agora. somente definições para classes pimpl são as exceções. Eu apenas declaro aqueles em cabeçalhos.
Johannes Schaub - litb
3
Talvez ele pense assim, porque é assim que o Visual C ++ insiste que esse código seja escrito. Quando você clica em um botão, a implementação é gerada no arquivo de cabeçalho. Eu não sei por que a Microsoft incentivaria isso, embora pelas razões que outros explicaram abaixo.
WKS
4
@WKS - A Microsoft prefere que todos programam em C # e, em C #, não há distinção "cabeçalho" x "corpo", é apenas um arquivo. Tendo estado nos mundos C ++ e C # há muito tempo, o modo C # é realmente muito mais fácil de lidar.
Mark Lakata
1
@ MarkLakata - Essa é realmente uma das coisas que ele apontou. Eu não ouvi esse argumento ultimamente, mas no IIRC ele estava argumentando que Java e C # funcionam dessa maneira, e o C # era novo na época, o que tornou uma tendência que todas as linguagens seguirão em breve
TED

Respostas:

209

Seu colega de trabalho está errado, a maneira comum é e sempre foi colocar o código nos arquivos .cpp (ou em qualquer extensão que você desejar) e nas declarações nos cabeçalhos.

Ocasionalmente, há algum mérito em colocar o código no cabeçalho, isso pode permitir inlining mais inteligente pelo compilador. Mas, ao mesmo tempo, isso pode destruir seus tempos de compilação, pois todo o código deve ser processado toda vez que for incluído pelo compilador.

Por fim, muitas vezes é irritante ter relações com objetos circulares (às vezes desejado) quando todo o código é o cabeçalho.

Bottom line, você estava certo, ele está errado.

Edição: Eu estive pensando sobre sua pergunta. Há um caso em que o que ele diz é verdade. modelos. Muitas bibliotecas "modernas" mais recentes, como o boost, fazem uso pesado de modelos e geralmente são "apenas cabeçalho". No entanto, isso só deve ser feito ao lidar com modelos, pois é a única maneira de fazê-lo ao lidar com eles.

Edição: algumas pessoas gostariam de um pouco mais de esclarecimento, aqui estão algumas reflexões sobre a desvantagem de escrever o código "somente cabeçalho":

Se você pesquisar, verá muitas pessoas tentando encontrar uma maneira de reduzir o tempo de compilação ao lidar com o aumento. Por exemplo: Como reduzir o tempo de compilação com o Boost Asio , que está vendo uma compilação de 14s de um único arquivo 1K com o impulso incluído. Os 14s podem não parecer "explodir", mas certamente são muito mais longos do que o típico e podem aumentar rapidamente. Ao lidar com um grande projeto. As bibliotecas apenas de cabeçalho afetam os tempos de compilação de maneira bastante mensurável. Nós apenas toleramos porque o impulso é muito útil.

Além disso, há muitas coisas que não podem ser feitas apenas nos cabeçalhos (até o boost tem bibliotecas às quais você precisa vincular determinadas partes, como threads, sistema de arquivos, etc.). Um exemplo principal é que você não pode ter objetos globais simples apenas nas bibliotecas de cabeçalho (a menos que recorra à abominação que é um singleton), pois você encontrará erros de definição múltipla. NOTA: As variáveis ​​embutidas do C ++ 17 tornarão esse exemplo específico possível no futuro.

Como ponto final, ao usar o boost como um exemplo de código apenas de cabeçalho, um grande detalhe geralmente é esquecido.

O impulso é uma biblioteca, não um código no nível do usuário. para que não mude com tanta frequência. No código do usuário, se você colocar tudo nos cabeçalhos, cada pequena alteração fará com que você precise recompilar todo o projeto. Isso é um monumental desperdício de tempo (e não é o caso de bibliotecas que não mudam de compilação para compilação). Quando você divide as coisas entre cabeçalho / origem e, melhor ainda, usa declarações de encaminhamento para reduzir inclusões, você pode economizar horas de recompilação quando somadas ao longo de um dia.

Evan Teran
fonte
16
Tenho certeza de que é de onde ele está tirando isso. Sempre que isso acontece, ele exibe modelos. Seu argumento é basicamente que você deve fazer todo o código dessa maneira para obter consistência.
TED
14
isso é um argumento pobre que ele está fazendo, para manter suas armas :)
Evan Teran
11
As definições de modelo podem estar em arquivos CPP se a palavra-chave "export" for suportada. Esse é um canto obscuro do C ++ que geralmente nem é implementado pela maioria das compilações, pelo que sei.
Andrei Taranchenko 24/02/09
2
Ver a parte inferior desta resposta (o topo é um tanto complicado) para um exemplo: stackoverflow.com/questions/555330/...
Evan Teran
3
Começa a ser significativo para esta discussão em "Hooray, sem erros de vinculação".
Evan Teran
158

No dia em que os codificadores C ++ concordam com o Caminho , os cordeiros se deitam com os leões, os palestinos abraçam os israelenses e os gatos e os cães podem se casar.

A separação entre arquivos .h e .cpp é arbitrária neste momento, um vestígio de otimizações do compilador há muito tempo. Para mim, as declarações pertencem ao cabeçalho e as definições pertencem ao arquivo de implementação. Mas isso é apenas hábito, não religião.

Sim - aquele Jake.
fonte
140
"No dia em que os codificadores C ++ concordarem com o The Way ...", restará apenas um codificador C ++!
27711 Brian Brian Brink
9
Eu pensei que eles já concordam com o modo, as declarações em .h e definições em .cpp
Hasen
6
Somos todos cegos e o C ++ é um elefante.
Roderick Taylor
hábito? então, o que dizer de usar .h para definir o escopo? por qual coisa foi substituída?
Hernán Eche
28

O código nos cabeçalhos geralmente é uma má ideia, pois força a recompilação de todos os arquivos que incluem o cabeçalho quando você altera o código real em vez das declarações. Isso também desacelerará a compilação, pois você precisará analisar o código em todos os arquivos que incluem o cabeçalho.

Um motivo para ter código nos arquivos de cabeçalho é que geralmente é necessário que a palavra-chave inline funcione corretamente e ao usar modelos instanciados em outros arquivos cpp.

Laserallan
fonte
1
"força a recompilação de todos os arquivos que incluem o cabeçalho quando você altera o código real em vez das declarações" Acho que esse é o motivo mais genuíno; também acompanha o fato de que as declarações nos cabeçalhos mudam com menos frequência do que a implementação nos arquivos .c.
Ninad
20

O que pode estar informando o colega de trabalho é uma noção de que a maioria dos códigos C ++ deve ser modelada para permitir a máxima usabilidade. E se estiver modelado, tudo precisará estar em um arquivo de cabeçalho, para que o código do cliente possa vê-lo e instancia-lo. Se é bom o suficiente para o Boost e o STL, é bom o suficiente para nós.

Não concordo com esse ponto de vista, mas pode ser de onde vem.

JohnMcG
fonte
Eu acho que você está certo sobre isso. Quando discutimos, ele está sempre usando o exemplo de modelos, onde você mais ou menos precisa fazer isso. Também não concordo com o "preciso", mas minhas alternativas são bastante complicadas.
TED
1
@ted - para código de modelo, você precisa colocar a implementação no cabeçalho. A palavra-chave 'export' permite que um compilador suporte separação de declaração e definição de modelos, mas o suporte à exportação é muito inexistente. anubis.dkuug.dk/jtc1/sc22/wg21/docs/papers/2003/n1426.pdf
Michael Burr
Um cabeçalho, sim, mas não precisa ser o mesmo cabeçalho. Veja a resposta do desconhecido abaixo.
TED
Isso faz sentido, mas não posso dizer que já deparei com esse estilo antes.
22711 Michael Burr
14

Acho que seu colega de trabalho é inteligente e você também está correto.

As coisas úteis que achei que colocar tudo nos cabeçalhos são as seguintes:

  1. Não há necessidade de escrever e sincronizar cabeçalhos e fontes.

  2. A estrutura é simples e nenhuma dependência circular força o codificador a criar uma estrutura "melhor".

  3. Portátil, fácil de incorporar a um novo projeto.

Concordo com o problema do tempo de compilação, mas acho que devemos notar que:

  1. É muito provável que a alteração do arquivo de origem altere os arquivos de cabeçalho que levam a todo o projeto a ser recompilado novamente.

  2. A velocidade de compilação é muito mais rápida do que antes. E se você tem um projeto a ser construído com muito tempo e alta frequência, isso pode indicar que o design do seu projeto apresenta falhas. Separe as tarefas em diferentes projetos e módulos, para evitar esse problema.

Por fim, eu só quero apoiar seu colega de trabalho, apenas na minha opinião pessoal.

XU Bin
fonte
3
+1. Ninguém, exceto você, teve a idéia de que, em um cabeçalho, apenas um projeto longo pode resultar em muitas dependências, o que é um design ruim. Bom ponto! Mas essas dependências podem ser removidas a ponto de o tempo de compilação ser realmente curto?
TobiMcNamobi
@TobiMcNamobi: Adoro a idéia de "relaxar" para obter um melhor feedback sobre as más decisões de design. No entanto, no caso de cabeçalho somente vs compilado separadamente, se optarmos por essa ideia, terminaremos com uma única unidade de compilação e grandes tempos de compilação. Mesmo quando o design é realmente ótimo.
Jo #
Em outras palavras, a separação entre interface e implementação faz parte do seu design. Em C, você precisa expressar suas decisões sobre o encapsulamento por meio de separação no cabeçalho e implementação.
Jo #
1
Estou começando a me perguntar se há alguma desvantagem em simplesmente remover os cabeçalhos completamente, como as línguas modernas.
Jo #
12

Frequentemente, colocarei funções de membro triviais no arquivo de cabeçalho, para permitir que elas sejam incorporadas. Mas, para colocar todo o corpo do código lá, apenas para ser consistente com os modelos? Isso é loucura.

Lembre-se: uma consistência tola é o hobgoblin das mentes pequenas .

Mark Ransom
fonte
Sim, eu também faço isso. A regra geral que eu uso parece ser algo parecido com "se ele se encaixa em uma linha de código, deixe no cabeçalho".
TED
O que acontece quando uma biblioteca fornece o corpo de uma classe de modelo A<B>em um arquivo cpp e o usuário deseja um A<C>?
JWW
@jww Eu não o declarei explicitamente, mas as classes de modelo devem ser totalmente definidas nos cabeçalhos, para que o compilador possa instancia-lo com os tipos que precisar. Esse é um requisito técnico, não uma escolha estilística. Acho que a questão na pergunta original é que alguém decidiu se era bom para modelos, também para aulas regulares.
Mark Ransom
7

Como Tuomas disse, seu cabeçalho deve ser mínimo. Para ser completo, expandirei um pouco.

Eu pessoalmente uso 4 tipos de arquivos em meus C++projetos:

  • Público:
  • Cabeçalho de encaminhamento: no caso de modelos etc., esse arquivo obtém as declarações de encaminhamento que aparecerão no cabeçalho.
  • Cabeçalho: este arquivo inclui o cabeçalho de encaminhamento, se houver, e declara tudo o que eu desejo que seja público (e define as classes ...)
  • Privado:
  • Cabeçalho privado: esse arquivo é um cabeçalho reservado para implementação, inclui o cabeçalho e declara as funções / estruturas auxiliares (para o Pimpl, por exemplo, ou predicados). Pule se desnecessário.
  • Arquivo de origem: inclui o cabeçalho privado (ou cabeçalho, se não houver cabeçalho privado) e define tudo (sem modelo ...)

Além disso, associo isso a outra regra: não defina o que você pode encaminhar declarar. Embora, é claro, eu seja razoável lá (usar o Pimpl em todos os lugares é um aborrecimento).

Isso significa que eu prefiro uma declaração direta a uma #include diretiva nos meus cabeçalhos sempre que eu puder me safar deles.

Por fim, também uso uma regra de visibilidade: limite os escopos dos meus símbolos o máximo possível, para que eles não poluam os escopos externos.

Juntando tudo:

// example_fwd.hpp
// Here necessary to forward declare the template class,
// you don't want people to declare them in case you wish to add
// another template symbol (with a default) later on
class MyClass;
template <class T> class MyClassT;

// example.hpp
#include "project/example_fwd.hpp"

// Those can't really be skipped
#include <string>
#include <vector>

#include "project/pimpl.hpp"

// Those can be forward declared easily
#include "project/foo_fwd.hpp"

namespace project { class Bar; }

namespace project
{
  class MyClass
  {
  public:
    struct Color // Limiting scope of enum
    {
      enum type { Red, Orange, Green };
    };
    typedef Color::type Color_t;

  public:
    MyClass(); // because of pimpl, I need to define the constructor

  private:
    struct Impl;
    pimpl<Impl> mImpl; // I won't describe pimpl here :p
  };

  template <class T> class MyClassT: public MyClass {};
} // namespace project

// example_impl.hpp (not visible to clients)
#include "project/example.hpp"
#include "project/bar.hpp"

template <class T> void check(MyClass<T> const& c) { }

// example.cpp
#include "example_impl.hpp"

// MyClass definition

O salva-vidas aqui é que na maioria das vezes o cabeçalho para a frente é inútil: só é necessário em caso de typedefou templatee assim é o cabeçalho implementação;)

Matthieu M.
fonte
6

Para adicionar mais diversão, você pode adicionar .ipparquivos que contêm a implementação do modelo (que está sendo incluído .hpp), enquanto .hppcontém a interface.

Além do código de modelo (dependendo do projeto, pode ser a maioria ou a minoria de arquivos), existe um código normal e aqui é melhor separar as declarações e as definições. Forneça também declarações avançadas sempre que necessário - isso pode afetar o tempo de compilação.

Anônimo
fonte
Foi o que eu levei a fazer com as definições de modelo também (embora não tenha certeza de que usei a mesma extensão ... já faz um tempo).
TED
5

Geralmente, ao escrever uma nova classe, colocarei todo o código na classe, para que não precise procurar em outro arquivo. Depois que tudo estiver funcionando, divido o corpo dos métodos no arquivo cpp , deixando os protótipos no arquivo hpp.

EvilTeach
fonte
4

Se esse novo caminho for realmente o Caminho , poderíamos estar correndo em direções diferentes em nossos projetos.

Porque tentamos evitar todas as coisas desnecessárias nos cabeçalhos. Isso inclui evitar cascata de cabeçalho. O código nos cabeçalhos provavelmente precisará incluir outro cabeçalho, que precisará de outro cabeçalho e assim por diante. Se formos forçados a usar modelos, tentamos evitar o excesso de cabeçalhos com itens de modelo.

Também usamos o padrão "ponteiro opaco" quando aplicável.

Com essas práticas, podemos criar versões mais rápidas do que a maioria de nossos colegas. E sim ... alterar o código ou os membros da classe não causará grandes reconstruções.

Virne
fonte
4

Eu pessoalmente faço isso nos meus arquivos de cabeçalho:

// class-declaration

// inline-method-declarations

Não gosto de misturar o código para os métodos com a classe, pois acho difícil pesquisar rapidamente.

Eu não colocaria TODOS os métodos no arquivo de cabeçalho. O compilador (normalmente) não será capaz de alinhar métodos virtuais e (provavelmente) apenas alinhará pequenos métodos sem loops (depende totalmente do compilador).

Fazer os métodos na classe é válido ... mas, de um ponto de vista legível, eu não gosto. Colocar os métodos no cabeçalho significa que, quando possível, eles serão incorporados.

TofuBeer
fonte
2

IMHO, ele só tem mérito se estiver fazendo modelos e / ou metaprogramação. Muitos motivos já mencionados mencionam que você limita os arquivos de cabeçalho a apenas declarações. Eles são apenas isso ... cabeçalhos. Se você deseja incluir código, compile-o como uma biblioteca e vincule-o.

Spoulson
fonte
2

Coloquei toda a implementação fora da definição de classe. Eu quero ter os comentários doxygen fora da definição de classe.

Jesus Fernandez
fonte
1
Eu sei que é tarde, mas os que rejeitam (ou simpatizam) se importam em comentar por quê? Parece-me uma afirmação razoável. Usamos o Doxygen, e o problema certamente surgiu.
TED
2

Eu acho que é absolutamente absurdo colocar TODAS as suas definições de função no arquivo de cabeçalho. Por quê? Porque o arquivo de cabeçalho é usado como a interface PUBLIC para sua classe. É o lado de fora da "caixa preta".

Quando você precisar examinar uma classe para referenciar como usá-la, consulte o arquivo de cabeçalho. O arquivo de cabeçalho deve fornecer uma lista do que pode ser feito (comentado para descrever os detalhes de como usar cada função) e incluir uma lista das variáveis ​​de membro. NÃO DEVE incluir COMO cada função individual é implementada, porque é uma carga de informações desnecessárias e apenas atravessa o arquivo de cabeçalho.

SeanRamey
fonte
1

Isso realmente não depende da complexidade do sistema e das convenções internas?

No momento, estou trabalhando em um simulador de rede neural incrivelmente complexo, e o estilo aceito que devo usar é:

Definições de classe em classname.h
Código de classe em classnameCode.h
código executável em classname.cpp

Isso divide as simulações criadas pelo usuário das classes base criadas pelo desenvolvedor e funciona melhor na situação.

No entanto, ficaria surpreso ao ver as pessoas fazendo isso em, digamos, um aplicativo gráfico ou qualquer outro aplicativo que não tenha o objetivo de fornecer aos usuários uma base de código.

Ed James
fonte
1
Qual é exatamente a distinção entre "código de classe" e "código executável"?
TED
Como eu disse, é um simulador neural: o usuário cria simulações executáveis ​​que são construídas em um grande número de classes que atuam como neurônios etc. isso faz o simulador fazer coisas.
26426 Ed James
Geralmente, você não poderia dizer "realmente não pode fazer nada por si só" para a grande maioria (se não a totalidade) da maioria dos programas? Você está dizendo que o código "principal" entra em um cpp, mas nada mais funciona?
TED
Nesta situação, é um pouco diferente. O código que escrevemos é basicamente uma biblioteca, e o usuário cria suas simulações em cima disso, que são realmente executáveis. Pense nisso como o openGL -> você obtém várias funções e objetos, mas sem um arquivo cpp que possa executá-los, eles são inúteis.
22426 James Ed Ed
0

O código do modelo deve estar apenas nos cabeçalhos. Além disso, todas as definições, exceto as linhas, devem estar em .cpp. O melhor argumento para isso seria as implementações da biblioteca std que seguem a mesma regra. Você não discordaria que os desenvolvedores da std lib estariam certos quanto a isso.

Abhishek Agarwal
fonte
Quais stdlibs? libstdc++Parece que o GCC (AFAICS) coloca quase nada srce quase tudo include, independentemente de ele "estar" em um cabeçalho. Portanto, não acho que seja uma citação precisa / útil. De qualquer forma, não acho que o stdlibs seja um modelo para o código do usuário: eles são obviamente escritos por codificadores altamente qualificados, mas para serem usados , e não lidos: eles abstraem a alta complexidade que a maioria dos codificadores não deveria pensar. , precisam de coisas feias em _Reserved __namestodos os lugares para evitar conflitos com o usuário, comentários e espaçamento estão abaixo do que eu recomendaria etc. Eles são exemplares de maneira estreita.
Underscore_d
0

Acho que seu colega de trabalho está certo, desde que ele não entre no processo para escrever o código executável no cabeçalho. O equilíbrio certo, eu acho, é seguir o caminho indicado pelo GNAT Ada, onde o arquivo .ads fornece uma definição de interface perfeitamente adequada do pacote para seus usuários e seus filhos.

A propósito, Ted, você já viu neste fórum a pergunta recente sobre a ligação Ada à biblioteca CLIPS que você escreveu há vários anos e que não está mais disponível (as páginas da Web relevantes agora estão fechadas). Mesmo se feita para uma versão antiga do Clips, essa ligação pode ser um bom exemplo de início para alguém disposto a usar o mecanismo de inferência CLIPS em um programa Ada 2012.

Emile
fonte
1
Ri muito. Dois anos depois, essa é uma maneira estranha de se apossar de alguém. Vou verificar se ainda tenho uma cópia, mas provavelmente não. Fiz isso para uma aula de IA para que eu pudesse fazer meu código na Ada, mas propositadamente fiz esse projeto CC0 (essencialmente sem direitos autorais) na esperança de que alguém o descaradamente aceitasse e fizesse algo com ele.
TED