26.4 — Specjalizacja szablonu klasy

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.

guest
Twój adres e-mail nie zostanie wyświetlony
Znalazłeś błąd? Zostaw komentarz powyżej!
Komentarze związane z poprawkami zostaną usunięte po przetworzeniu, aby pomóc zmniejszyć bałagan. Dziękujemy za pomoc w ulepszaniu witryny dla wszystkich!
Awatary z https://gravatar.com/ są połączone z podanym adresem e-mail.
Powiadamiaj mnie o odpowiedziach:  
109 Komentarze
Najnowsze
Najstarsze Najczęściej głosowane
Wbudowane opinie
Wyświetl wszystkie komentarze