Quando si scrive una classe C ++ basata su modelli, in genere sono disponibili tre opzioni:
(1) Inserire la dichiarazione e la definizione nell'intestazione.
// foo.h
#pragma once
template <typename T>
struct Foo
{
void f()
{
...
}
};
o
// foo.h
#pragma once
template <typename T>
struct Foo
{
void f();
};
template <typename T>
inline void Foo::f()
{
...
}
Pro:
- Utilizzo molto conveniente (basta includere l'intestazione).
con:
- L'implementazione dell'interfaccia e del metodo è mista. Questo è "solo" un problema di leggibilità. Alcuni lo trovano irraggiungibile, perché è diverso dal solito approccio .h / .cpp. Tuttavia, tenere presente che questo non è un problema in altre lingue, ad esempio C # e Java.
- Elevato impatto sulla ricostruzione: se si dichiara una nuova classe
Foo
come membro, è necessario includerla foo.h
. Ciò significa che la modifica dell'implementazione di si Foo::f
propaga attraverso i file di intestazione e di origine.
Diamo un'occhiata più da vicino all'impatto della ricostruzione: per le classi C ++ non basate su modelli, inserisci le dichiarazioni in .h e le definizioni dei metodi in .cpp. In questo modo, quando viene modificata l'implementazione di un metodo, è necessario ricompilare solo un .cpp. Questo è diverso per le classi di template se .h contiene tutto il tuo codice. Dai un'occhiata al seguente esempio:
// 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();
}
Qui, l'unico uso di Foo::f
è dentro bar.cpp
. Tuttavia, se si cambia l'attuazione di Foo::f
, sia bar.cpp
e qux.cpp
devono essere ricompilati. L'implementazione di Foo::f
vite in entrambi i file, anche se nessuna parte di Qux
utilizza direttamente nulla di Foo::f
. Per progetti di grandi dimensioni, questo può presto diventare un problema.
(2) Metti la dichiarazione in .h e la definizione in .tpp e includila in .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()
{
...
}
Pro:
- Utilizzo molto conveniente (basta includere l'intestazione).
- Le definizioni di interfaccia e metodo sono separate.
con:
- Elevato impatto sulla ricostruzione (uguale a (1) ).
Questa soluzione separa la dichiarazione e la definizione del metodo in due file separati, proprio come .h / .cpp. Tuttavia, questo approccio presenta lo stesso problema di ricostruzione di (1) , poiché l'intestazione include direttamente le definizioni del metodo.
(3) Inserisci la dichiarazione in .h e la definizione in .tpp, ma non includere .tpp in .h.
// foo.h
#pragma once
template <typename T>
struct Foo
{
void f();
};
// foo.tpp
#pragma once
template <typename T>
void Foo::f()
{
...
}
Pro:
- Riduce l'impatto della ricostruzione proprio come la separazione .h / .cpp.
- Le definizioni di interfaccia e metodo sono separate.
con:
- Utilizzo non conforme: quando si aggiunge un
Foo
membro a una classe Bar
, è necessario includerlo foo.h
nell'intestazione. Se chiami Foo::f
un .cpp, devi anche includerlo foo.tpp
.
Questo approccio riduce l'impatto della ricostruzione, poiché è Foo::f
necessario ricompilare solo i file .cpp che utilizzano realmente . Tuttavia, questo ha un prezzo: tutti questi file devono essere inclusi foo.tpp
. Prendi l'esempio dall'alto e usa il nuovo approccio:
// 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();
}
Come puoi vedere, l'unica differenza è l'inclusione aggiuntiva di foo.tpp
in bar.cpp
. Questo è scomodo e aggiungere una seconda inclusione per una classe a seconda che tu chiami metodi su di essa sembra molto brutto. Tuttavia, si riduce l'impatto della ricostruzione: bar.cpp
deve essere ricompilato solo se si modifica l'implementazione di Foo::f
. Il file qux.cpp
non necessita di ricompilazione.
Sommario:
Se si implementa una libreria, di solito non è necessario preoccuparsi della ricostruzione dell'impatto. Gli utenti della tua libreria prendono una versione e la usano e l'implementazione della libreria non cambia nel lavoro quotidiano dell'utente. In questi casi, la libreria può usare l'approccio (1) o (2) ed è solo una questione di gusti quale scegliere.
Tuttavia, se stai lavorando su un'applicazione o se stai lavorando su una libreria interna della tua azienda, il codice cambia frequentemente. Quindi devi preoccuparti di ricostruire l'impatto. La scelta dell'approccio (3) può essere una buona opzione se fai in modo che i tuoi sviluppatori accettino l'inclusione aggiuntiva.