Что происходит с шаблонной перегрузкой, если подстановка выведенных типов делает её параметр некорректным?
Если ошибка возникает в непосредственном контексте подстановки, шаблонная перегрузка исключается из набора кандидатов по правилу SFINAE — это не считается ошибкой компиляции. Затем компилятор продолжает выбор среди оставшихся перегрузок. Если ошибка возникает позже, например внутри тела уже выбранного шаблона, SFINAE не применяется и компиляция завершается ошибкой.
Обобщённый код должен был позволять выбирать реализацию в зависимости от свойств типа, не заставляя компилятор считать отсутствие неподходящего выражения фатальной ошибкой. SFINAE сформировал механизм, при котором неудачная подстановка отбраковывает конкретного кандидата во время разрешения перегрузки.
В современных версиях C++ ту же задачу часто решают с помощью concepts и ограничений. Они обычно дают более ясный синтаксис и диагностику, но понимание SFINAE по-прежнему важно для поддержки старого кода, стандартных библиотечных техник и совместимости с C++17 и более ранними стандартами.
Рассмотрим универсальную функцию, которая должна выбираться только для объектов с методом size(). Для целого числа такого метода нет. Если это отсутствие трактовать как обычную ошибку при компиляции шаблона, универсальная функция помешает выбрать запасную перегрузку.
Риск особенно велик в библиотеках: шаблон может быть проверен не в месте объявления, а только при вызове с конкретным типом. Неправильное размещение проверки превращает ожидаемое исключение кандидата в трудную для чтения ошибку инстанцирования.
При вызове шаблонной функции компилятор сначала выводит параметры шаблона, затем подставляет их в объявление кандидата. Если в этом процессе выражение в непосредственном контексте оказывается недействительным, кандидат удаляется из рассмотрения.
К непосредственному контексту относятся, в частности, тип возвращаемого значения, параметры функции и спецификации decltype, если именно там выполняется подстановка. Ошибки внутри тела функции к непосредственному контексту не относятся: тело инстанцируется после выбора перегрузки, поэтому такая ошибка является обычной диагностируемой ошибкой.
Минимальный пример:
В decltype(value.size(), void()) используется оператор запятая: сначала проверяется наличие size(), а итоговый тип выражения — void. Для int подстановка value.size() невозможна в непосредственном контексте, поэтому шаблонная перегрузка молча исключается.
SFINAE действует только на этапе подстановки и не означает, что любая ошибка в шаблоне безопасно игнорируется. Кроме того, сложные конструкции на SFINAE ухудшают читаемость и диагностику. В C++20 предпочтительнее выражать такие требования через concepts, если ограничения проекта позволяют использовать этот стандарт.
В библиотеке сериализации на C++17 требовалось вызвать пользовательский метод serialize, если он существует, и применить общий запасной формат в остальных случаях. Прямой вызов внутри единственной шаблонной функции приводил к ошибке для типов без такого метода.
Рассматривались три варианта:
if constexpr: хорошо подходит для ветвления внутри одной функции, но требует аккуратно сформулированного условия обнаружения метода.Для поддерживаемого C++17-кода выбрали SFINAE в отдельной вспомогательной перегрузке, а для новой ветки библиотеки — concepts. Это сохранило совместимость без попытки компилировать недопустимый вызов и одновременно упростило дальнейшую поддержку современного интерфейса.
Нет. SFINAE применяется только к ошибке подстановки в непосредственном контексте. Если шаблон уже выбран, а ошибка возникает в его теле, например из-за обращения к отсутствующему методу внутри исполняемой логики, это обычно жёсткая ошибка компиляции.
Нет. Он удаляет только конкретный шаблонный кандидат из конкретного набора кандидатов при конкретном вызове. Тот же шаблон может успешно участвовать в другом вызове с подходящими типами. Если после исключения не осталось допустимых перегрузок, компилятор выдаст ошибку разрешения вызова.
SFINAE кодирует условие косвенно: через недействительный тип или выражение, из-за чего кандидат исчезает. Constraints выражают требование явно и участвуют в упорядочивании перегрузок как ограничения; поэтому компилятор обычно может показать более точную причину несоответствия. SFINAE остаётся полезным для старых стандартов и некоторых низкоуровневых техник, но в новом C++ concepts обычно предпочтительнее.