Quando scrivi una classe C ++ basata su modelli, di solito hai tre opzioni:
(1) Inserisci dichiarazione e 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'interfaccia e l'implementazione del metodo sono misti. Questo è "solo" un problema di leggibilità. Alcuni lo trovano irraggiungibile, perché è diverso dal solito approccio .h / .cpp. Tuttavia, tieni presente che questo non è un problema in altre lingue, ad esempio C # e Java.
- Alto impatto di ricostruzione: se dichiari una nuova classe con
Foo
come membro, devi includere foo.h
. Ciò significa che la modifica dell'implementazione di Foo::say
viene propagata attraverso i file di intestazione e di origine.
Diamo un'occhiata più da vicino all'impatto della ricostruzione:
Per le classi C ++ non-templated, si mettono 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 uno .cpp. Questo è diverso per le classi template se il file .h contiene tutto il 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 utilizzo di Foo:f
è all'interno di bar.cpp
. Tuttavia, se cambi l'implementazione di Foo::f
, sia la bar.cpp
che la qux.cpp
devono essere ricompilate. L'implementazione di Foo::f
è presente in entrambi i file, anche se nessuna parte di Qux
utilizza direttamente qualcosa di Foo::f
. Per i 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:
- Alto impatto di 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) , perché l'intestazione include direttamente le definizioni del metodo.
(3) Inserisci dichiarazione in .h e 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 scomodo: quando aggiungi un membro
Foo
a una classe Bar
, devi includere foo.h
nell'intestazione. Se chiami Foo::f
in un file .cpp, anche devi includere foo.tpp
lì.
Questo approccio riduce l'impatto della ricostruzione, poiché è necessario ricompilare solo i file .cpp che utilizzano veramente Foo::f
. Tuttavia, questo ha un prezzo: tutti questi file devono includere foo.tpp
. Prendi l'esempio dall'alto e utilizza 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
. Ciò è inopportuno e aggiungere una seconda inclusione per una classe a seconda che si chiamino i metodi su di essa sembra molto brutto. Tuttavia, riduci l'impatto della ricostruzione: solo bar.cpp
deve essere ricompilato se cambi l'implementazione di Foo::f
. Il file qux.cpp
non ha bisogno di ricompilazione.
Riepilogo:
Se si implementa una libreria, di solito non è necessario preoccuparsi dell'impatto della ricostruzione. Gli utenti della tua libreria acquisiscono una versione e la usano e l'implementazione della libreria non cambia nel lavoro quotidiano dell'utente. In questi casi, la libreria può utilizzare l'approccio (1) o (2) ed è solo una questione di gusti che scegli.
Tuttavia, se quando si lavora su un progetto come un'applicazione o se la libreria è un progetto interno della propria azienda, il codice cambia frequentemente. Quindi devi preoccuparti dell'impatto della ricostruzione. La scelta dell'approccio (3) può essere una buona opzione se chiedi agli sviluppatori di accettare l'inclusione aggiuntiva.