На попередньому уроці ми говорили про те, як спеціалізувати шаблон функції, щоб при роботі з одним типом даних була одна реалізація функції, а при роботі з іншим типом даних — інша реалізація функції. Виявляється, ми можемо спеціалізувати не тільки шаблони функцій, але і шаблони класів.
Розглянемо клас-масив, який може зберігати 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 32 |
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 байт). Чому саме так? Про це читайте на уроці №48 і на уроці №49.
Тепер при оголошенні об’єкту класу 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
Слід ще раз відзначити, що збереження відкритого інтерфейсу в шаблонах ваших класів разом зі спеціалізаціями, як правило, є хорошою ідеєю, оскільки це спрощує використання класів і їх логіку, хоч і не вважається строго необхідним.