Перейти к содержимому

Шаблоны функций и тип дедукции

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

Зачем нужны шаблоны функций?

Без шаблонов написание функции, которая работает для нескольких типов, означает дублирование логики. int max(int, int), double max(double, double), std::string max(std::string, std::string) — идентичная логика, повторяемая для каждого типа. Функциональные шаблоны позволяют записать алгоритм один раз и позволяют компилятору автоматически генерировать версии для конкретных типов.

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

Шаблонный синтаксис

Вводится шаблон функций с ключевым словом template, за которым следует список параметров в угловых скобках. Вы можете использовать typename или class взаимозаменяемо для параметров типа — они означают точно то же самое. Современный конвент предпочитает typename.

function_template.cpp
#include <iostream>
#include <string>

// 'typename' and 'class' are interchangeable here
template <typename T>
T maximum(T a, T b) {
    return (a > b) ? a : b;
}

// Equivalent — 'class' means the same thing
template <class T>
T minimum(T a, T b) {
    return (a < b) ? a : b;
}

int main() {
    std::cout << maximum(3, 7)          << '\n'; // deduces T = int
    std::cout << maximum(3.14, 2.71)    << '\n'; // deduces T = double
    std::cout << maximum<std::string>("hello", "world") << '\n';
    // explicit instantiation needed — "hello" is const char*, not std::string
}

Правила вычета аргументов шаблона

Компилятор выводит аргументы шаблона, сопоставляя типы аргументов функции с параметрическими шаблонами. Ключевыми правилами являются:

1 Точный матч предпочтительнее maximum(3, 7) выводит T = int Поскольку оба аргумента являются int.
2. Никаких неявных преобразований во время вычета — maximum(3, 7.0) Ошибка: компилятор видит T = int Из первого аргумента и T = double от второго и отказывается от выбора.
3. Распад массива-указателя — когда T появляется как параметр значения, массивы распадаются на указатели. Использовать T(&)[N] Чтобы захватить тип и размер массива.
4. Конст/летучий элемент верхнего уровня удаляется от значений параметров, но сохраняется для эталонных/точечных параметров.
5. Ссылка рушится — если T выводится в качестве эталонного типа (при пересылке ссылок), применяются правила обрушения ссылок: T& & становится T&, T& && становится T&, T&& & становится T&, T&& && становится T&&.

Когда вычитание не удается или является двусмысленным, вы должны предоставить шаблонные аргументы * * явно * *: maximum(3, 7.0).

Нетиповые параметры шаблона

Параметры шаблонов не ограничиваются типами. Нетиповые параметры шаблона (NTTP) принимают значения времени компиляции, такие как целые числа, указатели или (с C++20) плавающие точки и типы классов. Они являются мощными для встраивания констант времени компиляции в систему типов.

non_type_params.cpp
#include <iostream>
#include <array>
#include <cstddef>

// Non-type parameter: N is a compile-time size_t value
template <typename T, std::size_t N>
T sum(const std::array<T, N>& arr) {
    T total{};
    for (const auto& elem : arr) {
        total += elem;
    }
    return total;
}

// C++20: auto non-type parameter accepts any structural type
template <auto Value>
constexpr auto doubled = Value * 2;

int main() {
    std::array<int, 4> nums = {1, 2, 3, 4};
    std::cout << sum(nums) << '\n';     // 10, deduces T=int, N=4

    std::cout << doubled<21> << '\n';   // 42
    std::cout << doubled<3.14> << '\n'; // 6.28 (C++20)
}
Примечание

Каждая уникальная комбинация аргументов шаблона заставляет компилятор генерировать отдельную инстанциацию — отличную копию функции в вашем двоичном файле. maximum, maximum и maximum выполняют три функции. Обычно это хорошо, но в больших шаблонах кодовых баз может способствовать раздуванию кода (более крупные двоичные файлы, более длительное время компиляции). Стратегии смягчения включают в себя: использование деклараций extern template для контроля того, где происходят инстанциации, факторинг независимого от типа кода из шаблонов и использование type erasure, когда полиморфизм во время выполнения приемлем.

Подводный камень

Поскольку компилятор должен видеть полное определение шаблона для его реализации, шаблоны не могут быть разделены на файлы .h / .cpp так, как могут обычные функции. Если вы поместите определение шаблона в файл .cpp и попытаетесь использовать его из другого блока перевода, вы получите ошибку linker (неопределенная ссылка). Стандартное решение заключается в том, чтобы поместить все определение шаблона в файл заголовка. Если время компиляции становится проблемой, вы можете использовать явную инстанциацию **: поместите определение в файл .cpp и добавьте template int maximum(int, int);, чтобы явно инстанцировать необходимые вам специализации.

Явное обоснование

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

explicit_instantiation.h
// ---- maximum.h ----
template <typename T>
T maximum(T a, T b) {
    return (a > b) ? a : b;
}

// Suppress implicit instantiation in every TU that includes this header
extern template int maximum<int>(int, int);
extern template double maximum<double>(double, double);

// ---- maximum.cpp ----
#include "maximum.h"

// Explicit instantiation: generate code here, once
template int maximum<int>(int, int);
template double maximum<double>(double, double);
Ключевые выводы
  • Шаблоны функций — это чертежи — компилятор генерирует конкретные функции для каждого набора аргументов шаблона.
  • typename и class взаимозаменяемы в списках параметров шаблонов
  • Дедукция аргумента шаблона выполняет не неявные преобразования - двусмысленные выводы являются ошибками
  • Нетиповые шаблонные параметры встраивают значения времени компиляции в систему типов
  • Шаблоны должны быть определены в заголовках (или использовать явное включение), потому что компилятор нуждается в полном определении для включения.

Тест — проверьте свои знания

(15 XP)

1. Что происходит, когда вы звоните `maximum(3, 7.0)` с шаблоном `template<typename T> T maximum(T a, T b)`?

2. Почему шаблонные определения обычно помещаются в файлы заголовков?

3. Что такое `extern template int maximum<int>(int, int);`?