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ę
Foojako członek, musisz ją uwzględnić foo.h. Oznacza to, że zmiana implementacji Foo::fpropagacji 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::fjest wnętrze bar.cpp. Jeśli jednak zmienić realizacji Foo::f, zarówno bar.cppi qux.cpppotrzeby rekompilacji. Implementacja Foo::fżycia w obu plikach, nawet jeśli żadna część Quxbezpoś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
Fooczłonka do klasy Bar, musisz dołączyć go foo.hdo nagłówka. Jeśli wywołujesz Foo::fplik .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::fmuszą 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.tppw 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.cppponownie skompilować, jeśli zmienisz implementację Foo::f. Plik qux.cppnie 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.