На уроці про спеціалізацію шаблону функції ми розглядали шаблон класу Repository:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
#include <iostream> template <class T> class Repository { private: T m_value; public: Repository(T value) { m_value = value; } ~Repository() { } void print() { std::cout << m_value << '\n'; } }; |
Ми говорили про проблему цього шаблону при роботі з типом char*, коли виконувалося поверхневе копіювання (присвоювання вказівника) в конструкторі класу Repository. В якості рішення ми використовували повну спеціалізацію шаблону для створення спеціалізованої версії конструктора класу Repository для роботи з типом char*, в якому виділялася пам’ять і виконувалося глибоке копіювання m_value. Ось спеціалізація конструктора і деструктора класу Repository для роботи з типом char* (з матеріалів того ж уроку):
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
template <> Repository<char*>::Repository(char* value) { // Визначаємо довжину value int length=0; while (value[length] != '\0') ++length; ++length; // +1, враховуючи нуль-термінатор // Виділяємо пам'ять для зберігання значення value m_value = new char[length]; // Копіюємо фактичне значення з value в m_value for (int count=0; count < length; ++count) m_value[count] = value[count]; } template<> Repository<char*>::~Repository() { delete[] m_value; } |
Хоча все відмінно працює з типом char*, але як щодо інших типів вказівників (наприклад, int*)? Оскільки T — це будь-який тип вказівника, то при роботі з тим же int* виконається поверхневе копіювання (що нам не потрібно), або нам доведеться дублювати вищенаведений код (спеціалізація конструктора і деструктора), але вже замість char* використовувати int*. А дублювання коду, як ми вже знаємо, не найкращий варіант!
На щастя, використовуючи часткову спеціалізацію шаблону, ми можемо визначити спеціальну версію класу Repository, яка працювала б з усіма типами вказівників (при цьому не потрібно вказувати конкретні типи вказівників):
|
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
#include <iostream> // Загальний шаблон класу Repository template <class T> class Repository { private: T m_value; public: Repository(T value) { m_value = value; } ~Repository() { } void print() { std::cout << m_value << '\n'; } }; template <typename T> class Repository<T*> // часткова спеціалізація шаблону класу Repository для роботи з типами вказівників { private: T* m_value; public: Repository(T* value) // T - тип вказівника { // Виконуємо глибоке копіювання m_value = new T(*value); // тут копіюється тільки одне окреме значення (не масив значень) } ~Repository() { delete m_value; // а тут виконується видалення цього значення } void print() { std::cout << *m_value << '\n'; } }; |
І приклад з практики:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
int main() { // Оголошуємо цілочисельний об'єкт для перевірки роботи загального шаблону класу Repository<int> myint(6); myint.print(); // Оголошуємо об'єкт з типом вказівник для перевірки роботи часткової спеціалізації шаблону класу int x = 8; Repository<int*> myintptr(&x); // Якби в myintptr виконалося поверхневе копіювання (присвоювання вказівника), // то зміна значення x призвела б до зміни значення myintptr x = 10; myintptr.print(); return 0; } |
Результат:
6
8
При оголошенні об’єкта myintptr з типом int*, компілятор бачить, що ми раніше визначили часткову спеціалізацію шаблону класу для роботи з типами вказівників, і, враховуючи, що ми використовували тип int*, компілятор створить екземпляр часткової спеціалізації шаблону для роботи з типом вказівника. Конструктор цієї спеціалізації виконує глибоке копіювання параметру x. Пізніше, коли ми змінюємо значення x на 10, myintptr.m_value ніяк не змінюється, тому що виконалося глибоке копіювання, при якому m_value отримав свою власну копію x.
Якби цієї часткової спеціалізації не існувало, то створився б екземпляр загального шаблону класу, в якому виконалося б поверхневе копіювання, а myintptr.m_value і x вказували б на одну і ту ж адресу в пам’яті. В такому випадку, при зміні значення змінної x на 10, ми також зачепили б і значення myintptr (воно також стало б дорівнювати 10).
Варто відзначити, що, оскільки в нашій частковій спеціалізації копіюється лише одне значення, при роботі з рядками C-style копіюватиметься тільки перший символ (тому що рядок — це масив, а вказівник на масив вказує тільки на перший елемент масиву). Якщо ж потрібно повністю скопіювати рядок, то спеціалізація конструктора (і деструктора) для типу char* повинна бути повною. В такому випадку, повна спеціалізація матиме більший пріоритет, ніж часткова спеціалізація. Наприклад, ось програма, в якій використовується як часткова спеціалізація для роботи з типами вказівників, так і повна спеціалізація для роботи з типом char*:
|
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 |
#include <iostream> #include <cstring> // Загальний шаблон класу Repository для роботи з НЕ вказівниками template <class T> class Repository { private: T m_value; public: Repository(T value) { m_value = value; } ~Repository() { } void print() { std::cout << m_value << '\n'; } }; // Часткова спеціалізація шаблону класу Repository для роботи з вказівниками template <class T> class Repository<T*> { private: T* m_value; public: Repository(T* value) { m_value = new T(*value); } ~Repository() { delete m_value; } void print() { std::cout << *m_value << '\n'; } }; // Повна спеціалізація шаблону конструктора класу Repository для роботи з типом char* template <> Repository<char*>::Repository(char* value) { // Визначаємо довжину value int length = 0; while (value[length] != '\0') ++length; ++length; // +1, враховуючи нуль-термінатор // Виділяємо пам'ять для зберігання значення value m_value = new char[length]; // Копіюємо фактичне значення value в m_value for (int count = 0; count < length; ++count) m_value[count] = value[count]; } // Повна спеціалізація шаблону деструктора класу Repository для роботи з типом char* template<> Repository<char*>::~Repository() { delete[] m_value; } // Повна спеціалізація шаблону метода print() для роботи з типом char*. // Без цього вивід Repository<char*> призвів би до виклику Repository<T*>::print(), який виводить тільки одне значення (у випадку з рядком C-style - тільки перший символ) template<> void Repository<char*>::print() { std::cout << m_value; } int main() { // Оголошуємо цілочисельний об'єкт для перевірки роботи загального шаблону класу Repository<int> myint(6); myint.print(); // Оголошуємо об'єкт з типом вказівник для перевірки роботи часткової спеціалізації шаблону int x = 8; Repository<int*> myintptr(&x); // Якби в myintptr виконалося поверхневе копіювання (присвоювання вказівника), // то зміна значення x призвела б до зміни значення myintptr x = 10; myintptr.print(); // Динамічно виділяємо тимчасовий рядок char *name = new char[40]{ "Anton" }; // необхідна підтримка C++14 // Якщо ваш компілятор не підтримує C++14, то закоментуйте рядок вище і розкоментуйте рядки нижче // char *name = new char[40]; // strcpy(name, "Anton"); // Зберігаємо ім'я Repository<char*> myname(name); // Видаляємо тимчасовий рядок delete[] name; // Виводимо ім'я myname.print(); } |
Все працює як потрібно:
6
8
Anton
Загалом, використання часткової спеціалізації шаблону класу для роботи з типами вказівників дозволяє передбачити всі можливі варіанти використання коду на практиці.

(43 оцінок, середня: 4,93 з 5)