Шаблоны и шаблонные функции в C++. Введение

25 комментариев

Шаблонные функции

Давайте рассмотрим простой пример. Допустим, у нас есть функция, которая меняет местами значения двух переменных типа int:

#include <iostream>

void my_swap ( int & first , int & second )
{
    int temp ( first ) ;
    first = second ;
    second = temp ;
}

int main ()
{
    int a = 5 ; 
    int b = 10 ;
    std::cout << a << " " << b << std::endl ;
    my_swap ( a , b ) ;
    std::cout << a << " " << b << std::endl ;
}

Теперь, допустим, у нас в функции main так же есть две переменные типа double, значения которых тоже нужно обменять. Функция для обмена значений двух переменных типа int нам не подойдет. Напишем функцию для double:

void my_swap ( double & first , double & second )
{
    double temp ( first ) ;
    first = second ;
    second = temp ;
}

И теперь перепишем main:

int main ()
{
    int a = 5 ; 
    int b = 10 ;
    std::cout << a << " " << b << std::endl ;
    my_swap ( a , b ) ;
    std::cout << a << " " << b << std::endl ;
    double c = 77.89 ;
    double d = 54.22 ;
    std::cout << c << " " << d << std::endl ;
    my_swap ( c , d ) ;
    std::cout << c << " " << d << std::endl ;
}

Как видите, у нас алгоритм абсолютно одинаковый, отличаются лишь типы параметров и тип переменной temp. А теперь представьте, что нам еще нужны функции для short, long double, char, string и еще множества других типов. Конечно, можно просто скопировать первую функцию, и исправить типы на нужные, тогда получим новую функцию с необходимыми типами. А если функция будет не такая простая? А вдруг потом еще обнаружится, что в первой функции была ошибка? Избежать всего этого можно, например, «шаманством» с препроцессором, но это нам ни к чему, нам помогут шаблоны.

Для начала, заглянем в википедию и посмотрим, что же такое шаблоны:

Шабло́ны (англ. template) — средство языка C++, предназначенное для кодирования обобщённых алгоритмов, без привязки к некоторым параметрам (например, типам данных, размерам буферов, значениям по умолчанию).
https://ru.wikipedia.org/wiki/Шаблоны_C++

Итак, описание шаблона начинается с ключевого слова template за которым в угловых скобках («<» и «>») следует список параметров шаблона. Далее, собственно идет объявление шаблонной сущности (например функция или класс), т. е. имеет вид: template < template-parameter-list > declaration.

Теперь давайте напишем шаблонную функцию my_swap. Исходя из упомянутой выше структуры объявления шаблона следует, что наша функция будет выглядеть так: template < параметры_шаблона > описание_функции.

Напишем функцию:

template < typename T >
void my_swap ( T & first , T & second )
{
    T temp(first) ;
    first = second ;
    second = temp ;
}

typename в угловых скобках означает, что параметром шаблона будет тип данных. T — имя параметра шаблона. Вместо typename здесь можно использовать слово class: template < class T > В данном контексте ключевые слова typename и class эквивалентны (лично мне больше нравится typename, а кому-то class). Далее, в тексте шаблона везде, где мы используем тип T, вместо T будет проставляться необходимый нам тип.

void my_swap ( T & first , T & second ) //T - тип, указанный в параметре шаблона
{
    T temp(first) ; //временная переменная должна быть того же типа, что и параметры
    first = second ;
    second = temp ;
}

теперь давайте напишем функцию main:

int main ()
{
    int a = 5 ; 
    int b = 10 ;
    std::cout << a << " " << b << std::endl ;
    my_swap<int> ( a , b ) ;
    std::cout << a << " " << b << std::endl ;
    double c = 77.89 ;
    double d = 54.22 ;
    std::cout << c << " " << d << std::endl ;
    my_swap<double> ( c , d ) ;
    std::cout << c << " " << d << std::endl ;
}

