Pisząc szablonową klasę C ++, zazwyczaj masz trzy opcje:
(1) Umieść deklarację i definicję w nagłówku.
// foo.h
#pragma once
template <typename T>
struct Foo
{
void f()
{
...
}
};
lub
// foo.h
#pragma once
template <typename T>
struct Foo
{
void f();
};
template <typename T>
inline void Foo::f()
{
...
}
Zawodowiec:
- Bardzo wygodne użycie (wystarczy dołączyć nagłówek).
Kon:
- Implementacja interfejsu i metody jest mieszana. Jest to „tylko” problem z czytelnością. Niektórzy uważają to za niemożliwe do utrzymania, ponieważ różni się ono od zwykłego podejścia .h / .cpp. Należy jednak pamiętać, że nie stanowi to problemu w innych językach, na przykład C # i Java.
- Duży wpływ na odbudowę: jeśli deklarujesz nową klasę
Foo
jako członek, musisz ją uwzględnić foo.h
. Oznacza to, że zmiana implementacji Foo::f
propagacji odbywa się zarówno przez pliki nagłówkowe, jak i źródłowe.
Przyjrzyjmy się bliżej wpływowi przebudowy: w przypadku nieszablonowanych klas C ++ deklaracje umieszczasz w .h, a definicje metod w .cpp. W ten sposób, gdy implementacja metody zostanie zmieniona, tylko jeden plik .cpp musi zostać ponownie skompilowany. Jest inaczej w przypadku klas szablonów, jeśli .h zawiera cały kod. Spójrz na następujący przykład:
// 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();
}
Tutaj jedynym zastosowaniem Foo::f
jest wnętrze bar.cpp
. Jeśli jednak zmienić realizacji Foo::f
, zarówno bar.cpp
i qux.cpp
potrzeby rekompilacji. Implementacja Foo::f
życia w obu plikach, nawet jeśli żadna część Qux
bezpośrednio z nich nie korzysta Foo::f
. W przypadku dużych projektów może to wkrótce stać się problemem.
(2) Umieść deklarację w .h, a definicję w .tpp i dołącz do .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()
{
...
}
Zawodowiec:
- Bardzo wygodne użycie (wystarczy dołączyć nagłówek).
- Definicje interfejsu i metod są rozdzielone.
Kon:
- Duży wpływ na odbudowę (taki sam jak (1) ).
To rozwiązanie dzieli deklarację i definicję metody na dwa osobne pliki, podobnie jak .h / .cpp. Jednak w tym podejściu występuje ten sam problem z odbudową, co (1) , ponieważ nagłówek zawiera bezpośrednio definicje metod.
(3) Umieść deklarację w .h, a definicję w .tpp, ale nie dołączaj .tpp do .h.
// foo.h
#pragma once
template <typename T>
struct Foo
{
void f();
};
// foo.tpp
#pragma once
template <typename T>
void Foo::f()
{
...
}
Zawodowiec:
- Zmniejsza wpływ odbudowy, podobnie jak separacja .h / .cpp.
- Definicje interfejsu i metod są rozdzielone.
Kon:
- Niewygodne użycie: dodając
Foo
członka do klasy Bar
, musisz dołączyć go foo.h
do nagłówka. Jeśli wywołujesz Foo::f
plik .cpp, musisz tam również dołączyć foo.tpp
.
Takie podejście zmniejsza wpływ przebudowy, ponieważ tylko pliki .cpp, które naprawdę korzystają, Foo::f
muszą zostać ponownie skompilowane. Ma to jednak swoją cenę: wszystkie te pliki muszą zostać uwzględnione foo.tpp
. Weź przykład z góry i zastosuj nowe podejście:
// 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();
}
Jak widać, jedyną różnicą jest dodatkowe włączenie foo.tpp
w bar.cpp
. Jest to niewygodne, a dodanie drugiej klasy dla klasy w zależności od tego, czy wywołujesz metody, wydaje się bardzo brzydkie. Zmniejszasz jednak wpływ przebudowy: musisz tylko bar.cpp
ponownie skompilować, jeśli zmienisz implementację Foo::f
. Plik qux.cpp
nie wymaga ponownej kompilacji.
Podsumowanie:
Jeśli implementujesz bibliotekę, zwykle nie musisz przejmować się skutkami odbudowy. Użytkownicy Twojej biblioteki pobierają wersję i korzystają z niej, a implementacja biblioteki nie zmienia się w codziennej pracy użytkownika. W takich przypadkach biblioteka może zastosować podejście (1) lub (2) i to tylko kwestia gustu, który wybierzesz.
Jeśli jednak pracujesz nad aplikacją lub pracujesz nad biblioteką wewnętrzną swojej firmy, kod często się zmienia. Musisz więc dbać o wpływ odbudowy. Wybór podejścia (3) może być dobrą opcją, jeśli zachęcisz programistów do zaakceptowania dodatkowego uwzględnienia.