На цьому уроці ми розглянемо, що таке часткова спеціалізація шаблону в мові С++, як вона використовується і які є нюанси.
Проблема
На уроці №184 ми дізналися, яким чином можна використовувати додатковий параметр шаблону. Розглянемо ще раз клас StaticArray з матеріалів того ж уроку:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
template <class T, int size> // size є non-type параметром шаблону class StaticArray { private: // Параметр size відповідає за довжину масиву T m_array[size]; public: T* getArray() { return m_array; } T& operator[](int index) { return m_array[index]; } }; |
Тут у нас є 2 параметри шаблону класу: параметр типу і параметр non-type.
Тепер припустимо, що нам потрібно написати функцію для виводу всіх елементів масиву. Хоча ми можемо зробити це через метод класу, ми реалізуємо це через окрему функцію (заради кращого розуміння теми).
Використовуючи шаблон функції, ми можемо написати наступне:
1 2 3 4 5 6 |
template <typename T, int size> void print(StaticArray<T, size> &array) { for (int count=0; count < size; ++count) std::cout << array[count] << ' '; } |
Це дозволить нам зробити:
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 |
#include <iostream> #include <cstring> template <class T, int size> // size є non-type параметром шаблону class StaticArray { private: // Параметр size відповідає за довжину масиву T m_array[size]; public: T* getArray() { return m_array; } T& operator[](int index) { return m_array[index]; } }; template <typename T, int size> void print(StaticArray<T, size> &array) { for (int count = 0; count < size; ++count) std::cout << array[count] << ' '; } int main() { // Оголошуємо цілочисельний масив StaticArray<int, 5> int5; int5[0] = 0; int5[1] = 1; int5[2] = 2; int5[3] = 3; int5[4] = 4; // Виводимо елементи масиву print(int5); return 0; } |
І отримати:
0 1 2 3 4
Хоча все працює правильно, але є один нюанс. Розглянемо наступний код функції main():
1 2 3 4 5 6 7 8 9 10 11 12 |
int main() { // Оголошуємо масив типу char StaticArray<char, 14> char14; strcpy_s(char14.getArray(), 14, "Hello, world!"); // Виводимо елементи масиву print(char14); return 0; } |
Примітка: Ми розглядали strcpy_s на уроці про рядки C-style.
Програма скомпілюється з наступним результатом:
H e l l o , w o r l d !
Для всіх типів, крім char, є сенс вказувати пробіл між кожним елементом масиву, щоб елементи не «злипалися». Однак з типом char є сенс вивести все разом, як рядок C-style, щоб не було зайвих пробілів. Як ми можемо це виправити?
Повна спеціалізація шаблону — рішення?
Спочатку ми могли б подумати про використання спеціалізації шаблону функції. Однак проблема з повною спеціалізацією шаблону полягає в тому, що всі параметри шаблону повинні бути явно визначені. Наприклад:
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> #include <cstring> template <class T, int size> // size є non-type параметром шаблону class StaticArray { private: // Параметр size відповідає за довжину масиву T m_array[size]; public: T* getArray() { return m_array; } T& operator[](int index) { return m_array[index]; } }; template <typename T, int size> void print(StaticArray<T, size> &array) { for (int count = 0; count < size; ++count) std::cout << array[count] << ' '; } // Шаблон функції print() з повною спеціалізацією шаблону класу StaticArray для роботи з типом char і довжиною масиву 14 template <> void print(StaticArray<char, 14> &array) { for (int count = 0; count < 14; ++count) std::cout << array[count]; } int main() { // Оголошуємо масив типу char StaticArray<char, 14> char14; strcpy_s(char14.getArray(), 14, "Hello, world!"); // Виводимо елементи масиву print(char14); return 0; } |
Як ви можете бачити, ми додали шаблон функції print() для роботи з типом char.
Результат:
Hello, world!
Хоча одна проблема вирішена, виникає інша проблема: використання повної спеціалізації шаблону класу означає, що ми повинні явно вказувати довжину переданого масиву! Розглянемо наступний приклад:
1 2 3 4 5 6 7 8 9 10 11 12 |
int main() { // Оголошуємо масив типу char StaticArray<char, 12> char12; strcpy_s(char12.getArray(), 12, "Hello, dad!"); // Виводимо елементи масиву print(char12); return 0; } |
Виклик print(char12)
викличе шаблон функції print() із загальним шаблоном StaticArray<T, size>
, тому що char12 є типу StaticArray<char, 12>
, а шаблон функції print() приймає тільки StaticArray<char, 14>
(довжина масиву відрізняється).
Хоча ми могли б скопіювати ще раз шаблон функції print() для роботи зі StaticArray<char, 12>
, але це неефективно. А що, якщо нам потрібно буде пізніше використати масив з 5 або 20 елементами? Знову копіювати шаблон? Це зайва робота.
Очевидно, що повна спеціалізація шаблону класу тут є “рішенням-костилем”. Часткова спеціалізація шаблону — ось, що нам потрібно.
Часткова спеціалізація шаблону
Часткова спеціалізація шаблону дозволяє виконати спеціалізацію шаблону класу (але не функції!), де деякі (але не всі) параметри шаблону явно визначені. Для нашої вищенаведеної задачі ідеальне рішення полягає в тому, щоб шаблон функції print() працював зі StaticArray типу char, але при цьому розмір масиву не був фіксованим значенням, а міг варіюватися.
Ось наш шаблон функції print(), який приймає частково спеціалізований шаблон класу StaticArray:
1 2 3 4 5 6 7 |
// Шаблон функції print() з частково спеціалізованим шаблоном класу StaticArray<char, size> в якості параметра template <int size> // size як і раніше є non-type параметром void print(StaticArray<char, size> &array) // ми тут явно вказуємо тип char { for (int count = 0; count < size; ++count) std::cout << array[count]; } |
Як ви можете бачити, ми тут явно вказали тип char, але size
залишили не фіксованим, тому функція print() працюватиме з масивами типу 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 |
#include <iostream> #include <cstring> template <class T, int size> // size є non-type параметром шаблону class StaticArray { private: // Параметр size відповідає за довжину масиву T m_array[size]; public: T* getArray() { return m_array; } T& operator[](int index) { return m_array[index]; } }; template <typename T, int size> void print(StaticArray<T, size> &array) { for (int count = 0; count < size; ++count) std::cout << array[count] << ' '; } // Шаблон функції print() з частково спеціалізованим шаблоном класу StaticArray<char, size> в якості параметра template <int size> void print(StaticArray<char, size> &array) { for (int count = 0; count < size; ++count) std::cout << array[count]; } int main() { // Оголошуємо масив типу char довжиною 14 StaticArray<char, 14> char14; strcpy_s(char14.getArray(), 14, "Hello, world!"); // Виводимо елементи масиву print(char14); // Тепер оголошуємо масив типу char довжиною 12 StaticArray<char, 12> char12; strcpy_s(char12.getArray(), 12, "Hello, dad!"); // Виводимо елементи масиву print(char12); return 0; } |
Результат:
Hello, world! Hello, dad!
Як і очікували.
Зверніть увагу, починаючи з C++14 часткова спеціалізація шаблону може використовуватися тільки з класами, але не з окремими функціями (для функцій використовується тільки повна спеціалізація шаблону). Наш приклад void print(StaticArray<char, size> & array)
працює тільки тому, що шаблон функції print() приймає в якості параметра шаблон класу, який, в свою чергу, частково спеціалізований.
Часткова спеціалізація шаблонів методів
Обмеження часткової спеціалізації для функцій може призвести до певних проблем при роботі з методами класу. Наприклад, що, якби ми визначили StaticArray наступним чином:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
template <class T, int size> // size є non-type параметром шаблону class StaticArray { private: // Параметр size відповідає за довжину масиву T m_array[size]; public: T* getArray() { return m_array; } T& operator[](int index) { return m_array[index]; } void print() { for (int i = 0; i < size; i++) std::cout << m_array[i] << ' '; std::cout << "\n"; } }; |
Функція print() є методом класу StaticArray<T, int>
. Що станеться, якщо ми захочемо частково спеціалізувати шаблон функції print(), щоб метод працював по-іншому? Ми можемо спробувати виконати наступне:
1 2 3 4 5 6 7 8 |
// Не спрацює template <int size> void StaticArray<double, size>::print() { for (int i = 0; i < size; i++) std::cout << std::scientific << m_array[i] << " "; std::cout << "\n"; } |
На жаль, це не спрацює, тому що ми намагаємося частково спеціалізувати шаблон функції, що робити заборонено.
Як же це можна обійти? Одним з очевидних рішень є часткова спеціалізація шаблону всього класу:
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 |
#include <iostream> template <class T, int size> // size є non-type параметром шаблону class StaticArray { private: // Параметр size відповідає за довжину масиву T m_array[size]; public: T* getArray() { return m_array; } T& operator[](int index) { return m_array[index]; } void print() { for (int i = 0; i < size; i++) std::cout << m_array[i] << ' '; std::cout << "\n"; } }; template <int size> // size є non-type параметром шаблону class StaticArray<double, size> { private: // Параметр size відповідає за довжину масиву double m_array[size]; public: double* getArray() { return m_array; } double& operator[](int index) { return m_array[index]; } void print() { for (int i = 0; i < size; i++) std::cout << std::scientific << m_array[i] << ' '; std::cout << "\n"; } }; int main() { // Оголошуємо цілочисельний масив довжиною 5 StaticArray<int, 5> intArray; // Заповнюємо масив, а потім виводимо його на екран for (int count = 0; count < 5; ++count) intArray[count] = count; intArray.print(); // Оголошуємо масив типу double довжиною 4 StaticArray<double, 4> doubleArray; for (int count = 0; count < 4; ++count) doubleArray[count] = (4.0 + 0.1 * count); doubleArray.print(); return 0; } |
Результат:
0 1 2 3 4
4.000000e+00 4.100000e+00 4.200000e+00 4.300000e+00
Хоча це працює, але це не найкращий варіант, тому що у нас тепер купа дубльованого коду з StaticArray<T, size>
в StaticArray<double, size>
.
От якби можна було б використати код з StaticArray<T, size>
в StaticArray<double, size>
без дублювання. Нічого вам це не нагадує? Як на мене, то це звучить, як відмінний варіант для застосування спадкування!
Ви можете почати з:
1 2 |
template <int size> // size є non-type параметром шаблону class StaticArray<double, size>: public StaticArray< // а потім що? |
Але як ми можемо посилатися на StaticArray? Ніяк, але, на щастя, є обхідний шлях з використанням загального батьківського класу:
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 |
#include <iostream> template <class T, int size> // size є non-type параметром шаблону class StaticArray_Base { protected: // Параметр size відповідає за довжину масиву T m_array[size]; public: T* getArray() { return m_array; } T& operator[](int index) { return m_array[index]; } virtual void print() { for (int i = 0; i < size; i++) std::cout << m_array[i] << ' '; std::cout << "\n"; } }; template <class T, int size> // size є non-type параметром шаблону class StaticArray: public StaticArray_Base<T, size> { public: StaticArray() { } }; template <int size> // size є non-type параметром шаблону class StaticArray<double, size>: public StaticArray_Base<double, size> { public: virtual void print() override { for (int i = 0; i < size; i++) std::cout << std::scientific << this->m_array[i] << " "; // Примітка: Префікс this-> на вищенаведеному рядку необхідний. Чому? Читайте тут - https://stackoverflow.com/a/6592617 std::cout << "\n"; } }; int main() { // Оголошуємо цілочисельний масив довжиною 5 StaticArray<int, 5> intArray; // Заповнюємо його, а потім виводимо на екран for (int count = 0; count < 5; ++count) intArray[count] = count; intArray.print(); // Оголошуємо масив типу double довжиною 4 StaticArray<double, 4> doubleArray; // Заповнюємо його, а потім виводимо на екран for (int count = 0; count < 4; ++count) doubleArray[count] = (4. + 0.1*count); doubleArray.print(); return 0; } |
Результат той же, що і в прикладі, наведеному вище, але дубльованого коду менше.