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
Foo
como membro, precisará incluir foo.h
. Isso significa que alterar a implementação de Foo::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:
// 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::f
está dentro bar.cpp
. No entanto, se você alterar a implementação de Foo::f
, both bar.cpp
e qux.cpp
precisar ser recompilado. A implementação de Foo::f
vidas em ambos os arquivos, mesmo que nenhuma parte Qux
use 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
Foo
membro a uma classe Bar
, você precisa incluir foo.h
no cabeçalho. Se você chamar Foo::f
um .cpp, também precisará incluir foo.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 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.tpp
pol 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.cpp
precisa ser recompilado se você alterar a implementação de Foo::f
. O arquivo qux.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.