8.15 – Шаблоны функций с несколькими шаблонными типами

Добавлено 27 июня 2021 в 15:26

В уроке «8.13 – Шаблоны функций» мы написали шаблон функции для вычисления максимального из двух значений:

#include <iostream>
 
template <typename T>
T max(T x, T y)
{
    return (x > y) ? x : y;
}
 
int main()
{
    std::cout << max(1, 2) << '\n';     // создаст экземпляр max(int, int)
    std::cout << max(1.5, 2.5) << '\n'; // создаст экземпляр max(double, double)
 
    return 0;
}

Теперь рассмотрим следующую аналогичную программу:

#include <iostream>
 
template <typename T>
T max(T x, T y)
{
    return (x > y) ? x : y;
}
 
int main()
{
    std::cout << max(2, 3.5) << '\n';  // ошибка компиляции
 
    return 0;
}

Вы можете удивиться, обнаружив, что эта программа не компилируется. Вместо этого компилятор выдаст кучу (возможно, сумасшедших) сообщений об ошибках. В Visual Studio автор получил следующее:

Project3.cpp(11,18): error C2672: 'max': no matching overloaded function found
Project3.cpp(11,28): error C2782: 'T max(T,T)': template parameter 'T' is ambiguous
Project3.cpp(4): message : see declaration of 'max'
Project3.cpp(11,28): message : could be 'double'
Project3.cpp(11,28): message : or       'int'
Project3.cpp(11,28): error C2784: 'T max(T,T)': could not deduce template argument for 'T' from 'double'
Project3.cpp(4): message : see declaration of 'max'

В нашем вызове функции max(2, 3.5) мы передаем аргументы двух разных типов: int и double. Поскольку мы вызываем функцию без использования угловых скобок для указания фактического типа, компилятор сначала проверяет, есть ли совпадение не с шаблоном для max(int, double). И не находит.

Затем компилятор проверит, сможет ли он найти совпадение с шаблоном функции (используя вывод аргументов шаблона, который мы рассмотрели в уроке «8.14 – Создание экземпляра шаблона функции»). Однако это также не удастся по простой причине: T может представлять только один тип. Для T не существует типа, который позволил бы компилятору создать экземпляр шаблона функции max<T>(T, T) для функции с двумя разными типами параметров. Другими словами, поскольку оба параметра в шаблоне функции относятся к типу T, они должны соответствовать одному и тому же фактическому типу.

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

Вы можете задаться вопросом, почему компилятор не сгенерировал функцию max<double>(double, double) и не использовал затем числовое преобразование для приведения типа аргумента int в double. Ответ прост: преобразование типов выполняется только при разрешении перегрузок функций, а не при выводе аргументов шаблона.

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

Придется найти другое решение. К счастью, мы можем решить эту проблему (по крайней мере) тремя способами.

Использование static_cast для преобразования аргументов в соответствующие типы

Первое решение – возложить бремя преобразования аргументов в соответствующие типы на вызывающего. Например:

#include <iostream>
 
template <typename T>
T max(T x, T y)
{
    return (x > y) ? x : y;
}
 
int main()
{
    // преобразовываем наш int в double, чтобы мы могли вызвать max(double, double)
    std::cout << max(static_cast<double>(2), 3.5) << '\n'; 
 
    return 0;
}

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

Однако это решение неудобно и трудночитаемо.

Предоставление фактического типа

Если бы мы написали функцию max(double, double), не являющуюся шаблоном, то мы могли бы вызвать max(int, double) и позволить правилам неявного преобразования типа преобразовать наш аргумент int в double, чтобы можно было разрешить вызов функции:

#include <iostream>
 
double max(double x, double y)
{
    return (x > y) ? x : y;
}
 
int main()
{
    std::cout << max(2, 3.5) << '\n'; // аргумент int будет преобразован в double
 
    return 0;
}

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

#include <iostream>
 
template <typename T>
T max(T x, T y)
{
    return (x > y) ? x : y;
}
 
int main()
{
    // мы предоставили фактический тип double, поэтому
    // компилятор не будет использовать вывод аргументов шаблона
    std::cout << max<double>(2, 3.5) << '\n'; 
 
    return 0;
}

В приведенном выше примере мы вызываем max<double>(2, 3.5). Поскольку мы явно указали, что T следует заменить на double, компилятор не будет использовать вывод аргументов шаблона. Вместо этого он просто создаст экземпляр функции max<double>(double, double), а затем выполнит преобразование типов для любых несовпадающих аргументов. Наш параметр int будет неявно преобразован в double.

Хотя это более читабельно, чем использование static_cast, было бы еще лучше, если бы нам вообще не приходилось думать о типах при вызове функции max.

Шаблоны функций с несколькими параметрами шаблонных типов

Корень нашей проблемы в том, что для нашего шаблона функции мы определили только один шаблонный тип (T), а затем указали, что оба параметра должны быть одного и того же типа.

Лучший способ решить эту проблему – переписать наш шаблон функции таким образом, чтобы наши параметры могли вычисляться в разные типы. Вместо того, чтобы использовать один параметр шаблонного типа T, теперь мы будем использовать два (T и U):

#include <iostream>
 
// Мы используем два параметра шаблонных типов с именами T и U
template <typename T, typename U> 
// x может определиться в тип T, а y может определиться в тип U
T max(T x, U y) 
{
    return (x > y) ? x : y; // упс, здесь у нас проблема сужающегося преобразования
}
 
int main()
{
    std::cout << max(2, 3.5) << '\n';
 
    return 0;
}

Поскольку мы определили x с шаблонным типом T, а y с шаблонным типом U, x и y теперь могут определять свои типы независимо. Когда мы вызываем max(2, 3.5), T может быть int, а U может быть double. Компилятор с радостью создаст для нас экземпляр max<int, double>(int, double).

Однако в приведенном выше коде всё еще есть проблема: используя обычные арифметические правила (8.4 – Преобразования при вычислении арифметических выражений), double имеет приоритет над int, поэтому наш условный оператор вернет double. Но наша функция определена как возвращающая T – в тех случаях, когда T определяется в int, наше возвращаемое значение double подвергнется сужающему преобразованию в int, что приведет к предупреждению (и возможной потере данных).

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

Как решить эту проблему? Это хорошее применение для автоматического типа возвращаемого значения – мы позволим компилятору определить из инструкции return, каким должен быть возвращаемый тип:

#include <iostream>
 
template <typename T, typename U>
auto max(T x, U y)
{
    return (x > y) ? x : y;
}
 
int main()
{
    std::cout << max(2, 3.5) << '\n';
 
    return 0;
}

Эта версия max теперь отлично работает с операндами разных типов.

Сокращенные шаблоны функций

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

Например:

auto max(auto x, auto y)
{
    return (x > y) ? x : y;
}

является сокращением в C++20 для следующего шаблона:

template <typename T, typename U>
auto max(T x, U y)
{
    return (x > y) ? x : y;
}

который совпадает с шаблоном функции max, который мы написали выше.

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

Лучшая практика


Не стесняйтесь использовать сокращенные шаблоны функций, если каждый параметр auto должен быть независимым шаблонным типом (а стандарт языка установлен у вас на C++20 или новее).

Теги

autoC++ / CppLearnCpptypenameДля начинающихОбучениеПрограммированиеШаблон / TemplateШаблон функции

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.


  • 2023-12-21Isa Abasov

    Ага, а вот и "auto". Приятно что в голову пришло что-то толковое.