Язык 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 T> 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 (или c любым другим типом данных). Вся работа была проделана только с классом Dollars, а обо всем остальном компилятор позаботился самостоятельно!
Следующий момент интересен — нам не пришлось перегружать конструктор копирования:
но если бы в нашем шаблоне мы ислользовали оператор= повторно, то нам пришлось бы либо надеятся, что проканает "Оператор присваивания по умолчанию", либо добавлять свой оператор присваивания. (урок 144)
В классе Dollars нет динамического выделения памяти, поэтому здесь будет более чем достаточно оператора поверхностного копирования, предоставляемого по-умолчаию….
В данном случае ни один из нормальных компиляторов не будет вызывать конструктор копий, здесь будет напрямую вызван конструктор (если не путаю, это что-то типа эллизии).
Вызываться будет обычный конструктор( не копирования) , потому что мы присваиваем значение , а не копируем)