Как видите, после имени функции в угловых скобках мы указываем тип, который нам необходим, он то и будет типом T. Шаблон — это лишь макет, по которому компилятор самостоятельно будет генерировать код. При виде такой конструкции: my_swap<тип> компилятор сам создаст функцию my_swap с необходимым типом. Это называется инстанцирование шаблона. То есть при виде my_swap<int> компилятор создаст функцию my_swap в которой T поменяет на int, а при виде my_swap<double> будет создана функция с типом double. Если где-то дальше компилятор опять встретит my_swap<int>, то он ничего генерировать не будет, т.к. код данной функции уже есть(шаблон с данным параметром уже инстанцирован).

Таким образом, если мы инстанцируем этот шаблон три раза с разными типами, то компилятор создаст три разные функции

Вывод типа шаблона исходя из параметров функции

На самом деле, мы можем вызвать функцию my_swap не указывая тип в угловых скобках. В ряде случаев компилятор может это сделать за вас.

рассмотрим вызов функции без указания типа:

int a = 5 ;
int b = 10 ;
my_swap ( a , b ) ;

Наша шаблонная функция принимает параметры типа T&, основываясь на шаблоне, компилятор видит, что Вы передаете в функцию аргументы типа int, поэтому может самостоятельно определить, что в данном месте имеется ввиду функция my_swap с типом int. Это deducing template arguments. Теперь давайте напишем пример посложнее. Например, программу сортировки массива(будем использовать сортировку «пузырьком»). Естественно, что алгоритм сортировки один и тот же, а вот типы элементов в массиве будут отличаться. Для обменивания значений будем использовать нашу шаблонную функцию my_swap. Приступим:

#include <iostream>

template < typename T >
void my_swap ( T & first , T & second ) //T - тип, указанный в параметре шаблона
{
    T temp(first) ; //временная переменная должна быть того же типа, что и параметры
    first = second ;
    second = temp ;
}

//Функция будет принимать указатель на данные 
//и кол-во элементов массива данных
//Сам алгоритм сортировки можете посмотреть в Интернете.
//Никаких оптимизаций и проверок аргументов применять не будем, нам нужна просто демонстрация.    
template < class ElementType > //Использовал class, но можно и typename - без разницы
void bubbleSort(ElementType * arr, size_t arrSize)
{
    for(size_t i = 0; i < arrSize - 1; ++i) 
        for(size_t j = 0; j < arrSize - 1; ++j)
            if (arr[j + 1] < arr[j]) 
                my_swap ( arr[j] , arr[j+1] ) ;
}

template < typename ElementType >
void out_array ( const ElementType * arr , size_t arrSize )
{
    for ( size_t i = 0 ; i < arrSize ; ++i )
       std::cout << arr[i] << ' ' ;
    std::cout << std::endl ;
}


int main ()
{
    const size_t n = 5 ;
    int arr1 [ n ] = { 10 , 5 , 7 , 3 , 4 } ;
    double arr2 [ n ] = { 7.62 , 5.56 , 38.0 , 56.0 , 9.0 } ;
    std::cout << "Source arrays:\n" ;
    out_array ( arr1 , n ) ;//Компилятор сам выведет параметр шаблона исходя из первого аргумента функции
    out_array ( arr2 , n ) ;

    bubbleSort ( arr1 , n ) ;
    bubbleSort ( arr2 , n ) ;

    std::cout << "Sorted arrays:\n" ;
    out_array ( arr1 , n ) ;
    out_array ( arr2 , n ) ;
}

Вывод программы:

Source arrays: 10 5 7 3 4 7.62 5.56 38 56 9 Sorted arrays: 3 4 5 7 10 5.56 7.62 9 38 56

Как видите, компилятор сам генерирует out_array для необходимого типа. Так же он сам генерирует функцию bubbleSort. А в bubbleSort у нас применяется шаблонная функция my_swap, компилятор сгенерирует и её код автоматически. Удобно, не правда ли?

Введение в шаблонные классы

