При проектировании API нужно, чтобы тип выводился только из первого аргумента, а второй лишь проверялся после вывода. Какой тип T будет выбран и что напечатает программа?
#include <iostream>
#include <type_traits>
template<class T>
void check(T value, std::type_identity_t<T> limit) {
std::cout << (std::is_same_v<T, int> ? "int" : "other");
}
int main() {
check(7, 2.5);
}
Будет выбран T = int, программа напечатает int. Параметр std::type_identity_t<T> находится в невыводимом контексте, поэтому второй аргумент не участвует в выводе T; после вывода T его значение проверяется обычным неявным преобразованием к int.
Шаблонный вывод типов в C++ предназначен для определения параметров шаблона по форме и типам аргументов вызова. Однако библиотечным API иногда требуется, чтобы один аргумент определял тип, а остальные только принимали значение уже выбранного типа.
Для этого применяют невыводимые контексты. В C++20 стандартная утилита std::type_identity_t<T> предоставляет простой способ сообщить компилятору: тип формально зависит от T, но извлекать T из этого места при выводе не следует.
Если заменить второй параметр на обычный T, компилятор будет пытаться вывести T из обоих аргументов. Для вызова с 7 и 2.5 первый аргумент предложит int, а второй — double, поэтому согласованного вывода не получится.
При использовании std::type_identity_t<T> тип выводится только по первому аргументу. Это позволяет принять второй аргумент, если после подстановки он может быть неявно преобразован к полученному типу, но также создаёт риск незаметной потери данных при таком преобразовании.
Во время вывода шаблонных аргументов компилятор рассматривает первый параметр T и получает T = int из аргумента 7. Второй параметр записан как std::type_identity_t<T>.
std::type_identity_t<T> раскрывается в T, но специальное правило вывода считает аргумент шаблона внутри этой конструкции невыводимым. Поэтому 2.5 не предлагает альтернативу double и не конфликтует с уже выведенным int.
После завершения вывода выполняется подстановка T = int. Сигнатура фактически становится эквивалентной check(int, int), а 2.5 преобразуется к int. В результате функция получает значение 2, а программа печатает int.
Минимальная реализация механизма выглядит так:
До C++20 аналогичный эффект обычно получали через собственный шаблон идентичности, например template<class T> struct identity { using type = T; };, а затем использовали typename identity<T>::type. В современном коде предпочтительна стандартная std::type_identity_t.
Этот приём не запрещает преобразования и не выполняет проверку строгого совпадения типов. Если преобразования недопустимы, вызов завершится ошибкой; если они допустимы, возможны усечение дробной части, переполнение или другие обычные последствия преобразования.
Предположим, библиотечная функция принимает значение и порог, причём тип значения должен определять арифметический тип всей операции. Вызов check(7, 2.5) должен использовать целочисленную семантику, а не менять тип операции на double из-за порога.
Вариант с двумя обычными параметрами T проще читается, но вызов с разными типами обычно не проходит вывод и требует явного приведения. Вариант с std::common_type_t<T, U> поддерживает смешанные типы, однако меняет правила API: результатом становится общий тип, а не тип первого аргумента.
Выбранный вариант с std::type_identity_t<T> оправдан, если тип первого аргумента является источником истины. При этом в интерфейсе или документации следует явно указать, что второй аргумент может быть преобразован и что такое преобразование способно привести к потере точности.
Допустимо ли считать std::type_identity_t<T> полностью другим типом?
Нет. После подстановки это обычный T; особенность действует именно на этапе вывода шаблонных аргументов. После вывода применяются проверка параметров функции, перегрузка и неявные преобразования по обычным правилам.
Что произойдёт при вызове check(7, "x")?
T всё равно будет выведен как int по первому аргументу. Затем компилятор попытается передать строковый литерал в параметр типа int; допустимого неявного преобразования нет, поэтому вызов будет отвергнут. Невыводимый контекст не отключает проверку типов после подстановки.
Чем этот приём отличается от явного указания шаблонного аргумента?
Вызов check<int>(7, 2.5) также фиксирует T = int, но тип становится частью места вызова и обычно требует дублирования информации. std::type_identity_t<T> сохраняет автоматический вывод из выбранного аргумента и одновременно исключает остальные аргументы из процесса вывода.