зачем нужны шаблоны классов
Основы шаблонов С++: шаблоны функций
Дисклаймер: статья была начата еще в феврале, но, по зависящим от меня причинам, закончена не была. Тема очень обширна, поэтому публикуется в урезанном виде. Что не поместилось, будет рассмотрено позже.
Невозможно разбираться в современном С++, не зная, что такое шаблоны программирования. Данное свойство языка открывает широкие возможности оптимизации и повторного использования кода. В данной статье попробуем разобраться, что это такое и как это всё работает.
Механизм шаблонов в языке С++ позволяет решать проблему унификации алгоритма для различных типов: нет необходимости писать различные функции для целочисленных, действительных или пользовательских типов – достаточно составить обобщенный алгоритм, не зависящий от типа данных, основывающийся только на общих свойствах. Например, алгоритм сортировки может работать как с целыми числами, так и с объектами типа «автомобиль».
Существуют шаблоны функций и шаблоны классов.
Рассмотрим более подробно шаблоны функций.
Шаблоны функций
Как написать первую шаблонную функцию?
Рассмотрим случай определения минимального элемента из двух. В случае целых и действительных чисел придется написать 2 функции.
Можно, конечно, реализовать только одну функцию, с действительными параметрами, но для понимания шаблонов это будет вредным.
Что произойдёт в случае компиляции приложения? Обе реализации функции попадут в бинарный код приложения, даже если они не используются (впрочем, сейчас компиляторы очень умные, умеют вырезать неиспользуемый код). А если необходимо добавить функцию, определяющую минимальную из 2 строк (сложно представить без уточнения, что есть минимальная строка)?!
В этом случае, если алгоритм является общим для типов, с которыми приходится работать, можно определить шаблон функции. Принцип, в общем случае, будет следующим:
Самым интересным является тот факт, что пока нет вызова функции min, при компиляции она в бинарном коде не создается (не инстанцируется). А если объявить группу вызовов функции с переменными различных типов, то для каждого компилятор создаст свою реализацию на основе шаблона.
Вызов шаблонной функции, в общем, эквивалентен вызову обыкновенной функции. В этом случае компилятор определит, какой тип использовать вместо Type, на основании типа фактических параметров. Но если подставляемые параметры окажутся разных типов, то компилятор не сможет вывести (инстанцировать шаблон) реализацию шаблона. Так, в ниже следующем коде компилятор споткнётся на третьем вызове, так как не может определить, чему равен Type (подумайте, почему?):
Решается эта проблема указанием конкретного типа при вызове функции.
Урок №175. Шаблоны классов
Обновл. 15 Сен 2021 |
На предыдущих уроках мы узнали, как с помощью шаблонов функций сделать одну версию функции, которая будет работать с разными типами данных. Хотя это значительный шаг на пути к обобщенному программированию, это не решает всех наших проблем. Рассмотрим пример такой проблемы и то, как шаблоны могут нам помочь в её решении.
Шаблоны и контейнерные классы
На уроке о контейнерных классах мы узнали то, как, используя композицию, реализовать классы, содержащие несколько объектов определенного типа данных. В качестве примера мы использовали класс ArrayInt:
Хотя этот класс обеспечивает простой способ создания массива целочисленных значений, но что, если нам нужно будет работать со значениями типа double? Используя традиционные методы программирования мы создали бы новый класс ArrayDouble для работы со значениями типа double:
Хотя кода много, но классы почти идентичны, меняется только тип данных! Как вы уже могли бы догадаться, это идеальный случай для использования шаблонов. Создание шаблона класса аналогично созданию шаблона функции. Например, создадим шаблон класса 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++.
Шаблоны классов в Стандартной библиотеке С++
Шаблоны классов и Заголовочные файлы
Шаблон не является ни классом, ни функцией — это трафарет, используемый для создания классов или функций. Таким образом, шаблоны работают не так, как обычные функции или классы. В большинстве случаев это не является проблемой, но на практике случаются разные ситуации.
Вышеприведенная программа скомпилируется, но вызовет следующую ошибку линкера:
unresolved external symbol «public: int __thiscall Array::getLength(void)» (?GetLength@?$Array@H@@QAEHXZ)
Почему так? Сейчас разберемся.
Эту проблему можно решить несколькими способами.
Самый простой вариант — поместить код из Array.cpp в Array.h ниже класса. Таким образом, когда мы будем подключать Array.h, весь код шаблона класса (полное объявление и определение как класса, так и его методов) будет находиться в одном месте. Плюс этого способа — простота. Минус — если шаблон класса используется во многих местах, то мы получим много локальных копий шаблона класса, что увеличит время компиляции и линкинга файлов (линкер должен будет удалить дублирование определений класса и методов, дабы исполняемый файл не был «слишком раздутым»). Рекомендуется использовать это решение до тех пор, пока время компиляции или линкинга не является проблемой.
Если вы считаете, что размещение кода из Array.cpp в Array.h сделает Array.h слишком большим/беспорядочным, то альтернативой будет переименование Array.cpp в Array.inl (.inl от англ. «inline» = «встроенный»), а затем подключение Array.inl из нижней части файла Array.h. Это даст тот же результат, что и размещение всего кода в заголовочном файле, но таким образом код получится немного чище.
Еще один альтернативный вариант — использовать подход трех файлов:
Определение шаблона класса хранится в заголовочном файле.
Затем добавляем третий файл, который содержит все необходимые нам экземпляры шаблона класса.
Шаблоны классов
В этом разделе описываются правила, относящиеся к шаблонам классов C++.
Функции элементов в шаблонах классов
Функции-члены могут быть определены как внутри шаблона класса, так и за его пределами. В последнем случае они определяются как шаблоны функций.
Обратите внимание, что как и в функциях-членах класса шаблона, определение функции-члена для конструктора класса подразумевает, что список аргументов шаблона приводится дважды.
Функции-члены сами могут быть шаблонами функций, в которых указываются дополнительные параметры, как показано в следующем примере.
Шаблоны вложенных классов
Шаблоны можно определить в классах или шаблонах классов (в этом случае они называются шаблонами членов). Шаблоны членов, которые являются классами, называются шаблонами вложенных классов. Шаблоны элементов, которые являются функциями, обсуждаются в шаблонах функций элементов.
Шаблоны вложенных классов объявляются как шаблоны классов внутри области внешнего класса. Их можно определить во включающем классе или вне его.
В следующем примере кода демонстрируется шаблон вложенного класса внутри обычного класса.
Локальные классы не могут иметь шаблоны элементов.
Друзья в шаблоне
Шаблоны классов могут иметь друзей. Дружественными объектами класса-шаблона могут быть классы или шаблоны классов, функции или шаблоны функций. Ими также могут быть специализации (кроме частичных) шаблонов классов или шаблонов функций.
В следующем примере дружественная функция определена как шаблон функции в шаблоне класса. Этот код создает по одной версии дружественной функции для каждого экземпляра шаблона. Такую конструкцию можно использовать в тех ситуациях, когда дружественная функция зависит от тех же параметров шаблона, что и класс.
В следующем примере создается дружественная функция, которая имеет специализацию шаблона. Если исходный шаблон функции является дружественным объектом, то и его специализация автоматически становится дружественной.
Кроме того, как отмечается в комментарии перед объявлением дружественной функции в следующем коде, вы можете взять только специализированную версию шаблона и объявить ее в качестве дружественной. В этом случае определение дружественной специализации шаблона необходимо поместить за пределами класса шаблона.
В следующем примере показан дружественный шаблон класса, объявленный в пределах шаблона класса. Затем этот шаблон класса используется в качестве аргумента шаблона для дружественного класса. Дружественные шаблоны классов должны определяться за пределами шаблона класса, в котором они объявлены. Все специализации или и частичные специализации дружественного шаблона также являются дружественными объектами исходного шаблона класса.
Повторное использование параметров шаблона
Параметры шаблона могут повторно использования в списке параметров шаблона. Например, приведенный ниже код допустим:
Введение в магию шаблонов
Зачем?