Шаблонными могут быть не только функции. Рассмотрим шаблонные классы. Начнем с простого примера. Мы добавим в наш предыдущий код функцию, которая будет искать максимум и минимум в массиве. При создании функции «упираемся» в проблему — как вернуть два указателя? Можно передать их в функцию в качестве параметров, а можно вернуть объект, который будет содержать в себе два указателя. Первый вариант при большом кол-ве возвращаемых значений приведет к заваливанию функции параметрами, поэтому я предлагаю сделать структуру:

struct my_pointer_pair
{
   тип * first ;
   тип * second ;
} ;

А какого же типа будут указатели? Можно сделать их void*, но тогда придется постоянно кастовать их к нужному типу, и код станет похож на «Доширак». А что, если сделать эту структуру шаблонной? Попробуем:

template < typename T, typename U >
struct my_pointer_pair
{
   T * first ;
   U * second ;
}  ;

Теперь компилятор при виде кода my_pointer_pair<тип1,тип2> сам сгенерирует нам код структуры с соответствующими типами. В данном примере указатели у нас будут одинакового типа, но структуру мы сделаем такой, чтобы типы указателей могли быть разными. Это может быть полезно в других примерах (в данном случае я просто хотел показать, что у шаблона может быть не только один параметр).

int main ()
{
    my_pointer_pair<int,double> obj = { new int(10) , new double(67.98) } ;//Создаем объект типа my_pointer_pair<int,double>
    std::cout << *obj.first << ' ' << *obj.second << std::endl ;
    delete obj.first ;
    delete obj.second ;
}

Компилятор не будет автоматически определять типы для шаблона класса, поэтому необходимо их указывать самостоятельно.

Теперь давайте напишем код шаблонной функции для поиска максимума и минимума:

//Шаблон наш будет с одним параметром - тип элементов массива (T)
//Возвращаемое значение - объект типа my_pointer_pair< T , T >
//т.е. first и second в my_pointer_pair будут иметь тип T*.
template < typename T >
my_pointer_pair< T , T > my_minmax_elements (  T * arr , size_t arrSize )
{
    my_pointer_pair< T , T > result = { 0 , 0 } ;
    if ( arr == 0 || arrSize < 1 )
         return result ;
    result.first = arr ;
    result.second = arr ;
    for ( size_t i = 1 ; i < arrSize ; ++i )
    {
        if ( arr[i] < *result.first )
            result.first = arr+i ;
        if ( arr[i] > *result.second )
           result.second = arr+i ;
    }
    return result ;
}

Теперь мы можем вызывать данную функцию:

my_pointer_pair< int , int > mm = my_minmax_elements ( arr1 , n ) ;

Для классов мы должны явно указывать параметры шаблона. В стандарте C++11, устаревшее ключевое слово auto поменяло свое значение и теперь служит для автоматического вывода типа в зависимости от типа инициализатора, поэтому мы можем написать так:

auto mm = my_minmax_elements ( arr1 , n ) ;

Предлагаю написать еще одну функцию, которая будет выводить объект my_pointer_pair в стандартный поток вывода:

template < typename T1 , typename T2 >
void out_pair ( const my_pointer_pair< T1 , T2 > & mp )
{
    if ( mp.first == 0 || mp.second == 0 )
        std::cout << "not found" << std::endl ;
    else
        std::cout << "min = " << *mp.first << " max = " << *mp.second << std::endl ;
}

Теперь соберем всё воедино:

#include <iostream>


template < typename ElementType >
void out_array ( const ElementType * arr , size_t arrSize )
{
    for ( size_t i = 0 ; i < arrSize ; ++i )
       std::cout << arr[i] << ' ' ;
    std::cout << std::endl ;
}


template < typename T, typename U >
struct my_pointer_pair
{
   T * first ;
   U * second ;
}  ;


