Урок №183. Шаблони класів

  Юрій  | 

  Оновл. 25 Бер 2021  | 

 32

На попередніх уроках ми дізналися, як за допомогою шаблонів функцій зробити одну версію функції, яка працюватиме з різними типами даних. Хоча це значний крок на шляху до узагальненого програмування, це не вирішує всіх наших проблем. Розглянемо приклад такої проблеми і те, як шаблони допоможуть нам в її вирішенні.

Шаблони і контейнерні класи

На уроці про контейнерні класи ми дізналися, як, використовуючи композицію, реалізувати класи, які містять кілька об’єктів певного типу даних. В якості прикладу ми використовували клас ArrayInt:

Хоча цей клас забезпечує простий спосіб створення масиву цілочисельних значень, але що, якщо нам потрібно буде працювати зі значеннями типу double? Використовуючи традиційні методи програмування ми створили б новий клас ArrayDouble для роботи зі значеннями типу double:

Хоча коду багато, але класи майже ідентичні, змінюється тільки тип даних! Як ви вже могли б здогадатися, це ідеальний випадок для використання шаблонів. Створення шаблону класу аналогічно створенню шаблону функції. Наприклад, створимо шаблон класу Array:

Array.h:

Як ви можете бачити, ця версія майже ідентична версії ArrayInt, за винятком того, що ми додали оголошення параметра шаблону класу і змінили тип даних з int на T.

Зверніть увагу, ми визначили функцію getLength() поза тілом класу. Це необов’язково, але початківці зазвичай спотикаються на цьому через синтаксис. Кожен метод шаблону класу, оголошений поза тілом класу, потребує власного оголошення шаблону. Також зверніть увагу, що ім’я шаблону класу — Array<T>, а не Array (Array вказуватиме на НЕ шаблонну версію класу Array).

Ось приклад використання шаблону класу Array:

Результат:

9     9.5
8     8.5
7     7.5
6     6.5
5     5.5
4     4.5
3     3.5
2     2.5
1     1.5
0     0.5

Шаблони класів працюють точно так же, як і шаблони функцій: компілятор копіює шаблон класу, замінюючи типи параметрів шаблону класу на фактичні (передані) типи даних, а потім компілює цю копію. Якщо у вас є шаблон класу, але ви його не використовуєте, то компілятор не буде його навіть компілювати.

Шаблони класів ідеально підходять для реалізації контейнерних класів, тому що дуже часто таким класам доводиться працювати з різними типами даних, а шаблони дозволяють це організувати в мінімальній кількості коду. Хоча синтаксис трохи потворний, і повідомлення про помилки іноді можуть бути «об’ємними», шаблони класів дійсно є однією з кращих і найбільш корисних конструкцій мови C++.

Шаблони класів в Стандартній бібліотеці С++

Тепер ви вже зрозуміли, чим насправді є std::vector<int>? Правильно, std::vector — це шаблон класу, а int — це всього лише переданий тип даних! Стандартна бібліотека С++ містить багато визначених шаблонів класів, доступних для вашого використання.

Шаблони класів і Заголовкові файли

Шаблон не є ані класом, ані функцією — це трафарет, який використовується для створення класів або функцій. Таким чином, шаблони працюють не так, як звичайні функції або класи. У більшості випадків це не є проблемою, але на практиці трапляються різні ситуації.

Працюючи зі звичайними класами ми поміщаємо визначення класу в заголовковий файл, а визначення методів цього класу в окремий файл .cpp з аналогічним ім’ям. Таким чином, фактичне визначення класу компілюється як окремий файл всередині проекту. Однак з шаблонами все відбувається дещо інакше (про те, чому слід поміщати визначення методів класу в окремий файл, читайте в матеріалах уроку №130). Розглянемо наступне:

Array.h:

Array.cpp:

main.cpp:

Вищенаведена програма скомпілюється, але викличе наступну помилку лінкера:

unresolved external symbol "public: int __thiscall Array::getLength(void)" (?GetLength@?$Array@H@@QAEHXZ)

Чому так? Зараз розберемося.

Для використання шаблону компілятор повинен бачити як визначення шаблону (а не тільки оголошення), так і тип шаблону, який застосовується для створення екземпляру шаблону. Пам’ятаємо, що мова C++ компілює файли окремо. Коли заголовок Array.h підключається в main.cpp, то визначення шаблону класу копіюється в цей файл. У main.cpp компілятор бачить, що нам потрібні два екземпляри шаблону класу: Array<int> і Array<double>, він створить їх, а потім скомпілює весь цей код як частину файлу main.cpp. Однак, коли справа дійде до компіляції Array.cpp (окремим файлом), компілятор забуде, що ми використовували Array<int> і Array<double> в main.cpp і не створить екземпляр шаблону функції getLength(), який нам потрібен для виконання програми. Ми отримаємо помилку лінкера, тому що компілятор не зможе знайти визначення Array<int>::getLength() або Array<double>::getLength().

Цю проблему можна вирішити кількома способами.

Найпростіший варіант — помістити код з Array.cpp в Array.h нижче класу. Таким чином, коли ми підключатимемо Array.h, весь код шаблону класу (повне оголошення і визначення як класу, так і його методів) знаходитиметься в одному місці. Плюс цього способу — простота. Мінус — якщо шаблон класу використовується в багатьох місцях, то ми отримаємо багато локальних копій шаблону класу, що збільшить час компіляції і лінкінгу файлів (лінкер повинен буде видалити дублювання визначень класу і методів, щоб виконуваний файл не був «занадто роздутим»). Рекомендується використовувати це рішення до тих пір, поки час компіляції або лінкінгу не є проблемою.

Якщо ви вважаєте, що розміщення коду з Array.cpp в Array.h зробить Array.h занадто велики /безладним, то альтернативою буде перейменування Array.cpp в Array.inl (.inl від англ. inline” = “вбудований”), а потім підключення Array.inl з нижньої частини файлу Array.h. Це дасть той же результат, що і розміщення всього коду в заголовку, але таким чином код вийде трохи чистішим.

Є ще одне рішення — підключення файлів .cpp, але цей варіант не рекомендується використовувати через нестандартне застосування директиви #include.

Ще один альтернативний варіант — використовувати підхід 3-х файлів:

   Визначення шаблону класу зберігається в заголовковому файлі.

   Визначення методів шаблону класу зберігаються в окремому файлі .cpp.

   Потім додаємо третій файл, який містить всі необхідні нам екземпляри шаблону класу.

Наприклад, templates.cpp:

Частина template class змусить компілятор явно створити зазначені екземпляри шаблону класу. У прикладі, наведеному вище, компілятор створить Array<int> і Array<double> всередині templates.cpp. Оскільки templates.cpp знаходиться всередині нашого проекту, то він скомпілюється і вдало зв’яжеться з іншими файлами (відбудеться лінкінг).

Цей метод ефективніший, але вимагає створення/підтримки третього файлу (templates.cpp) для кожної з ваших програм (проектів) окремо.

Оцінити статтю:

1 Зірка2 Зірки3 Зірки4 Зірки5 Зірок (1 оцінок, середня: 5,00 з 5)
Loading...

Залишити відповідь

Ваш E-mail не буде опублікований. Обов'язкові поля відмічені *