Normalmente, ao declarar uma classe C ++, é recomendável colocar apenas a declaração no arquivo de cabeçalho e colocar a implementação em um arquivo de origem. No entanto, parece que esse modelo de design não funciona para classes de modelo.
Ao procurar on-line, parece haver duas opiniões sobre a melhor maneira de gerenciar classes de modelo:
1. Declaração e implementação completas no cabeçalho.
Isso é bastante simples, mas leva ao que é, na minha opinião, difícil de manter e editar arquivos de código quando o modelo se torna grande.
2. Escreva a implementação em um arquivo de inclusão de modelo (.tpp) incluído no final.
Esta parece ser uma solução melhor para mim, mas não parece ser amplamente aplicada. Existe uma razão para que essa abordagem seja inferior?
Eu sei que muitas vezes o estilo do código é ditado pela preferência pessoal ou pelo estilo legado. Estou iniciando um novo projeto (portando um projeto C antigo para C ++) e sou relativamente novo no design de OO e gostaria de seguir as melhores práticas desde o início.
fonte
Respostas:
Ao escrever uma classe C ++ com modelo, você geralmente tem três opções:
(1) Coloque declaração e definição no cabeçalho.
ou
Pró:
Vigarista:
Foo
como membro, precisará incluirfoo.h
. Isso significa que alterar a implementação deFoo::f
propaga-se através dos arquivos de cabeçalho e de origem.Vamos analisar mais detalhadamente o impacto da reconstrução: Para classes C ++ sem modelo, você coloca declarações em. He definições de método em .cpp. Dessa forma, quando a implementação de um método é alterada, apenas um .cpp precisa ser recompilado. Isso é diferente para as classes de modelo se o .h contiver todo o código. Veja o seguinte exemplo:
Aqui, o único uso de
Foo::f
está dentrobar.cpp
. No entanto, se você alterar a implementação deFoo::f
, bothbar.cpp
equx.cpp
precisar ser recompilado. A implementação deFoo::f
vidas em ambos os arquivos, mesmo que nenhuma parteQux
use diretamente nadaFoo::f
. Para projetos grandes, isso pode se tornar um problema em breve.(2) Coloque a declaração em .h e a definição em .tpp e inclua-a em .h.
Pró:
Vigarista:
Essa solução separa a declaração e a definição do método em dois arquivos separados, assim como .h / .cpp. No entanto, essa abordagem tem o mesmo problema de reconstrução que (1) , porque o cabeçalho inclui diretamente as definições de método.
(3) Coloque a declaração em. He a definição em .tpp, mas não inclua .tpp em .h.
Pró:
Vigarista:
Foo
membro a uma classeBar
, você precisa incluirfoo.h
no cabeçalho. Se você chamarFoo::f
um .cpp, também precisará incluirfoo.tpp
lá.Essa abordagem reduz o impacto da reconstrução, pois apenas os arquivos .cpp que realmente usam
Foo::f
precisam ser recompilados. No entanto, isso tem um preço: todos esses arquivos precisam incluirfoo.tpp
. Pegue o exemplo acima e use a nova abordagem:Como você pode ver, a única diferença é a inclusão adicional de
foo.tpp
polbar.cpp
. Isso é inconveniente e adicionar uma segunda inclusão para uma classe, dependendo se você chama métodos, parece muito feio. No entanto, você reduz o impacto da reconstrução: sóbar.cpp
precisa ser recompilado se você alterar a implementação deFoo::f
. O arquivoqux.cpp
não precisa de recompilação.Resumo:
Se você implementar uma biblioteca, normalmente não precisará se preocupar com o impacto da reconstrução. Os usuários da sua biblioteca agarram um release e o utilizam, e a implementação da biblioteca não muda no trabalho diário do usuário. Nesses casos, a biblioteca pode usar a abordagem (1) ou (2) e é apenas uma questão de gosto que você escolher.
No entanto, se você estiver trabalhando em um aplicativo ou em uma biblioteca interna da sua empresa, o código mudará frequentemente. Então você precisa se preocupar com o impacto da reconstrução. Escolher a abordagem (3) pode ser uma boa opção se você conseguir que seus desenvolvedores aceitem a inclusão adicional.
fonte
Semelhante à
.tpp
idéia (que nunca vi usada), colocamos a maioria das funcionalidades embutidas em um-inl.hpp
arquivo incluído no final do.hpp
arquivo usual .Como outros indicam, isso mantém a interface legível movendo a desordem das implementações embutidas (como modelos) em outro arquivo. Permitimos algumas linhas de interface, mas tentamos limitá-las a funções pequenas, geralmente de linha única.
fonte
Uma moeda profissional da 2ª variante é que seus cabeçalhos parecem mais organizados.
O engodo pode ser que você pode ter a verificação de erro do IDE embutido e as ligações do depurador estragadas.
fonte
Eu prefiro muito a abordagem de colocar a implementação em um arquivo separado e ter apenas a documentação e as declarações no arquivo de cabeçalho.
Talvez a razão pela qual você não tenha visto muito essa abordagem seja muito prática, seja por não ter procurado os lugares certos ;-)
Ou - talvez seja porque é preciso um pouco de esforço extra no desenvolvimento do software. Mas para uma biblioteca de classes, esse esforço vale MUITO, IMHO, e se paga em uma biblioteca muito mais fácil de usar / ler.
Veja esta biblioteca, por exemplo: https://github.com/SophistSolutions/Stroika/
A biblioteca inteira é escrita com essa abordagem e, se você examinar o código, verá como ele funciona.
Os arquivos de cabeçalho têm o tamanho dos arquivos de implementação, mas são preenchidos apenas com declarações e documentação.
Compare a legibilidade do Stroika com a sua implementação std c ++ favorita (gcc ou libc ++ ou msvc). Todos eles usam a abordagem de implementação embutida no cabeçalho e, embora sejam extremamente bem escritos, IMHO, não são implementações legíveis.
fonte