//Шаблон наш будет с одним параметром - тип элементов массива (T)
//Возвращаемое значение - объект типа my_pointer_pair< T , T >
//т.е. first и second в my_pointer_pair будут иметь тип T*.
template < typename T >
my_pointer_pair< T , T > my_minmax_elements (  T * arr , size_t arrSize )
{
    my_pointer_pair< T , T > result = { 0 , 0 } ;
    if ( arr == 0 || arrSize < 1 )
         return result ;
    result.first = arr ;
    result.second = arr ;
    for ( size_t i = 1 ; i < arrSize ; ++i )
    {
        if ( arr[i] < *result.first )
            result.first = arr+i ;
        if ( arr[i] > *result.second )
           result.second = arr+i ;
    }
    return result ;
}

template < typename T >
void out_pair ( const my_pointer_pair< T , T > & mp )
{
    if ( mp.first == 0 || mp.second == 0 )
        std::cout << "not found" << std::endl ;
    else
        std::cout << "min = " << *mp.first << " max = " << *mp.second << std::endl ;
}

int main ()
{
    const size_t n = 5 ;
    int arr1 [ n ] = { 10 , 5 , 7 , 3 , 4 } ;
    double arr2 [ n ] = { 7.62 , 5.56 , 38.0 , 56.0 , 9.0 } ;
    std::cout << "Arrays:\n" ;
    out_array ( arr1 , n ) ;//Компилятор сам выведет параметр шаблона исходя из первого аргумента функии
    out_array ( arr2 , n ) ;

    out_pair ( my_minmax_elements ( arr1 , n ) ) ;
    out_pair ( my_minmax_elements ( arr2 , n ) ) ;
}

Вывод программы:

Arrays: 10 5 7 3 4 7.62 5.56 38 56 9 min = 3 max = 10 min = 5.56 max = 56

Шаблоны и STL

В комплекте с компилятором Вам предоставляется стандартная библиотека шаблонов (Standart Template Library). Она содержит множество шаблонных функций и классов. Например, класс двусвязного списка(list), класс «пара» (pair), функция обмена двух переменных(swap), функции сортировок, динамически расширяемый массив(vector) и т.д. Всё это — шаблоны и Вы можете их использовать. Для небольшого примера возьмем std::vector:

#include <vector>
#include <algorithm>
#include <iostream>

int main ()
{
    std::vector <int> arr;
    arr.push_back ( 5 ) ; //Добавляем элемент в конец
    arr.push_back ( 7 ) ;
    arr.push_back ( 3 ) ;
    arr.push_back ( 8 ) ;
    std::cout << "Source vector:\n" ;
    for ( size_t i = 0 , size = arr.size() ; i < size ; ++i )
        std::cout << arr[i] << ' ' ;
    std::cout << std::endl ;

    std::sort ( arr.begin() , arr.end() ) ; //Сортируем вектор

    std::cout << "Sorted vector:\n" ;
    for ( size_t i = 0 , size = arr.size() ; i < size ; ++i )
        std::cout << arr[i] << ' ' ;
   std::cout << std::endl ;
}

Заметьте, когда писали std::vector, авторы понятия не имели, элементы какого типа Вы будете хранить.

Шаблоны это слишком большой и мощный инструмент и описать всё в одной статье не представляется возможным. Это было лишь небольшое введение в мир шаблонов. Углубляясь в шаблоны, Вы поразитесь тому, какой мощный это инструмент и какие возможности он предоставляет.

P.S. высказывайте мнение о статье, критику, дополнения/исправления и интересующие вопросы в комментариях.

P.P.S. Просьба вопросы «консоль закрывается, что делать?», «русский язык не показывает. Что делать?», «как работает сортировка?», «что такое size_t», «что такое std::» и им подобные задавать либо в гугл, либо искать на данном сайте в других статьях. Не нужно захламлять комментарии этой чепухой. Если Вы этого не знаете, то может лучше сначала подтянуть свои знания?

Продолжение: Шаблоны в C++ — часть 2.

Комментарии к статье: 25

Подождите, загружаются комментарии...

Возможность комментировать эту статью отключена автором. Возможно, во всем виновата её провокационная тематика или большое обилие флейма от предыдущих комментаторов.

Если у вас есть вопросы по содержанию статьи, рекомендуем вам обратиться за помощью на наш форум.