19.4 – Специализация шаблона класса

Добавлено 25 августа 2021 в 01:17

В предыдущем уроке «19.3 – Специализация шаблона функции» мы увидели, как можно специализировать функции, чтобы предоставлять различные функциональные возможности для определенных типов данных. Оказывается, специализировать можно не только функции, но и весь класс!

Рассмотрим случай, когда вы хотите создать класс, который хранит 8 объектов. Вот упрощенный класс для этого:

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];
    }
};

Поскольку этот класс является шаблонным, он будет работать с любым заданным типом:

#include <iostream>
 
int main()
{
    // Определяем Storage8 для целых чисел
    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';
 
    // Определяем Storage8 для логических значений
    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;
}

Этот пример напечатает:

0
1
2
3
4
5
6
7
false
true
true
true
false
true
true
true

Хотя этот класс полностью работоспособен, оказывается, что реализация Storage8<bool> очень неэффективна. Поскольку все переменные должны иметь адрес, а CPU не может адресовать ничего меньше байта, все переменные должны иметь размер не менее байта. Следовательно, переменная типа bool в конечном итоге использует весь байт, хотя технически ей нужен только один бит для хранения своего значения, true или false! Таким образом, логическое значение – это 1 бит полезной информации и 7 бит потраченного впустую места. Наш класс Storage8<bool>, который содержит 8 булевых значений, содержит 1 байт полезной информации и 7 байт потраченного впустую места.

Оказывается, используя некоторую базовую логику работы с битами, все 8 логических значений можно сжать в один байт, полностью исключив бесполезное пространство. Однако для этого нам нужно будет обновить класс при использовании с типом bool, заменив массив из 8 bool на переменную размером в один байт. Хотя мы могли бы создать для этого совершенно новый класс, у этого был бы один серьезный недостаток: мы должны дать ему другое имя. Затем программист должен будет запомнить, что Storage8<T> предназначен для типов, отличных от bool, тогда как Storage8Bool (или как там мы назовем новый класс) предназначен для bool. Мы бы предпочли избежать этой ненужной сложности. К счастью, C++ предоставляет для этого нам более подходящий метод: специализацию шаблона класса.

Специализация шаблона класса

Специализация шаблона класса позволяет нам специализировать шаблонный класс для определенного типа данных (или типов данных, если у шаблона несколько параметров). В этом случае мы собираемся использовать специализацию шаблона класса для написания специализированной версии Storage8<bool>, которая будет иметь приоритет над обобщенным классом Storage8<T>. Это работает аналогично тому, как специализированная функция имеет приоритет над обобщенной шаблонной функцией.

Специализации шаблонов классов рассматриваются как полностью независимые классы, даже если они размещаются так же, как и шаблонный класс. Это означает, что в нашей специализации класса мы можем изменить абсолютно всё, в том числе способ его реализации и даже функции, которые он делает открытыми, как если бы это был независимый класс. Вот наш специализированный класс:

template <> // далее идет шаблонный класс без шаблонных параметров
class Storage8<bool> // мы специализируем Storage8 для bool
{
// Далее следует просто обычные детали реализации класса
private:
    unsigned char m_data{};
 
public:
    void set(int index, bool value)
    {
        // Выясняем, какой бит мы устанавливаем/сбрасываем
        // Это поместит 1 в бит, который мы хотим установить/сбросить
        auto mask{ 1 << index };
 
        if (value)  // Если мы устанавливаем бит
            m_data |= mask;  // Используем побитовое ИЛИ, чтобы установить бит в 1
        else  // Если мы сбрасываем бит
            m_data &= ~mask;  // побитовое И и инверсия для сброса бита в 0
	}
	
    bool get(int index)
    {
        // Выясняем, какой бит мы получаем
        auto mask{ 1 << index };
        // побитовое И, чтобы получить значение интересующего нас бита
        // Затем неявное приведение к bool
        return (m_data & mask);
    }
};

Во-первых, обратите внимание, что мы начинаем с template<>. Ключевое слово template сообщает компилятору, что всё последующее является шаблоном, а пустые угловые скобки означают, что у шаблона отсутствуют параметры. В этом случае никаких шаблонных параметров нет, потому что мы заменяем единственный параметр шаблона (T) на определенный тип (bool).

Затем мы добавляем <bool> к имени класса, чтобы обозначить, что мы специализируем для на bool-версию класса Storage8.

Все остальные изменения – это просто детали реализации класса. Вам не нужно понимать, как работает битовая логика, чтобы использовать класс (хотя, если хотите понять это, можете просмотреть статью «O.2 – Побитовые операторы», чтобы освежить в памяти, как работают побитовые операторы).

Обратите внимание, что эта специализация класса использует один unsigned char (1 байт) вместо массива из 8 значений bool (8 байтов).

Теперь, когда мы объявляем класс типа Storage8<T>, где T не является bool, мы получим версию, созданную по образцу из обобщенного шаблонного класса Storage8<T>. Когда мы объявляем класс типа Storage8<bool>, мы получим только что созданную специализированную версию. Обратите внимание, что мы сохранили открытый интерфейс обоих классов одинаковым – в то время как C++ дает нам свободу добавлять, удалять или изменять функции Storage8<bool> по своему усмотрению, сохранение согласованного интерфейса означает, что программист может использовать любой класс одинаковым способом.

Мы можем использовать тот же пример, что и раньше, чтобы показать, как создаются экземпляры Storage8<T> и Storage8<bool>:

int main()
{
    // Определяем Storage8 для целых чисел
    // (создает экземпляр Storage8<T>, где 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';
    }
 
    // Определяем Storage8 для bool
    // (создает экземпляр специализации Storage8<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;
}

Как и следовало ожидать, это выводит тот же результат, что и в предыдущем примере, в котором использовалась неспециализированная версия Storage8<bool>:

0
1
2
3
4
5
6
7
false
true
true
true
false
true
true
true

Стоит еще раз отметить, что сохранение одинакового открытого интерфейса в вашем шаблоне класса и во всех специализациях, как правило, является хорошей идеей, поскольку это упрощает их использование, однако это не является строго необходимым.

Теги

C++ / CppLearnCppДля начинающихКласс (программирование)ОбучениеПрограммированиеСпециализация шаблонаШаблон / TemplateШаблон класса

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.