Шаблоны — одна из самых мощных функций C++. Шаблоны предоставляют нам код, который не зависит от типа данных.
Другими словами, используя шаблоны, мы можем написать общий код, который работает с любым типом данных. Нам просто нужно передать тип данных в качестве параметра. Этот параметр, который передает тип данных, также называется именем типа.
В этой статье мы поговорим о шаблонах и их различных аспектах.
Что такое шаблоны?
Как говорилось выше, шаблоны являются общими, т.е. не зависят от типа данных. Шаблоны используются в основном для обеспечения повторного использования кода и гибкости программ. Мы можем просто создать простую функцию или класс, который принимает тип данных в качестве параметра, и реализовать код, который работает для любого типа данных.
Например, если мы хотим, чтобы алгоритм сортировки работал для всех числовых типов данных, а также для символьных строк, мы просто напишем функцию, которая принимает тип данных в качестве аргумента, и реализуем технику сортировки.
Затем, в зависимости от типа данных (имени типа), который передается алгоритму сортировки, мы можем отсортировать данные независимо от типа данных. И это значит, нам не нужно писать десять алгоритмов для десяти типов данных.
Таким образом, шаблоны можно использовать в приложениях, где требуется, чтобы код можно было использовать для более чем одного типа данных. Шаблоны также используются в приложениях, в которых возможность повторного использования кода имеет первостепенное значение.
Использование шаблонов и реализация
Шаблоны могут быть реализованы двумя способами:
- Как шаблон функции
- Как шаблон класса
Шаблон функции
Шаблон функции похож на обычную функцию, но единственная разница в том, что обычная функция может работать только с одним типом данных, а код шаблона функции может работать с несколькими типами данных.
Хотя на самом деле мы можем просто перегрузить обычную функцию для работы с различными типами данных, но шаблоны функций всегда более полезны, поскольку нам нужно написать единственную программу, которая сможет работать со всеми типами данных.
Далее вы увидите реализацию шаблонов функций.
Общий синтаксис шаблона функции:
1 2 3 4 5 6 7 |
template<class T> T function_name(T args){ …… //function body } |
Здесь T — аргумент шаблона, который принимает различные типы данных, а class — ключевое слово. Вместо ключевого слова class мы также можем написать «typename».
Когда определенный тип данных передается в function_name, копия этой функции создается компилятором с этим типом данных в качестве аргумента, и функция выполняется.
Давайте посмотрим на пример, чтобы лучше понять шаблоны функций:
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 |
#include <iostream> using namespace std; template <typename T> void func_swap(T &arg1, T &arg2) { T temp; temp = arg1; arg1 = arg2; arg2 = temp; } int main() { int num1 = 10, num2 = 20; double d1 = 100.53, d2 = 435.54; char ch1 = 'A', ch2 = 'Z'; cout << "Original data\n"; cout << "num1 = " << num1 << "\tnum2 = " << num2<<endl; cout << "d1 = " << d1 << "\td2 = " << d2<<endl; cout << "ch1 = " << ch1 << "\t\tch2 = " << ch2<<endl; func_swap(num1, num2); func_swap(d1, d2); func_swap(ch1, ch2); cout << "\n\nData after swapping\n"; cout << "num1 = " << num1 << "\tnum2 = " << num2<<endl; cout << "d1 = " << d1 << "\td2 = " << d2<<endl; cout << "ch1 = " << ch1 << "\t\tch2 = " << ch2<<endl; return 0; } |
Вывод данных:
1 2 3 4 5 6 7 8 9 |
Исходные данные num1 = 10 num2 = 20 d1 = 100.53 d2 = 435.54 ch1 = A ch2 = Z Данные после замены num1 = 20 num2 = 10 d1 = 435.54 d2 = 100.53 ch1 = Z ch2 = A |
В приведенной выше программе мы определили шаблон функции «func_swap», который меняет местами два значения. Функция принимает два ссылочных аргумента типа T. Затем она меняет значения местами. Поскольку аргументы являются ссылками, любые изменения, которые мы делаем с аргументами в функции, будут отражены в вызывающей функции.
В основной функции мы определяем данные типа int, double и char. Мы вызываем функцию func_swap с каждым типом данных. Затем мы отображаем замененные данные для каждого типа данных.
Таким образом, это показывает, что нам не нужно писать три функции для трех типов данных. Достаточно написать только одну функцию и сделать ее шаблонной, чтобы она не зависела от типа данных.
Шаблоны классов
Как и в шаблонах функций, нам может понадобиться класс, аналогичный всем остальным аспектам, но только с другими типами данных.
В этой ситуации у нас могут быть разные классы для разных типов данных или разные реализации для разных типов данных в одном классе. Но это сделает наш код громоздким.
Лучшим решением для этого является использование класса шаблона. Класс шаблона также ведет себя подобно шаблонам функций. Нам нужно передать тип данных в качестве параметра классу при создании объектов или вызове функций-членов.
Общий синтаксис шаблона класса:
1 2 3 4 5 6 7 8 9 |
template <class T> class className{ ….. public: T memVar; T memFunction(T args); }; |
В приведенном выше определении T действует как заполнитель для типа данных. В memVar и memFunction открытых членах также используется T в качестве заполнителя для типов данных.
Как только класс шаблона определен, как указано выше, мы можем создавать объекты класса следующим образом:
1 2 3 |
className<int> classObejct1; className<float> classObject2; className<char> classObject3; |
Вот пример кода для демонстрации шаблонов классов:
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 |
#include <iostream> using namespace std; template <class T> class myclass { T a, b; public: myclass (T first, T second) {a=first; b=second;} T getMaxval (); }; template <class T> T myclass<T>::getMaxval () { return (a>b? a : b); } int main () { myclass <int> myobject (100, 75); cout<<"Maximum of 100 and 75 = "<<myobject.getMaxval()<<endl; myclass<char> mychobject('A','a'); cout<<"Maximum of 'A' and 'a' = "<<mychobject.getMaxval()<<endl; return 0; } |
Вывод данных:
1 2 |
Maximum of 100 and 75 = 100 Maximum of ‘A’ and ‘a’ = a |
Вышеприведенная программа реализует пример шаблона класса. У нас есть класс шаблона myclass. Внутри у нас есть конструктор, который инициализирует два члена a и b класса. Существует еще одна функция-член getMaxval, которая также является шаблоном функции и возвращает максимум a и b.
В основной функции мы создаем два объекта: myobject целочисленного типа и mychoobject символьного типа. Затем мы вызываем функцию getMaxval для каждого из этих объектов, чтобы определить максимальное значение.
Обратите внимание, что помимо параметров типа шаблона (параметров типа T), функции шаблона также могут иметь обычные параметры, такие как обычные функции, а также значения параметров по умолчанию.
Имя типа или ключевое слово класса
При объявлении класса или функции шаблона мы используем одно из двух ключевых слов class или typename. Эти два слова семантически эквивалентны и могут использоваться взаимозаменяемо.
Но в некоторых случаях мы не можем использовать эти слова как равнозначные. Например, когда мы используем зависимые типы данных в таких шаблонах, как «typedef», мы используем имя типа вместо класса.
Кроме того, ключевое слово class должно использоваться, когда нам нужно явно создать экземпляр шаблона.
Создание шаблона и специализация
Шаблоны написаны общим способом, что означает, что это общая реализация независимо от типа данных. В соответствии с предоставленным типом данных нам нужно создать конкретный класс для каждого типа данных.
Например, если у нас есть шаблонный алгоритм сортировки, мы можем сгенерировать конкретный класс для sort<int>, другой класс для sort<float> и т. д. Это называется созданием экземпляра шаблона.
Мы подставляем аргументы шаблона (фактические типы данных) вместо параметров шаблона в определении класса шаблона.
Например,
1 2 |
template <class T> class sort {}; |
Когда мы передаем тип данных <int>, компилятор заменяет ‘T’ типом данных <int>, так что алгоритм сортировки становится sort<int>.
Каждый раз, когда мы используем класс шаблона или функцию, возникает необходимость в экземпляре, когда мы передаем определенный тип данных. Если этого экземпляра еще нет, компилятор создает его с определенным типом данных. Это неявное воплощение.
Одним из недостатков неявного создания экземпляров является то, что компилятор создает класс экземпляра только для тех аргументов, которые используются в данный момент. Это означает, что если мы хотим сгенерировать библиотеку экземпляров перед использованием этих экземпляров, нам нужно перейти к явному созданию экземпляров.
Пример объявления шаблона приведен ниже:
1 |
template class Array(T) |
Может быть создан как:
1 |
template class Array<char> |
При создании экземпляра класса также создаются экземпляры всех его членов.
Специализация шаблона
При программировании с использованием шаблонов мы можем столкнуться с ситуацией, когда нам может потребоваться специальная реализация для определенного типа данных. Когда возникает такая ситуация, мы идем на специализацию шаблона.
В специализации шаблона мы реализуем специальное поведение для определенного типа данных помимо исходного определения шаблона для других типов данных.
Например, предположим, что у нас есть класс шаблона « myIncrement», в котором есть конструктор для инициализации значения и функция шаблона toIncrement, которая увеличивает значение на 1.
Этот конкретный класс будет отлично работать для всех типов данных, кроме char. Вместо того, чтобы увеличивать значение для char, мы можем придать ему особое поведение и преобразовать символ в верхний регистр.
Для этого мы можем пойти на специализацию шаблона для типа данных char.
Эта реализация показана в приведенном ниже примере кода:
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 |
#include <iostream> using namespace std; // шаблон класса: template <class T> class myIncrement { T value; public: myIncrement (T arg) {value=arg;} T toIncrement () {return ++value;} }; // специализация шаблона класса: template <> class myIncrement <char> { char value; public: myIncrement (char arg) {value=arg;} char uppercase () { if ((value>='a')&&(value<='z')) value+='A'-'a'; return value; } }; int main () { myIncrement<int> myint (7); myIncrement<char> mychar ('s'); myIncrement<double> mydouble(11.0); cout<<"Incremented int value: "<< myint.toIncrement()<< endl; cout<<"Uppercase value: "<<mychar.uppercase()<< endl; cout<<"Incremented double value: "<<mydouble.toIncrement()<< endl; return 0; } |
Вывод данных:
1 2 3 |
Увеличенное значение int: 8 Значение в верхнем регистре: S Увеличенное двойное значение: 12 |
В приведенной выше программе, которая демонстрирует специализацию шаблона, вы можете видеть, как мы объявили специализированный шаблон для типа char. Сначала мы объявляем исходный класс, а затем «специализируем» его для типа char. Чтобы начать специализацию, мы используем пустое объявление шаблона «template<>».
Затем после имени класса мы включаем тип данных <char>. После этих двух изменений класс записывается для типа char.
Обратите внимание, что в функции main нет никакой разницы между созданием экземпляра типа char и другими типами. Единственное отличие состоит в том, что мы переопределяем специализированный класс.
Также, обратите внимание, что мы должны определить все члены специализированного класса, даже если они точно такие же в универсальном/исходном классе шаблона. Это связано с тем, что у нас нет функции наследования элементов из общего шаблона в специализированный шаблон.
Вариативные шаблоны C++
До сих пор мы разбирали шаблоны функций, которые принимают фиксированное количество аргументов. Но существуют также шаблоны, которые принимают переменное количество аргументов. Эти шаблоны функций называются шаблонами с переменным числом переменных. Шаблоны Variadic, т.е. вариативные — одна из новейших возможностей C++.
Шаблоны Variadic принимают переменное количество аргументов, которые являются типобезопасными, и аргументы разрешаются во время компиляции.
Давайте посмотрим полный пример программирования для понимания вышеизложенного:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
#include <iostream> #include <string> using namespace std; template<typename T> T summation(T val) { return val; } template<typename T, typename... Args> T summation(T first, Args... args) { return first + summation(args...); } int main() { long sum = summation(1, 2, 3, 8, 7); cout<<"Sum of long numbers = "<<sum<<endl; string s1 = "H", s2 = "e", s3 = "ll", s4 = "o"; string s_concat = summation(s1, s2, s3, s4); cout<<"Sum of strings = "<<s_concat; } |
Вывод данных:
1 2 |
Sum of long numbers = 21 Sum of strings = Hello |
В приведенном выше примере демонстрируется вариационная функция «суммирование». Как показано выше, сначала нам нужна базовая функция, реализующая базовый случай. Затем мы реализуем вариационную функцию поверх этой функции.
В суммировании функций переменных «typename…args» называется пакетом параметров шаблона, тогда как «Args…args» называется пакетом параметров функции.
После написания шаблона функции, реализующего базовый случай, мы пишем функцию с переменным числом аргументов, реализующую общий случай. Вариативная функция записывается аналогично рекурсии, как показано для суммирования (args…). Первый аргумент отделяется от пакета параметров функции в тип T (первый).
При каждом вызове суммирования список параметров сужается на один аргумент, и в конечном итоге базовое условие достигается. Вывод показывает суммирование длинных целых чисел и символов.
Итог
На этом мы заканчиваем разбор шаблонов в C++. Шаблоны помогают нам сделать наши программы универсальными, т.е. независимыми от типа.
Общие программы всегда стоят поверх других программ, поэтому нам не нужно писать отдельные программы для каждого типа данных. Таким образом, разработка универсальных программ с безопасным типом может стать важным шагом на пути к эффективному программированию.
В следующей статье мы объясним на примерах символы C++ и функции преобразования, такие как: isdigit, islower, isupper, isalpha и т. д.
С Уважением, МониторБанк