W poprzednim lekcja 26.3 -- Specjalizacja szablonu funkcji, widzieliśmy, jak to było możliwość specjalizacji funkcji w celu zapewnienia innej funkcjonalności dla określonych typów danych. Jak się okazuje, możliwa jest nie tylko specjalizacja funkcji, ale także klas!
Rozważmy przypadek, w którym chcemy mieć klasę przechowującą 8 obiektów. Oto uproszczony szablon klasy, który może to zrobić:
#include <iostream>
template <typename T>
class Storage8
{
private:
T m_array[8];
public:
void set(int index, const T& value)
{
m_array[index] = value;
}
const T& get(int index) const
{
return m_array[index];
}
};
int main()
{
// Define a Storage8 for integers
Storage8<int> intStorage;
for (int count{ 0 }; count < 8; ++count)
intStorage.set(count, count);
for (int count{ 0 }; count < 8; ++count)
std::cout << intStorage.get(count) << '\n';
// Define a Storage8 for bool
Storage8<bool> boolStorage;
for (int count{ 0 }; count < 8; ++count)
boolStorage.set(count, count & 3);
std::cout << std::boolalpha;
for (int count{ 0 }; count < 8; ++count)
{
std::cout << boolStorage.get(count) << '\n';
}
return 0;
}Ten przykład wyświetla:
0 1 2 3 4 5 6 7 false true true true false true true true
Chociaż ta klasa jest w pełni funkcjonalna, okazuje się, że implementacja Storage8<bool> jest bardziej nieefektywna, niż powinna. Ponieważ wszystkie zmienne muszą mieć adres, a procesor nie może zaadresować niczego mniejszego niż bajt, wszystkie zmienne muszą mieć rozmiar co najmniej jednego bajtu. W rezultacie zmienna typu bool wykorzystuje cały bajt, chociaż technicznie rzecz biorąc, potrzebuje tylko jednego bitu do przechowywania swojej wartości prawdziwej lub fałszywej! Zatem a bool to 1 bit użytecznej informacji i 7 bitów zmarnowanego miejsca. Nasza Storage8<bool> klasa, która zawiera 8 bool, to 1 bajt przydatnych informacji i 7 bajtów zmarnowanego miejsca.
Jak się okazuje, używając podstawowej logiki bitowej, możliwe jest skompresowanie wszystkich 8 booli do jednego bajtu, całkowicie eliminując zmarnowaną przestrzeń. Jednakże, aby to zrobić, będziemy musieli zmodernizować klasę, jeśli zostanie użyta z typem bool, zastępując tablicę 8 bool zmienną o rozmiarze jednego bajtu. Chociaż moglibyśmy w tym celu stworzyć zupełnie nową klasę, ma to jedną poważną wadę: musimy nadać jej inną nazwę. Następnie programista musi pamiętać, że Storage8<T> jest przeznaczony dla typów innych niż bool, podczas gdy Storage8Bool (czy jakkolwiek nazwiemy nową klasę) jest przeznaczony dla bool. To niepotrzebna złożoność, której wolelibyśmy uniknąć. Na szczęście C++ zapewnia nam lepszą metodę: specjalizację szablonów klas.
Specjalizacja szablonów klas
Specjalizacja szablonów klas pozwala nam specjalizować klasę szablonów dla określonego typu danych (lub typów danych, jeśli istnieje wiele parametrów szablonu). W tym przypadku użyjemy specjalizacji szablonu klasy, aby napisać dostosowaną wersję Storage8<bool> , która będzie miała pierwszeństwo przed klasą generyczną Storage8<T> .
Specjalizacje szablonów klas są traktowane jako całkowicie niezależne klasy, mimo że są tworzone w taki sam sposób, jak klasa szablonowa. Oznacza to, że możemy zmienić wszystko w naszej klasie specjalizacji, w tym sposób jej implementacji, a nawet funkcje, które udostępnia publicznie, tak jakby była to niezależna klasa.
Podobnie jak wszystkie szablony, kompilator musi widzieć pełną definicję specjalizacji, aby z niej skorzystać. Ponadto zdefiniowanie specjalizacji szablonu klasy wymaga najpierw zdefiniowania klasy niewyspecjalizowanej.
Oto przykład specjalizacji Storage8<bool> klasę:
#include <cstdint>
// First define our non-specialized class template
template <typename T>
class Storage8
{
private:
T m_array[8];
public:
void set(int index, const T& value)
{
m_array[index] = value;
}
const T& get(int index) const
{
return m_array[index];
}
};
// Now define our specialized class template
template <> // the following is a template class with no templated parameters
class Storage8<bool> // we're specializing Storage8 for bool
{
// What follows is just standard class implementation details
private:
std::uint8_t m_data{};
public:
// Don't worry about the details of the implementation of these functions
void set(int index, bool value)
{
// Figure out which bit we're setting/unsetting
// This will put a 1 in the bit we're interested in turning on/off
auto mask{ 1 << index };
if (value) // If we're setting a bit
m_data |= mask; // use bitwise-or to turn that bit on
else // if we're turning a bit off
m_data &= ~mask; // bitwise-and the inverse mask to turn that bit off
}
bool get(int index)
{
// Figure out which bit we're getting
auto mask{ 1 << index };
// bitwise-and to get the value of the bit we're interested in
// Then implicit cast to boolean
return (m_data & mask);
}
};
// Same example as before
int main()
{
// Define a Storage8 for integers (instantiates Storage8<T>, where T = int)
Storage8<int> intStorage;
for (int count{ 0 }; count < 8; ++count)
{
intStorage.set(count, count);
}
for (int count{ 0 }; count < 8; ++count)
{
std::cout << intStorage.get(count) << '\n';
}
// Define a Storage8 for bool (instantiates Storage8<bool> specialization)
Storage8<bool> boolStorage;
for (int count{ 0 }; count < 8; ++count)
{
boolStorage.set(count, count & 3);
}
std::cout << std::boolalpha;
for (int count{ 0 }; count < 8; ++count)
{
std::cout << boolStorage.get(count) << '\n';
}
return 0;
}Najpierw zauważ, że nasz szablon klasy specjalistycznej zaczyna się od template<>. Słowo kluczowe szablon mówi kompilatorowi, że to, co następuje, jest szablonem, a puste nawiasy klamrowe oznaczają, że nie ma żadnych parametrów szablonu. W tym przypadku nie ma żadnych parametrów szablonu, ponieważ zastępujemy jedyny parametr szablonu (T) określonym typem (bool).
Następnie dodajemy <bool> do nazwy klasy, aby zaznaczyć, że specjalizujemy się w bool wersji class Storage8.
Wszystkie pozostałe zmiany to tylko szczegóły implementacji klasy.Nie musisz rozumieć, jak działa logika bitowa działa, aby móc używać tej klasy (chociaż możesz przejrzeć O.2 -- Operatory bitowe jeśli chcesz to rozgryźć, ale potrzebujesz odświeżenia wiedzy na temat działania operatorów bitowych).
Zauważ, że ta klasa specjalizacji wykorzystuje std::uint8_t (1 bajt bez znaku int) zamiast tablicy 8 bool (8 bajtów).
Teraz, gdy utworzymy instancję typu obiektu Storage<T>, gdzie T nie jest bool, otrzymamy wersję wzorowaną na ogólnej klasie templated Storage8<T> . Kiedy utworzymy instancję obiektu typu Storage8<bool>, otrzymamy właśnie utworzoną wyspecjalizowaną wersję. Należy zauważyć, że publicznie dostępny interfejs obu klas pozostał taki sam — podczas gdy C++ daje nam swobodę dodawania, usuwania lub zmiany funkcji Storage8<bool> według własnego uznania, utrzymanie spójnego interfejsu oznacza, że programista może używać obu klas w dokładnie ten sam sposób.
Jak można się spodziewać, wyświetla to ten sam wynik, co w poprzednim przykładzie, w którym użyto niewyspecjalizowanej wersji Storage8<bool>:
0 1 2 3 4 5 6 7 false true true true false true true true
specjalizującego elementu członkowskiego funkcje
W poprzedniej lekcji przedstawiliśmy następujący przykład:
#include <iostream>
template <typename T>
class Storage
{
private:
T m_value {};
public:
Storage(T value)
: m_value { value }
{
}
void print()
{
std::cout << m_value << '\n';
}
};
int main()
{
// Define some storage units
Storage i { 5 };
Storage d { 6.7 };
// Print out some values
i.print();
d.print();
}Naszym pragnieniem jest wyspecjalizowanie print() funkcji tak, aby drukowała podwójność w notacji naukowej. Korzystając ze specjalizacji szablonów klas, moglibyśmy zdefiniować wyspecjalizowaną klasę dla Storage<double>:
#include <iostream>
template <typename T>
class Storage
{
private:
T m_value {};
public:
Storage(T value)
: m_value { value }
{
}
void print()
{
std::cout << m_value << '\n';
}
};
// Explicit class template specialization for Storage<double>
// Note how redundant this is
template <>
class Storage<double>
{
private:
double m_value {};
public:
Storage(double value)
: m_value { value }
{
}
void print();
};
// We're going to define this outside the class for reasons that will become obvious shortly
// This is a normal (non-specialized) member function definition (for member function print of specialized class Storage<double>)
void Storage<double>::print()
{
std::cout << std::scientific << m_value << '\n';
}
int main()
{
// Define some storage units
Storage i { 5 };
Storage d { 6.7 }; // uses explicit specialization Storage<double>
// Print out some values
i.print(); // calls Storage<int>::print (instantiated from Storage<T>)
d.print(); // calls Storage<double>::print (called from explicit specialization of Storage<double>)
}Jednak zauważ, jak dużo jest tutaj redundancji. Powieliliśmy całą definicję klasy tylko po to, abyśmy mogli zmienić jedną funkcję składową!
Na szczęście możemy zrobić lepiej. C++ nie wymaga od nas jawnej specjalizacji Storage<double> aby jawnej specjalizacji Storage<double>::print(). Zamiast tego możemy pozwolić kompilatorowi na niejawną specjalizację Storage<double> z Storage<T> i zapewnienie jawnej specjalizacji tylko Storage<double>::print()! Oto jak to wygląda:
#include <iostream>
template <typename T>
class Storage
{
private:
T m_value {};
public:
Storage(T value)
: m_value { value }
{
}
void print()
{
std::cout << m_value << '\n';
}
};
// This is a specialized member function definition
// Explicit function specializations are not implicitly inline, so make this inline if put in header file
template<>
void Storage<double>::print()
{
std::cout << std::scientific << m_value << '\n';
}
int main()
{
// Define some storage units
Storage i { 5 };
Storage d { 6.7 }; // will cause Storage<double> to be implicitly instantiated
// Print out some values
i.print(); // calls Storage<int>::print (instantiated from Storage<T>)
d.print(); // calls Storage<double>::print (called from explicit specialization of Storage<double>::print())
}To wszystko!
Jak zauważono w poprzedniej lekcji (26.3 -- Specjalizacja szablonu funkcji), jawne specjalizacje funkcji nie są domyślnie wbudowane, dlatego powinniśmy oznaczyć naszą specjalizację Storage<double>::print() jako inline, jeśli jest zdefiniowana w nagłówku plik.
Gdzie zdefiniować specjalizacje szablonu klasy
Aby użyć specjalizacji, kompilator musi widzieć pełną definicję zarówno klasy niewyspecjalizowanej, jak i klasy wyspecjalizowanej. Jeśli kompilator widzi tylko definicję klasy niewyspecjalizowanej, użyje jej zamiast specjalizacji.
Z tego powodu wyspecjalizowane klasy i funkcje są często definiowane w pliku nagłówkowym tuż pod definicją klasy niewyspecjalizowanej, więc pojedynczy nagłówek obejmuje zarówno klasę niewyspecjalizowaną, jak i wszelkie specjalizacje. Dzięki temu specjalizacja będzie zawsze widoczna, gdy tylko widoczna będzie także klasa niewyspecjalizowana.
Jeśli specjalizacja jest wymagana tylko w jednej jednostce tłumaczeniowej, można ją zdefiniować w pliku źródłowym tej jednostki tłumaczeniowej. Ponieważ inne jednostki tłumaczeniowe nie będą mogły zobaczyć definicji specjalizacji, będą nadal używać wersji niespecjalistycznej.
Uważaj, aby nie umieszczać specjalizacji w osobnym pliku nagłówkowym, mając na celu włączenie nagłówka specjalizacji do dowolnej jednostki tłumaczeniowej, w której specjalizacja jest pożądana. Złym pomysłem jest projektowanie kodu, który w przejrzysty sposób zmienia zachowanie w oparciu o obecność lub brak pliku nagłówkowego. Na przykład, jeśli zamierzasz używać specjalizacji, ale zapomnisz o dołączeniu nagłówka specjalizacji, zamiast tego możesz użyć wersji niewyspecjalizowanej. Jeśli zamierzasz używać braku specjalizacji, możesz i tak skorzystać z tej specjalizacji, jeśli jakiś inny nagłówek zawiera specjalizację jako dołączenie przechodnie.

