На предыдущем уроке мы говорили о том, как специализировать шаблон функции, чтобы при работе с одним типом данных была одна реализация функции, а при работе с другим типом данных — другая реализация функции. Оказывается, мы можем специализировать не только шаблоны функций, но и шаблоны классов.
Рассмотрим класс-массив, который может хранить 8 объектов:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
template <class T> class Repository8 { private: T m_array[8]; public: void set(int index, const T &value) { m_array[index] = value; } const T& get(int index) { return m_array[index]; } }; |
Поскольку это шаблон класса, то он будет работать с любым типом данных:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
#include <iostream> int main() { // Объявляем целочисленный объект-массив Repository8<int> intRepository; for (int count=0; count<8; ++count) intRepository.set(count, count); for (int count=0; count<8; ++count) std::cout << intRepository.get(count) << '\n'; // Объявляем объект-массив типа bool Repository8<bool> boolRepository; for (int count=0; count<8; ++count) boolRepository.set(count, count % 5); for (int count=0; count<8; ++count) std::cout << (boolRepository.get(count) ? "true" : "false") << '\n'; return 0; } |
Результат:
0
1
2
3
4
5
6
7
false
true
true
true
true
false
true
true
Хотя всё работает правильно, реализация Repository8<bool>
, на самом деле, не столь эффективна, какой она могла бы быть. Поскольку все переменные должны иметь адрес, а ЦП не может дать адрес чему-либо меньшему, чем 1 байт, то размер всех переменных должен быть не менее 1 байта. Следовательно, каждая переменная типа bool занимает целый байт, хотя технически ей нужен только 1 бит для хранения значения true
или false
! Таким образом, переменная типа bool — это 1 бит полезной информации и 7 бит потраченного впустую места. Получается, что наш класс Repository8<bool>
, который имеет 8 переменных типа bool, фактически работает только с 1 байтом данных, а остальные 7 байтов тратятся зря.
Выход есть: мы можем сжать 8 переменных типа bool в 1 байт, сэкономив при этом остальные 7 байтов. Однако для этого нам нужно будет изменить реализацию класса, заменив массив из 8 переменных типа bool (8 байтов) на 1 переменную типа unsigned char (1 байт). Мы могли бы, конечно, использовать для этого новый отдельный класс, но это неэффективно, и программисту придется помнить, что Repository8<T>
работает со всеми типами данных, кроме bool, а при работе с bool следует вызывать Repository8Bool
(неважно, какое имя будет у этого класса). Это лишняя работа, которую можно избежать, используя явную специализацию шаблона класса.
Специализация шаблона класса
Специализация шаблона класса (или «явная специализация шаблона класса») позволяет специализировать шаблон класса для работы с определенным типом данных (или сразу с несколькими типами данных, если есть несколько параметров шаблона).
Специализация шаблона класса рассматривается компилятором как полностью отдельный и независимый класс, хоть и выделяется как обычный шаблон класса. Это означает, что мы можем изменить в классе всё что угодно, включая его реализацию/методы/спецификаторы доступа и т.д.
Рассмотрим специализацию шаблона класса Repository8 для работы с типом bool:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
template <> class Repository8<bool> // специализируем шаблон класса Repository8 для работы с типом bool { // Реализация класса private: unsigned char m_data; public: Repository8() : m_data(0) { } void set(int index, bool value) { // Выбираем оперируемый бит unsigned char mask = 1 << index; if (value) // если на входе у нас true, то бит нужно "включить" m_data |= mask; // используем побитовое ИЛИ, чтобы "включить" бит else // если на входе у нас false, то бит нужно "выключить" m_data &= ~mask; // используем побитовое И, чтобы "выключить" бит } bool get(int index) { // Выбираем бит unsigned char mask = 1 << index; // Используем побитовое И для получения значения бита, а затем выполняется его неявное преобразование в тип bool return (m_data & mask) != 0; } }; |
Во-первых, начинаем с template<>
. Ключевое слово template сообщает компилятору, что это шаблон, а пустые угловые скобки означают, что нет никаких параметров. А параметров нет из-за того, что мы заменяем единственный параметр шаблона (T
, который отвечает за тип данных) конкретным типом данных (bool). Затем мы пишем имя класса и добавляем к нему <bool>
, сообщая компилятору, что будем работать с типом bool.
Все остальные изменения — это просто детали реализации класса. Обратите внимание, вместо массива из 8 переменных типа bool (8 байтов) мы используем 1 переменную типа unsigned char (1 байт). Почему именно так? Об этом читайте на уроке №45 и на уроке №46.
Теперь при объявлении объекта класса Repository8<T>
, где T
не является bool, мы получим экземпляр общего шаблона класса, тогда как при объявлении объекта Repository8<bool>
, мы получим экземпляр шаблона Repository8<bool>
. Обратите внимание, мы не изменяли интерфейс класса, а оставили его открытым (каким он и был), в то время как язык C++ предоставляет нам возможность добавлять/изменять/удалять методы класса. Дело в том, что изменение интерфейса класса в специализациях шаблона не всегда приветствуется, так как программист может это дело забыть, а оно, в свою очередь, приведет к ошибкам.
Создадим объекты Repository8<T>
и Repository8<bool>
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
int main() { // Объявляем целочисленный объект-массив (создается экземпляр Repository8<T>, где T = int) Repository8<int> intRepository; for (int count=0; count<8; ++count) intRepository.set(count, count); for (int count=0; count<8; ++count) std::cout << intRepository.get(count) << '\n'; // Объявляем объект-массив типа bool (создается экземпляр специализации Repository8<bool>) Repository8<bool> boolRepository; for (int count=0; count<8; ++count) boolRepository.set(count, count % 5); for (int count=0; count<8; ++count) std::cout << (boolRepository.get(count) ? "true" : "false") << '\n'; return 0; } |
Как вы можете видеть, результат тот же, что и выше, где использовался общий шаблон класса Repository8:
0
1
2
3
4
5
6
7
false
true
true
true
true
false
true
true
Следует еще раз отметить, что сохранение открытого интерфейса в шаблонах ваших классов вместе со специализациями, как правило, является хорошей идеей, поскольку это упрощает использование классов и их логику, хоть и не считается строго необходимым.
Эти каким математиком надо быть, что бы так рассчитывать побитовое возвращение. Проще не экономить память)
понятное дело так проще, но представь себе, что происходит симуляция большого количества физических точек, типа молекул или атомов(будем считать что их больше десятка милионов), с безумным количеством различных булов, там волей не волей, а если хочешь получить оптимизированную симуляцию, то лучше всетаки использовать побитовые вычисления.
например "клеточная игра в жизнь" где у каждой "живой" есть проверка на наличие 8 и более соседних клеток
Вы не правы, памяти как раз хватит, а вот с такой оптимизацией будут лишние вычисления масок, что затормозит процесс
Размышления, конечно, очень интересные, но хотелось бы формального доказательства, почему побитовые операции дороже, чем экономия 7 байт на одном объекте. (если их, скажем, довольно много в какой-нибудь агрегирующей структуре данных.)
Нужно сравнить оба варианта на практике.
Второй момент — использование побитовых сдвигов из статьи для микроконтроллеров, где количество памяти важнее тактов процессора. Хотя тут, конечно, сомнительно, применимо ли ООП и templates с программированием под микроконтроллеры.
(Дайте знать, если применимо)
cпециализация шаблона класса не работает
'Repository8' is not a class template
Потому что нужно указывать ещё и следующий код:
Спасибо!