Мы используем шаблоны для красоты. Каждый С++ разработчик знает, что такое красота, красота — это когда код компактный, понятный и быстрый.
Мета-магия и неявные интерфейсы
Что такое метопрограмма? Метопрограмма — это программа, результатом работы которой будет другая программа. Для С++ выполнением метапрограмм занимается компилятор, а результатом является бинарный файл.
Именно для написания метапрограмм используются шаблоны.
Чем еще отличается полиморфизм шаблонов от полиморфизма виртуальных функций? Если класс обладает явным интрерфейсом, который мы определили в объявлении класса, то далее в программе объекты этого типа могут использоваться в соответствии с этим самым интерфесом. А вот для шаблонов мы используем неявные интерфейсы, т.е. использованием объекта типа мы определяем неявный интерфейс типа, который выведет компилятор при построении метапрограммы.
Первые заклинания: волшебная дубина
Конкретизируем наш шаблон и посмотрим, какие типы мы получили для различных параметров шаблона:
В выводе программы видно, что типы конкретизаций шаблона разные даже для эквивалентных типов — unsigned char & char. При этом они идентичны для char & CHAR, т.к. typedef не создает тип, а лишь дает ему другое имя. Идентичны они и для выражений 1 и 2-1, т.к. компилятор вычисляет выражения и вместо 2-1 использует 1.
Отсюда и вытекает, что мы не можем использовать для шаблонов раздельную компиляцию без дополнительных проблем:
Вообще, в стандарте С++ для этого есть ключевое слово export, однако эта фича слишком труднореализуема и отсутствует в большинстве компиляторов. Есть компиляторы, которые ее поддерживают, но не советую ее использовать в переносимом коде.
Кроме классов существуют и шаблоны функций:
Если компилятор может вывести тип параметра шаблона из типа параметров — он так и поступит, при этом нам не нужно указывать его в коде. Если нет, то мы можем определить разрешающую функцию:
Она не несет никаких накладных расходов.
Специализация — это новый уровень
Обычно используя шаблоны мы хотим написать универсальный код, однако в некоторых случаях мы можем проиграть в производительности. Для решения проблемы существует специальное заклятие — специализация шаблона. Специализация — это повторное определение шаблона с конкретным типом либо классом типов:
Компилятор сам выберет наиболее точно подходящую специализацию, в примере это класс типов “указатель на тип”.
Зловещая магия: рекурсия
Специализации и тот факт, что мы можем использовать шаблоны в шаблонах, дает дам одну очень интересную возможность — рекурсия времени компиляции.
Самый простой и популярный пример — вычисление какого-либо ряда или полинома, скажем, сумма ряда натуральных чисел:
Смотрим… Работает! Круто? Увеличим количество итераций до 500:
Теперь компиляция занимает больше времени, при этом время выполнения программы — константа! Чудеса!
Не делай козу если хотел грозу
Тут есть пара моментов.
Максимальная глубина рекурсии по умолчанию ограничена реализацией, для нового gcc это 900, для старых версий он меньше. Параметр
снимает это ограничение.
Второй подводный камень — не ждите отчетов об ошибках. Меняем сумму на факториал:
Получаем некорректный результат, и ни одного предупреждения.
Третий момент, очевидный: мы можем создать слишком много почти одинаковых конкретизаций шаблона и вместо прироста производительности получить прирост бинарного кода.
Мощные заклинания древних
А можно ли совместить магию наследования с шаблонной магией?
Древние используют для этого заклинание CRTP. Идея проста: применить не виртуальное наследование и обеспечить полиморфное поведение с помощью явного приведения типа наследника к типу родителя. Давайте рассмотрим пример использования:
Мы получаем наследуемые inline методы с полиморфным поведением! Кто скажет что это не круто — мой враг навсегда.
Древние также советуют добавлять в конструктор родителя что-то типа того:
Чтобы демоны, разбуженные мощным заклинанием, не смогли причинить вред вызвавшему их магу.
Есть еще много тайных техник, древних и не очень. Надеюсь на не скорую встречу /*в аду*/, и да прибудет с вами мощь древних.
BestProg
Содержание
Поиск на других ресурсах:
1. Что называется шаблоном класса? Что такое шаблон класса?
Часто, при разработке классов для разных типов данных, программисту приходится писать программный код для каждого типа в отдельности. Методы и операции над данными разных типов могут содержать один и тот же повторяемый код. Во избежание повторяемости написания кода для разных типов данных, в языке C++ используются так называемые шаблоны (templates).
Фактически, объявление шаблона класса есть только описанием. Создание реального класса с заданным типом данных осуществляется компилятором в момент компиляции, когда объявляется объект класса.
2. Какая общая форма объявления шаблона класса и объекта шаблонного класса, которые не содержат аргументов? Ключевое слово template
В простейшем случае общая форма объявления шаблона класса без аргументов имеет следующий вид:
Общая форма объявления объекта шаблонного класса имеет следующий вид:
3. Какие преимущества дает использование шаблонов классов?
Объявление шаблона класса дает следующие преимущества:
4. Пример объявления шаблона класса, который содержит методы обработки числа, тип которого может быть целым или вещественным
Пусть нужно объявить шаблон класса, который будет обрабатывать некоторое число. Число может быть любого типа, который позволяет выполнять над ним арифметические операции.
В примере объявляется шаблон класса, содержащий методы, которые выполняют следующие операции над некоторым числом:
Объявление шаблона имеет вид
Использование шаблона класса MyNumber в другом программном коде
5. Общая форма объявления шаблона класса, принимающего аргументы
Бывают случаи, когда в шаблоне класса нужно использовать некоторые аргументы. Эти аргументы могут использоваться методами, которые описываются в шаблоне класса.
Общая форма шаблона класса, содержащего аргументы, следующая:
Общая форма объявления объекта шаблонного класса, содержащего один аргумент:
6. Пример использования шаблона класса, принимающего два аргумента
Шаблон класса получает два целых числа:
Эти числа используются в методах для выполнения операций над массивом. Шаблон класса содержит следующие данные и методы:
Текст шаблона класса следующий:
Использование шаблона в некотором другом программном коде (функции, методе)



