Ao escrever uma classe C ++ com modelo, você geralmente tem três opções:
(1) Coloque declaração e definição no cabeçalho.
// foo.h
#pragma once
template <typename T>
struct Foo
{
void f()
{
...
}
};
ou
// foo.h
#pragma once
template <typename T>
struct Foo
{
void f();
};
template <typename T>
inline void Foo::f()
{
...
}
Pró:
- Uso muito conveniente (apenas inclua o cabeçalho).
Vigarista:
- A implementação da interface e do método é mista. Este é "apenas" um problema de legibilidade. Alguns acham isso impossível de manter, porque é diferente da abordagem usual .h / .cpp. No entanto, esteja ciente de que isso não é problema em outros idiomas, por exemplo, C # e Java.
- Alto impacto de reconstrução: se você declarar uma nova classe
Foocomo membro, precisará incluir foo.h. Isso significa que alterar a implementação de Foo::fpropaga-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:
// bar.h
#pragma once
#include "foo.h"
struct Bar
{
void b();
Foo<int> foo;
};
// bar.cpp
#include "bar.h"
void Bar::b()
{
foo.f();
}
// qux.h
#pragma once
#include "bar.h"
struct Qux
{
void q();
Bar bar;
}
// qux.cpp
#include "qux.h"
void Qux::q()
{
bar.b();
}
Aqui, o único uso de Foo::festá dentro bar.cpp. No entanto, se você alterar a implementação de Foo::f, both bar.cppe qux.cppprecisar ser recompilado. A implementação de Foo::fvidas em ambos os arquivos, mesmo que nenhuma parte Quxuse diretamente nada Foo::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.
// foo.h
#pragma once
template <typename T>
struct Foo
{
void f();
};
#include "foo.tpp"
// foo.tpp
#pragma once // not necessary if foo.h is the only one that includes this file
template <typename T>
inline void Foo::f()
{
...
}
Pró:
- Uso muito conveniente (apenas inclua o cabeçalho).
- As definições de interface e método são separadas.
Vigarista:
- Alto impacto de reconstrução (o mesmo que (1) ).
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.
// foo.h
#pragma once
template <typename T>
struct Foo
{
void f();
};
// foo.tpp
#pragma once
template <typename T>
void Foo::f()
{
...
}
Pró:
- Reduz o impacto da reconstrução, assim como a separação .h / .cpp.
- As definições de interface e método são separadas.
Vigarista:
- Uso inconveniente: ao adicionar um
Foomembro a uma classe Bar, você precisa incluir foo.hno cabeçalho. Se você chamar Foo::fum .cpp, também precisará incluir foo.tpplá.
Essa abordagem reduz o impacto da reconstrução, pois apenas os arquivos .cpp que realmente usam Foo::fprecisam ser recompilados. No entanto, isso tem um preço: todos esses arquivos precisam incluir foo.tpp. Pegue o exemplo acima e use a nova abordagem:
// bar.h
#pragma once
#include "foo.h"
struct Bar
{
void b();
Foo<int> foo;
};
// bar.cpp
#include "bar.h"
#include "foo.tpp"
void Bar::b()
{
foo.f();
}
// qux.h
#pragma once
#include "bar.h"
struct Qux
{
void q();
Bar bar;
}
// qux.cpp
#include "qux.h"
void Qux::q()
{
bar.b();
}
Como você pode ver, a única diferença é a inclusão adicional de foo.tpppol bar.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.cppprecisa ser recompilado se você alterar a implementação de Foo::f. O arquivo qux.cppnã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.