Мова C++ не компілює шаблони функцій напряму. Замість цього, коли компілятор зустрічає виклик шаблону функції, він копіює шаблон функції і замінює типи параметрів шаблону функції фактичними (переданими) типами даних. Функція з фактичними типами даних називається екземпляром шаблону функції (або «об’єктом шаблону функції»).
Розглянемо це на практиці. По-перше, створимо шаблон функції:
1 2 3 4 5 |
template <typename T> // оголошення параметра шаблону функції const T& max(const T& a, const T& b) { return (a > b) ? a : b; } |
Потім зробимо виклик шаблону функції:
1 |
int i = max(4, 8); // викликається max(int, int) |
Компілятор бачить, що обидва числа є цілочисельними, тому він копіює шаблон функції і створює екземпляр шаблону max(int, int)
:
1 2 3 4 |
const int& max(const int &a, const int &b) { return (a > b) ? a : b; } |
Це тепер уже «звичайна функція». Припустимо, що нам потрібно знову викликати функцію max(), але вже з іншим типом даних:
1 |
double d = max(7.58, 19.378); // викликається max(double, double) |
Мова C++ автоматично створює екземпляр шаблону max(double, double)
:
1 2 3 4 |
const double& max(const double &a, const double &b) { return (a > b) ? a : b; } |
І потім компілює його. Також варто відзначити, що, якщо ви створите шаблон функції, але не викличете його, екземпляри цього шаблону не будуть створені.
Оператори, виклики функцій і шаблони функцій
Шаблони функцій працюють як з фундаментальними типами даних (char, int, double тощо), так і з класами (але є нюанс). Екземпляр шаблону компілюється як звичайна функція. У звичайній функції будь-які оператори або виклики інших функцій, які використовуються в цій функції, повинні бути визначені/перевантажені, або ви отримаєте помилку компіляції. Аналогічно, будь-які оператори або виклики інших функцій, які присутні в шаблоні функції, повинні бути визначені/перевантажені для роботи з фактичними (переданими) типами даних. Розглянемо це на практиці.
По-перше, створимо простий клас:
1 2 3 4 5 6 7 8 9 10 |
class Dollars { private: int m_dollars; public: Dollars(int dollars) : m_dollars(dollars) { } }; |
Тепер подивимося, що станеться при спробі виклику функції max() з об’єктами класу Dollars:
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 |
template <typename T> // оголошення параметра шаблону функції const T& max(const T& a, const T& b) { return (a > b) ? a : b; } class Dollars { private: int m_dollars; public: Dollars(int dollars) : m_dollars(dollars) { } }; int main() { Dollars seven(7); Dollars twelve(12); Dollars bigger = max(seven, twelve); return 0; } |
Мова C++ створить наступний екземпляр шаблону функції max():
1 2 3 4 |
const Dollars& max(const Dollars &a, const Dollars &b) { return (a > b) ? a : b; } |
А потім компілятор спробує скомпілювати цю функцію, але нічого не вийде, тому що C++ не знає, як обробляти вираз a > b
! Отже, це призведе до помилки:
Помилка C2676 бінарний ">": "const T" не визначає цей оператор або перетворення до типу припустимо до вбудованого оператора
Повідомлення про помилку вказує на той факт, що ми не перевантажили оператор > для класу Dollars. Давайте перевантажимо:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class Dollars { private: int m_dollars; public: Dollars(int dollars) : m_dollars(dollars) { } friend bool operator>(const Dollars &d1, const Dollars &d2) { return (d1.m_dollars > d2.m_dollars); } }; |
Тепер мова C++ знає, як обробляти вираз a > b
, коли в якості змінних використовуються об’єкти класу Dollars!
Ще один приклад
Створимо шаблон функції, яка обчислює середнє арифметичне елементів масиву:
1 2 3 4 5 6 7 8 9 10 |
template <class T> T average(T *array, int length) { T sum = 0; for (int count=0; count < length; ++count) sum += array[count]; sum /= length; return sum; } |
Протестуємо:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
#include <iostream> template <class T> T average(T *array, int length) { T sum = 0; for (int count=0; count < length; ++count) sum += array[count]; sum /= length; return sum; } int main() { int array1[] = { 6, 4, 1, 3, 7 }; std::cout << average(array1, 5) << '\n'; double array2[] = { 4.25, 5.37, 8.44, 9.25 }; std::cout << average(array2, 4) << '\n'; return 0; } |
Результат:
4
6.8275
Як ви бачите, все відмінно працює з фундаментальними типами даних!
Оскільки тип повернення шаблону функції той же, що і тип переданих елементів масиву в функцію, то обчислення середнього арифметичного цілочисельних значень призведе до цілочисельного результату (з відкиданням будь-якої дробової частини), як і обчислення значень типу double призведе до результату типу double. Це може бути не очевидним, тому хорошим тоном буде вказати на це в коментарях.
Тепер подивимося, що станеться при виконанні функції average() з об’єктами класу Dollars:
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 |
#include <iostream> class Dollars { private: int m_dollars; public: Dollars(int dollars) : m_dollars(dollars) { } friend bool operator>(const Dollars &d1, const Dollars &d2) { return (d1.m_dollars > d2.m_dollars); } }; template <class T> T average(T *array, int length) { T sum = 0; for (int count=0; count < length; ++count) sum += array[count]; sum /= length; return sum; } int main() { Dollars array3[] = { Dollars(7), Dollars(12), Dollars(18), Dollars(15) }; std::cout << average(array3, 4) << '\n'; return 0; } |
Результат:
1>c:\users\kicli\source\repos\consoleapplication10\consoleapplication10\consoleapplication10.cpp(37): error C2679: бінарний "<<": не знайдено оператор, який приймає правий операнд типу "T" (або прийнятне перетворення відсутнє) 1> with
1> [
1> T=Dollars
1> ]
1>c:\program files (x86)\microsoft visual studio\2017\community\vc\tools\msvc\14.15.26726\include\ostream(508): note: может быть "std::basic_ostream<char,std::char_traits> &std::basic_ostream<char,std::char_traits>::operator <<(std::basic_streambuf<char,std::char_traits> *)"
1>c:\program files (x86)\microsoft visual studio\2017\community\vc\tools\msvc\14.15.26726\include\ostream(480): note: або "std::basic_ostream<char,std::char_traits> &std::basic_ostream<char,std::char_traits>::operator <<(const void *)" 1>c:\program files (x86)\microsoft visual studio\2017\community\vc\tools\msvc\14.15.26726\include\ostream(460): note: або "std::basic_ostream<char,std::char_traits> &std::basic_ostream<char,std::char_traits>::operator <<(long double)" 1>c:\program files (x86)\microsoft visual studio\2017\community\vc\tools\msvc\14.15.26726\include\ostream(440): note: або "std::basic_ostream<char,std::char_traits> &std::basic_ostream<char,std::char_traits>::operator <<(double)" 1>c:\program files (x86)\microsoft visual studio\2017\community\vc\tools\msvc\14.15.26726\include\ostream(420): note: або "std::basic_ostream<char,std::char_traits> &std::basic_ostream<char,std::char_traits>::operator <<(float)" 1>c:\program files (x86)\microsoft visual studio\2017\community\vc\tools\msvc\14.15.26726\include\ostream(400): note: або "std::basic_ostream<char,std::char_traits> &std::basic_ostream<char,std::char_traits>::operator <<(unsigned __int64)" 1>c:\program files (x86)\microsoft visual studio\2017\community\vc\tools\msvc\14.15.26726\include\ostream(380): note: або "std::basic_ostream<char,std::char_traits> &std::basic_ostream<char,std::char_traits>::operator <<(__int64)" 1>c:\program files (x86)\microsoft visual studio\2017\community\vc\tools\msvc\14.15.26726\include\ostream(360): note: або "std::basic_ostream<char,std::char_traits> &std::basic_ostream<char,std::char_traits>::operator <<(unsigned long)" 1>c:\program files (x86)\microsoft visual studio\2017\community\vc\tools\msvc\14.15.26726\include\ostream(340): note: або "std::basic_ostream<char,std::char_traits> &std::basic_ostream<char,std::char_traits>::operator <<(long)" 1>c:\program files (x86)\microsoft visual studio\2017\community\vc\tools\msvc\14.15.26726\include\ostream(320): note: або "std::basic_ostream<char,std::char_traits> &std::basic_ostream<char,std::char_traits>::operator <<(unsigned int)" 1>c:\program files (x86)\microsoft visual studio\2017\community\vc\tools\msvc\14.15.26726\include\ostream(295): note: або "std::basic_ostream<char,std::char_traits> &std::basic_ostream<char,std::char_traits>::operator <<(int)" 1>c:\program files (x86)\microsoft visual studio\2017\community\vc\tools\msvc\14.15.26726\include\ostream(275): note: або "std::basic_ostream<char,std::char_traits> &std::basic_ostream<char,std::char_traits>::operator <<(unsigned short)" 1>c:\program files (x86)\microsoft visual studio\2017\community\vc\tools\msvc\14.15.26726\include\ostream(241): note: або "std::basic_ostream<char,std::char_traits> &std::basic_ostream<char,std::char_traits>::operator <<(short)" 1>c:\program files (x86)\microsoft visual studio\2017\community\vc\tools\msvc\14.15.26726\include\ostream(221): note: або "std::basic_ostream<char,std::char_traits> &std::basic_ostream<char,std::char_traits>::operator <<(bool)" 1>c:\program files (x86)\microsoft visual studio\2017\community\vc\tools\msvc\14.15.26726\include\ostream(215): note: або "std::basic_ostream<char,std::char_traits> &std::basic_ostream<char,std::char_traits>::operator <<(std::ios_base &(__cdecl *)(std::ios_base &))" 1>c:\program files (x86)\microsoft visual studio\2017\community\vc\tools\msvc\14.15.26726\include\ostream(209): note: або "std::basic_ostream<char,std::char_traits> &std::basic_ostream<char,std::char_traits>::operator <<(std::basic_ios<char,std::char_traits> &(__cdecl *)(std::basic_ios<char,std::char_traits> &))"
1>c:\program files (x86)\microsoft visual studio\2017\community\vc\tools\msvc\14.15.26726\include\ostream(204): note: або "std::basic_ostream<char,std::char_traits> &std::basic_ostream<char,std::char_traits>::operator <<(std::basic_ostream<char,std::char_traits> &(__cdecl *)(std::basic_ostream<char,std::char_traits> &))"
1>c:\program files (x86)\microsoft visual studio\2017\community\vc\tools\msvc\14.15.26726\include\ostream(702): note: або "std::basic_ostream<char,std::char_traits> &std::operator <<<char,std::char_traits>(std::basic_ostream<char,std::char_traits> &,const char *)"
1>c:\program files (x86)\microsoft visual studio\2017\community\vc\tools\msvc\14.15.26726\include\ostream(749): note: або "std::basic_ostream<char,std::char_traits> &std::operator <<<char,std::char_traits>(std::basic_ostream<char,std::char_traits> &,char)"
1>c:\program files (x86)\microsoft visual studio\2017\community\vc\tools\msvc\14.15.26726\include\ostream(787): note: або "std::basic_ostream<char,std::char_traits> &std::operator <<<std::char_traits>(std::basic_ostream<char,std::char_traits> &,const char *)"
1>c:\program files (x86)\microsoft visual studio\2017\community\vc\tools\msvc\14.15.26726\include\ostream(834): note: або "std::basic_ostream<char,std::char_traits> &std::operator <<<std::char_traits>(std::basic_ostream<char,std::char_traits> &,char)"
1>c:\program files (x86)\microsoft visual studio\2017\community\vc\tools\msvc\14.15.26726\include\ostream(960): note: або "std::basic_ostream<char,std::char_traits> &std::operator <<<std::char_traits>(std::basic_ostream<char,std::char_traits> &,const signed char *)"
1>c:\program files (x86)\microsoft visual studio\2017\community\vc\tools\msvc\14.15.26726\include\ostream(967): note: або "std::basic_ostream<char,std::char_traits> &std::operator <<<std::char_traits>(std::basic_ostream<char,std::char_traits> &,signed char)"
1>c:\program files (x86)\microsoft visual studio\2017\community\vc\tools\msvc\14.15.26726\include\ostream(974): note: або "std::basic_ostream<char,std::char_traits> &std::operator <<<std::char_traits>(std::basic_ostream<char,std::char_traits> &,const unsigned char *)"
1>c:\program files (x86)\microsoft visual studio\2017\community\vc\tools\msvc\14.15.26726\include\ostream(981): note: або "std::basic_ostream<char,std::char_traits> &std::operator <<<std::char_traits>(std::basic_ostream<char,std::char_traits> &,unsigned char)"
1>c:\program files (x86)\microsoft visual studio\2017\community\vc\tools\msvc\14.15.26726\include\ostream(1047): note: або "std::basic_ostream<char,std::char_traits> &std::operator <<<char,std::char_traits>(std::basic_ostream<char,std::char_traits> &,const std::error_code &)"
1>c:\users\kicli\source\repos\consoleapplication10\consoleapplication10\consoleapplication10.cpp(37): note: при спробі зіставити список аргументів "(std::ostream, T)"
1> with
1> [
1> T=Dollars
1> ]
Компілятор збожеволів. Ми згадували про такі помилки на попередньому уроці. Незважаючи на настільки об’ємний «результат», тут все досить просто. У перших рядках повідомляється, що компілятор не зміг знайти перевантаження оператора << для класу Dollars. Далі вказуються функції з типами даних, які викликалися для порівняння, але так і не підійшли. І в кінці вказуються параметр шаблону і фактичний тип параметра.
Visual Studio береже наші нерви і надає нам альтернативний вивід помилок:
Помилка E0349 відсутній оператор "<<", який відповідає цим операндам
Помилка C2679 бінарний "<<": не знайдено оператор, який приймає правий операнд типу "T" (або прийнятне перетворення відсутнє)
Пам’ятайте, що average() повертає об’єкт класу Dollars, а ми намагаємося цей об’єкт вивести за допомогою оператора виводу <<
і std::cout. Однак, ми не перевантажили оператор <<
для класу Dollars. Давайте виправимо це:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
class Dollars { private: int m_dollars; public: Dollars(int dollars) : m_dollars(dollars) { } friend bool operator>(const Dollars &d1, const Dollars &d2) { return (d1.m_dollars > d2.m_dollars); } friend std::ostream& operator<< (std::ostream &out, const Dollars &dollars) { out << dollars.m_dollars << " dollars "; return out; } }; |
Якщо ж тепер запустити програму, то отримаємо наступне:
Помилка C2676 бінарний "+=": "T" не визначає цей оператор або перетворення до типу припустимо до вбудованого оператора
Помилка C2676 бінарний "/=": "T" не визначає цей оператор або перетворення до типу припустимо до вбудованого оператора
Ці помилки були викликані екземпляром шаблону функції, створеним при виклику average(Dollars*, int)
. Пам’ятайте, що при виклику шаблону функції, компілятор копіює шаблон функції з типами параметрів, а потім замінює типи параметрів шаблону на фактичні (передані) типи даних. Ось екземпляр шаблону функції average(), де T
є класом Dollars:
1 2 3 4 5 6 7 8 9 10 |
template <class Dollars> Dollars average(Dollars *array, int length) { Dollars sum = 0; for (int count=0; count < length; ++count) sum += array[count]; sum /= length; return sum; } |
Причина, по якій ми отримали повідомлення про помилку, криється в наступному рядку:
1 |
sum += array[count]; |
В цьому випадку sum
є об’єктом класу Dollars. А щоб все запрацювало, нам потрібно перевантажити оператори +=
і /=
для класу Dollars:
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 |
class Dollars { private: int m_dollars; public: Dollars(int dollars) : m_dollars(dollars) { } friend bool operator>(const Dollars &d1, const Dollars &d2) { return (d1.m_dollars > d2.m_dollars); } friend std::ostream& operator<< (std::ostream &out, const Dollars &dollars) { out << dollars.m_dollars<< " dollars "; return out; } Dollars& operator+=(Dollars dollars) { m_dollars += dollars.m_dollars; return *this; } Dollars& operator/=(int value) { m_dollars /= value; return *this; } }; |
Нарешті, наш код скомпілюється, і результат:
13 dollars
Хоча виконана робота може здатися дуже великою, але це тільки через те, що наш клас Dollars з самого початку був “худий як тріска”. Ключовий момент заключається в тому, що нам не потрібно модифікувати average(), щоб він працював з об’єктами класу Dollars (або з будь-яким іншим типом даних). Вся робота була пророблена тільки з класом Dollars, а про все інше компілятор подбав самостійно!