19.4 – Специализация шаблона класса
В предыдущем уроке «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
Стоит еще раз отметить, что сохранение одинакового открытого интерфейса в вашем шаблоне класса и во всех специализациях, как правило, является хорошей идеей, поскольку это упрощает их использование, однако это не является строго необходимым.