Maneiras de organizar interface e implementação em C ++

12

Eu já vi que existem vários paradigmas diferentes no C ++ sobre o que entra no arquivo de cabeçalho e o que o arquivo cpp. AFAIK, a maioria das pessoas, especialmente as que têm experiência em C, fazem:

foo.h

 class foo {
 private:
     int mem;
     int bar();
 public:
     foo();
     foo(const foo&);
     foo& operator=(foo);
     ~foo();
 }

foo.cpp

 #include foo.h
 foo::bar() { return mem; }
 foo::foo() { mem = 42; }
 foo::foo(const foo& f) { mem = f.mem; }
 foo::operator=(foo f) { mem = f.mem; }
 foo::~foo() {}
 int main(int argc, char *argv[]) { foo f; }

No entanto, meus professores geralmente ensinam C ++ para iniciantes como este:

foo.h

 class foo {
 private:
     int mem;
     int bar() { return mem; }
 public:
     foo() { mem = 42; }
     foo(const foo& f) { mem = f.mem; }
     foo& operator=(foo f) { mem = f.mem; }
     ~foo() {}
 }

foo.cpp

 #include foo.h
 int main(int argc, char* argv[]) { foo f; }
 // other global helper functions, DLL exports, and whatnot

Originalmente vindo de Java, também sempre segui esse segundo caminho por várias razões, como por exemplo, que só preciso alterar algo em um só lugar se os nomes da interface ou do método mudarem, pois gosto da indentação diferente das coisas nas classes quando veja a implementação deles e acho os nomes mais legíveis em foocomparação com foo::foo.

Quero coletar prós e contras de qualquer maneira. Talvez ainda haja outras maneiras?

Uma desvantagem do meu caminho é, obviamente, a necessidade de declarações futuras ocasionais.

Felix Dombek
fonte
2
foo.cppagora não tem nada a ver com sua fooclasse e deve ser deixado em branco (talvez, #includepara deixar seu sistema de compilação feliz).
Benjamin Bannier
2
Seus professores são loucos.
Lightness Races in Orbit

Respostas:

16

Embora a segunda versão seja mais fácil de escrever, ela mistura interface com implementação.

Os arquivos de origem que incluem arquivos de cabeçalho precisam ser recompilados toda vez que os arquivos de cabeçalho são alterados. Na primeira versão, você alteraria o arquivo de cabeçalho apenas se precisar alterar a interface. Na segunda versão, você alteraria o arquivo de cabeçalho se precisar alterar a interface ou a implementação.

Além disso, você não deve expor os detalhes da implementação , obterá recompilação desnecessária com a segunda versão.

LennyProgrammers
fonte
1
+1 Meu criador de perfil não instrumenta o código inserido nos arquivos de cabeçalho - esse também é um motivo valioso.
Eugene
Se você vir minha resposta a essa pergunta programmers.stackexchange.com/questions/4573/… , verá como isso depende muito da semântica da classe, ou seja, o que a usará (em particular se for uma parte exposta do sua interface com o usuário e quantas outras classes em seu sistema a usam diretamente).
CashCow
3

Fiz isso pela segunda vez em 93-95. Demorei alguns minutos para recompilar um aplicativo pequeno com 5 a 10 funções / arquivos (no mesmo PC 486 .. e não, eu também não sabia sobre aulas, tinha apenas 14 a 15 anos e não havia internet ) .

Portanto, o que você ensina para iniciantes e o que usa profissionalmente são técnicas muito diferentes, especialmente em C ++.

Eu acho que a comparação entre C ++ e um carro de F1 é adequada. Você não coloca iniciantes em um carro de F1 (que nem liga, a menos que você pré-aqueça o motor a 80-95 graus Celsius).

Não ensine C ++ como o primeiro idioma. Você deve ter experiência suficiente para saber por que a opção 2 é pior do que a opção 1 em geral, saber um pouco o que significa compilação / vinculação estática e, portanto, entender por que o C ++ prefere da primeira maneira.

Macke
fonte
Esta resposta seria ainda melhor se você elaborado um pouco sobre a compilação estática / ligando (eu não sabia que naquela época!)
Felix Dombek
2

O segundo método é o que eu chamaria de classe totalmente embutida. Você está escrevendo uma definição de classe, mas todo o seu código usado apenas embutirá o código.

Sim, o compilador decide quando incorporar e quando não ... Nesse caso, você está ajudando o compilador a tomar uma decisão e, potencialmente, acabará gerando menos código e potencialmente mais rápido.

É provável que essa vantagem supere o fato de que, se você modificar a implementação de uma função, precisará reconstruir toda a fonte que a usa. Na natureza leve da classe, você não estará modificando a implementação. Se você adicionar um novo método, precisará modificar o cabeçalho de qualquer maneira.

À medida que sua classe fica mais complexa, mesmo adicionando um loop, o benefício de fazê-lo dessa maneira diminui.

Ainda tem suas vantagens, em particular:

  • Se esse for um código de "funcionalidade comum", você pode apenas incluir o cabeçalho e usá-lo em vários projetos sem precisar vincular uma biblioteca que contém sua origem.

A desvantagem de inlining se torna um problema quando isso significa que você precisa incluir detalhes de implementação em seu cabeçalho, ou seja, você precisa começar a incluir cabeçalhos extras.

Observe que os modelos são um caso especial, pois você praticamente precisa incluir os detalhes da implementação. Você pode ocultá-lo em outro arquivo, mas ele precisa estar lá. (Há uma exceção a essa regra com instanciações, mas em geral você alinha seus modelos).

CashCow
fonte
1

Pode não ser significativo ou verdadeiro se o seu executável ficar maior, mas mais código nos arquivos de cabeçalho permite ao compilador mais chances de otimizar a velocidade.

Se você decidir decidir escrever uma biblioteca somente de cabeçalho , este tópico é apenas uma de suas preocupações.

davidvandebunte
fonte