Урок №174. Экземпляры шаблонов функций

  Юрий  | 

  |

  Обновл. 14 Июл 2023  | 

 38333

 ǀ   4 

Язык C++ не компилирует шаблоны функций напрямую. Вместо этого, когда компилятор встречает вызов шаблона функции, он копирует шаблон функции и заменяет типы параметров шаблона функции фактическими (передаваемыми) типами данных. Функция с фактическими типами данных называется экземпляром шаблона функции (или «объектом шаблона функции»).

Рассмотрим это на практике. Во-первых, создадим шаблон функции:

Затем сделаем вызов шаблона функции:

Компилятор видит, что оба числа являются целочисленными, поэтому он копирует шаблон функции и создает экземпляр шаблона max(int, int):

Это теперь уже «обычная функция». Допустим, что нам нужно снова вызвать функцию max(), но уже с другим типом данных:

Язык C++ автоматически создает экземпляр шаблона max(double, double):

И затем компилирует его. Также стоит отметить, что, если вы создадите шаблон функции, но не вызовете его, экземпляры этого шаблона созданы не будут.

Операторы, вызовы функций и шаблоны функций

Шаблоны функций работают как с фундаментальными типами данных (char, int, double и т.д.), так и с классами (но есть нюанс). Экземпляр шаблона компилируется как обычная функция. В обычной функции любые операторы или вызовы других функций, которые используются в этой функции, должны быть определены/перегружены, или вы получите ошибку компиляции. Аналогично, любые операторы или вызовы других функций, которые присутствуют в шаблоне функции, должны быть определены/перегружены для работы с фактическими (передаваемыми) типами данных. Рассмотрим это на практике.

Во-первых, создадим простой класс:

Теперь посмотрим, что произойдет при попытке вызова функции max() с объектами класса Dollars:

Язык C++ создаст следующий экземпляр шаблона функции max():

А затем компилятор попытается скомпилировать эту функцию, но ничего не получится, так как C++ не имеет понятия, как обрабатывать выражение a > b! Следовательно, это приведет к ошибке:

Ошибка C2676 бинарный ">": "const T" не определяет этот оператор или преобразование к типу приемлемо к встроенному оператору

Сообщение об ошибке указывает на тот факт, что мы не перегрузили оператор > для класса Dollars. Давайте перегрузим:

Теперь C++ знает, как обрабатывать выражение a > b, когда в качестве переменных используются объекты класса Dollars!

Еще один пример


Создадим шаблон функции, которая вычисляет среднее арифметическое элементов массива:

Протестируем:

Результат:

4
6.8275

Как вы видите, всё отлично работает с фундаментальными типами данных!

Поскольку тип возврата шаблона функции тот же, что и тип передаваемых элементов массива в функцию, то вычисление среднего арифметического целочисленных значений приведет к целочисленному результату (с отбрасыванием любой дробной части), как и вычисление значений типа double приведет к результату типа double. Это может быть не очевидным, поэтому хорошим тоном будет указать на это в комментариях.

Теперь посмотрим, что произойдет при вызове функции average() с объектами класса Dollars:

Результат:

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. Давайте исправим это:

Если же теперь запустить программу, то получим следующее:

Ошибка C2676 бинарный "+=": "T" не определяет этот оператор или преобразование к типу приемлемо к встроенному оператору
Ошибка C2676 бинарный "/=": "T" не определяет этот оператор или преобразование к типу приемлемо к встроенному оператору

Эти ошибки были вызваны экземпляром шаблона функции, созданным при вызове average(Dollars*, int). Помните, что при вызове шаблона функции, компилятор копирует шаблон функции с типами параметров, а затем заменяет типы параметров шаблона на фактические (передаваемые) типы данных. Вот экземпляр шаблона функции average(), где T является классом Dollars:

Причина, по которой мы получили сообщение об ошибке, кроется в следующей строке:

В этом случае sum является объектом класса Dollars. А чтобы всё заработало, нам нужно перегрузить операторы += и /= для класса Dollars:

Наконец, наш код скомпилируется, и результат:

13 dollars

Хотя проделанная работа может показаться очень большой, но это только из-за того, что наш класс Dollars с самого начала был «кожа да кости». Ключевой момент здесь в том, что нам не нужно модифицировать average(), чтобы он работал с объектами класса Dollars (или c любым другим типом данных). Вся работа была проделана только с классом Dollars, а обо всем остальном компилятор позаботился самостоятельно!

Оценить статью:

Звёзд: 1Звёзд: 2Звёзд: 3Звёзд: 4Звёзд: 5 (186 оценок, среднее: 4,90 из 5)
Загрузка...

Комментариев: 4

  1. kmish:

    Следующий момент интересен — нам не пришлось перегружать конструктор копирования:

    но если бы в нашем шаблоне мы ислользовали оператор= повторно, то нам пришлось бы либо надеятся, что проканает "Оператор присваивания по умолчанию", либо добавлять свой оператор присваивания. (урок 144)

    1. Илья:

      В классе Dollars нет динамического выделения памяти, поэтому здесь будет более чем достаточно оператора поверхностного копирования, предоставляемого по-умолчаию….

    2. Kris:

      В данном случае ни один из нормальных компиляторов не будет вызывать конструктор копий, здесь будет напрямую вызван конструктор (если не путаю, это что-то типа эллизии).

    3. Surprizze:

      Вызываться будет обычный конструктор( не копирования) , потому что мы присваиваем значение , а не копируем)

Добавить комментарий

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