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

Последнее обновление: 08.03.2023

Шаблоны

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

Шаблоны функций (function template) позволяют определять функции, которые не зависят от конкретных типов.

Вначале рассмотрим пример, где это может пригодиться. Например, нам надо определить функцию для сложения двух чисел int, double и std::string. Первое, что приходит на ум, сделать перегрузку функции - для каждого типа определить свою версию:

#include <iostream>

int add(int, int);
double add(double, double);
std::string add(std::string, std::string);

int main()
{
    std::cout << "int: " << add(4, 5) << std::endl; 
    std::cout << "double: " << add(4.4, 5.5) << std::endl;
    std::cout << "string: " << add(std::string("hel"), std::string("lo")) << std::endl;
}

int add(int x, int y)
{
    return x + y;
}
double add(double x, double y)
{
    return x + y;
}

std::string add(std::string str1, std::string str2)
{
    return str1 + str2;
}

Данный пример отлично работает, производит вычисления, как и должен. Однако в данном случае мы сталкиваемся с тем, что функция add фактически повторяется. Ее версии фактически выполняют одно и то же действие, единственно что отличается тип параметров и возвращаемого значения.

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

#include <iostream>

template<typename T> T add(T, T);	// прототип функции

int main()
{
    std::cout << "int: " << add(4, 5) << std::endl; 
    std::cout << "double: " << add(4.4, 5.5) << std::endl;
    std::cout << "string: " << add(std::string("hel"), std::string("lo")) << std::endl;
}
template<typename T> T add(T a, T b)
{
    return a + b;
}

Определение шаблона функции начинается с ключевого слова template, после которого в угловых скобках идет слово typename и затем список параметров шаблона:

template<typename T> T add(T a, T b)

В данном случае после typename указан один параметр - T. Параметр шаблона представляет произвольный идентификатор, в качестве которого, как правило, применяются заглавные буквы, например, T. Но это необязательно. То есть в данном случае параметр T будет представлять некоторый тип, который становится известным во время компиляции. Это может быть и тип int, и double, и string, и любой другой тип. Но поскольку внутри функции мы применяем операцию сложения, важно, чтобы тип, который будет применяться вместо параметра T, поддерживал операцию сложения, которая возвращала бы объект этого же типа. Если вдруг используемый тип не будет применять операцию сложения, то на этапе компиляции мы столкнемся с ошибкой.

И при вызове функции add в нее можно передавать объекты и типа int, и типа double, и любого другого типа. При вызове функции компилятор на основании типа аргументов выведет конкретный тип, связанный с параметром шаблона T, и создаст экземпляр функции add, который работает с конкретным типом, и при вызове функции будет вызваться данный экземпляр функции. Если для последующего вызова функции требуется тот же экземпляр, то компилятор использует существующий экземпляр функции.

При этом также можно использовать ссылки, указатели, массивы, которые представляют тип параметра шаблона. Например, в программе выше передадим параметры по ссылке и сделаем их константными:

#include <iostream> 

template<typename T> T add(const T&, const T&); 

int main()
{
    std::cout << "int: " << add(4, 5) << std::endl; 
    std::cout << "double: " << add(4.4, 5.5) << std::endl;
    std::cout << "string: " << add(std::string("hel"), std::string("lo")) << std::endl;
}
template<typename T> T add(const T& a, const T& b)
{
    return a + b;
}

Другой пример - функция обмена значениями:

#include <iostream> 

template<typename T> void swap(T&, T&); 

int main()
{
    int c {30};
    int d {10};
    swap(c, d);
    std::cout << "c = " << c << "\t d = " << d << std::endl;    // с = 10   d = 30
}

template <typename T> void swap(T& a, T& b)
{
    T temp = a;
    a = b;
    b = temp;
}

Функция swap принимает два параметра любого типа и меняет их значения.

Использование указателей на примере функции вычисления наибольшего значения:

#include <iostream> 

template<typename T> T* max(T*, T*); 

int main()
{
    int a{4}, b{5};
    std::cout << "int: " << *max(&a, &b) << std::endl; // int: 5

    double c{3.4}, d{2.3};
    std::cout << "double: " << *max(&c, &d) << std::endl;   // double: 3.4
}
template<typename T> T* max(T* a, T* b)
{
    return *a > *b? a : b;
}

Явный вызов реализации функции для определенного типа

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

#include <iostream> 

template<typename T> T add(const T&, const T&); 

int main()
{
    double d { add<double>(3.3, 2.2)};
    std::cout << "d: " << d << std::endl;   //d: 5.5
    d  = add<double>(3, 2);
    std::cout << "d: " << d << std::endl;   //d: 5
}
template<typename T> T add(const T& a, const T& b)
{
    return a + b;
}

Здесь мы явным образом указываем, что мы хотим использовать тип double:

double d { add<double>(3.3, 2.2)};

При этом во втором случае в функцию передаются целочисленные литералы, однако мы все равно используем реализацию функции для типа double:

d  = add<double>(3, 2);

Перегрузка функций и параметризация

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

#include <iostream>

template<typename T> const T* max(const T*, const T*); 
template <typename T> const T* max(const T[], unsigned);

int main()
{
    int a{4}, b{5};
    std::cout << *max(&a, &b) << std::endl;  // 5

    double numbers[]{3.4, 2.3, 6.1, 4.3};
    std::cout << *max(numbers, std::size(numbers)) << std::endl;   //6.1
}
template<typename T> const T* max(const T* a, const T* b)
{
    return *a > *b? a : b;
}

template <typename T>
const T* max(const T data[], unsigned size)
{
    const T* result {}; // если вектор пуст, то возвращается nullptr
    for(unsigned i{}; i < size; i++)
    {
        // если result не равен nullptr и *result меньше value
        if (!result || data[i] > *result) 
            result = &data[i];
    }
    return result;
}

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

Использование нескольких параметров

Можно использовать несколько параметров. Например, нам надо определить функцию перевода некоторой суммы с одного счета на другой и передать код операции. Для идентификаторов счета и кода операции мы можем использовать разные типы - числа, строки и т.д. Для этого определим следующую функцию:

#include <iostream>
 
template <typename T, typename K>
void transfer(T, T, K, int);
 
int main()
{
    transfer("id1234", "id5678", 2804, 5000);
}

template <typename T, typename K>
void transfer(T fromAccount, T toAccount, K code, int sum)
{
    std::cout << "From: " << fromAccount << "\nTo: " << toAccount
        << "\nSum: " << sum << "\nCode: " << code << std::endl;
}

В данном случае при вызове transfer("id1234", "id5678", 2804, 5000); вместо параметра T будет подставляться символьный массив, а вместо параметра K - тип int.

Выведение типов и decltype(auto)

Иногда может быть неизвестно, значение какого именного типа будет возвращаться, или мы захотим предоставить компилятору выводить точный тип возвращаемых данных. В этом случае вместо возвращаемого типа можно использовать заменитель decltype(auto). Например, определим функцию вычисления среднего арифметического с выведением результата:

#include <iostream> 

template <typename T> decltype(auto) average(const T (&data)[], unsigned size)
{
    T result {};
    for(unsigned i{}; i < size; i++)
    {
        result += data[i];
    }
    return result / size;
}

int main()
{
    int numbers[]{1, 3, 4, 5, 6};
    std::cout << average(numbers, std::size(numbers)) << std::endl;   // 3
}

Нетипизированные параметры

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

#include <iostream> 

template <typename T, unsigned N=1> void print(const T&);

int main()
{
    print<int, 4>(3);
}
// печатаем value N раз
template <typename T, unsigned N> void print(const T& value)
{
    for(unsigned i{}; i < N; i++)
    {
        std::cout << value << std::endl;
    }
}

Здесь определен шаблон функции print, которая печатает значение value, которое представляет тип T, N раз. При этом параметр N имеет четко установленный тип - unsigned int. При вызове функции параметру N можно передать значение. Например, напечатаем 4 раза число 3:

print<int, 4>(3);

Большого смысла такие параметры не имеют, поскольку мы равным образом можем определить стандартные параметры в функции. Однако в ряде сценариев они могут оказаться полезными. Например, вычислим длину любого массива:

#include <iostream>

template <typename T, size_t N> size_t size(const T (&data)[N]) { return N; }

int main()
{
    const int numbers1[]{1, 2, 3, 4, 5};
    const double numbers2[]{1.2, 2.3, 3.4};
    const char* people[] {"Sam", "Tom", "Bob", "Mike"};
    
    std::cout << "numbers1 size: " << size(numbers1) << std::endl;
    std::cout << "numbers2 size: " << size(numbers2) << std::endl;
    std::cout << "people size: " << size(people) << std::endl;
}

Другой пример - в одном из листингов выше для вычисления среднего значения чисел массива в функцию передавался размер массива. Но с помощью нетипизированных параметров мы можем избавиться от необходимости передавать в функцию размер массива:

#include <iostream> 

template <typename T, size_t N> T average(const T (&)[N]);

int main()
{
    int numbers1[]{1, 2, 3, 4, 5};
    std::cout << average(numbers1) << std::endl;   // 3

    double numbers2[]{1.1, 3.2, 4.3, 5.4, 6.5, 2.6};
    std::cout << average(numbers2) << std::endl;   // 3.85
}

template <typename T, size_t N> T average(const T (&data)[N])
{
    T result {};
    for(unsigned i{}; i < N; i++)
    {
        result += data[i];
    }
    return result / N;
}

Здесь для представления размера массива определен параметр N типа size_t. При вызове функции на основе переданного в функцию массива этот параметр получает конкретное значение - размер массива.

Выведение типов и результата

Стоит отметить, что начиная со стандарта C++20 можно определять параметры, типы которых автоматически выводятся исходя из переданных аргументов. Аналогично можно и выводить тип результата. Для этого применяется ключевое слово auto. Равным образом для определения типов параметров и результатов функции можно использовать выражения auto*, auto& и const auto&:

#include <iostream>>

auto add(const auto& a, const auto& b)
{
    return a + b;
}

int main()
{
    const int n1{3};
    const int n2{4};
    std::cout << add(n1, n2) << std::endl;  // 7

    const double d1{3.3};
    const double d2{4.4};
    std::cout << add(d1, d2) << std::endl;  // 7.7
}
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850