SFINAE
Условно подключайте функции
«SFINAE» — бесплатный урок C++ Academy на CoddyKit. Это урок 3 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения C++ Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс C++ Academy содержит 4 уроков всего.
Что означает SFINAE
SFINAE — это сокращение от английской фразы «Substitution Failure Is Not An Error» («ошибка подстановки не является ошибкой»). Когда компилятор подставляет аргументы шаблона и получает некорректный результат, такой кандидат просто удаляется, а не вызывает критическую ошибку.
- Это позволяет создавать условные перегрузки.
- Это основа выбора на этапе компиляции.
#include <iostream>
#include <type_traits>
template <typename T>
typename std::enable_if<std::is_integral<T>::value, bool>::type
isOdd(T n) { return n % 2 != 0; }
int main() {
std::cout << std::boolalpha << isOdd(3) << '\n';
return 0;
}Подстановка срабатывает на практике
Если подстановка типа делает сигнатуру функции некорректной, компилятор молча пропускает эту функцию и продолжает проверять другие варианты.
#include <iostream>
template <typename T>
auto sizeOfValue(T t) -> decltype(t.size()) { return t.size(); }
int main() {
std::string s = "hello";
std::cout << sizeOfValue(s) << '\n';
return 0;
}Две перегрузки — побеждает одна
Предоставьте две перегрузки, из которых для заданного типа успешно подставляется ровно одна. SFINAE отбрасывает некорректную.
#include <iostream>
#include <type_traits>
template <typename T>
typename std::enable_if<std::is_integral<T>::value, const char*>::type
name() { return "integral"; }
template <typename T>
typename std::enable_if<std::is_floating_point<T>::value, const char*>::type
name() { return "floating"; }
int main() {
std::cout << name<int>() << ' ' << name<double>() << '\n';
return 0;
}decltype и SFINAE выражений
Использование decltype для выражения в возвращаемом типе позволяет функции существовать только тогда, когда это выражение допустимо для данного типа.
#include <iostream>
template <typename T>
auto tryDouble(T x) -> decltype(x + x) { return x + x; }
int main() {
std::cout << tryDouble(21) << '\n';
std::cout << tryDouble(2.5) << '\n';
return 0;
}Обнаружение члена
Классический приём SFINAE определяет, есть ли у типа конкретный член, проверяя, компилируется ли выражение с его использованием.
#include <iostream>
#include <type_traits>
#include <vector>
template <typename T>
auto hasSize(int) -> decltype(std::declval<T>().size(), std::true_type{});
template <typename T>
std::false_type hasSize(...);
int main() {
std::cout << std::boolalpha;
std::cout << decltype(hasSize<std::vector<int>>(0))::value << '\n';
std::cout << decltype(hasSize<int>(0))::value << '\n';
return 0;
}Запасной вариант ...
Перегрузка, принимающая ... (переменное число аргументов), имеет наименьший приоритет. Она служит универсальным вариантом, когда перегрузка с ограничением SFINAE оказывается недоступной.
#include <iostream>
template <typename T>
auto pick(int) -> decltype(T{}.value, void()) { std::cout << "has value\n"; }
template <typename T>
void pick(...) { std::cout << "no value\n"; }
struct WithValue { int value = 0; };
int main() {
pick<WithValue>(0);
pick<int>(0);
return 0;
}void_t
В C++17 std::void_t упрощает обнаружение: он отображает любые корректные типы в void, поэтому специализация срабатывает только тогда, когда проверяемое выражение допустимо.
#include <iostream>
#include <type_traits>
#include <vector>
template <typename T, typename = void>
struct HasSize : std::false_type {};
template <typename T>
struct HasSize<T, std::void_t<decltype(std::declval<T>().size())>> : std::true_type {};
int main() {
std::cout << std::boolalpha;
std::cout << HasSize<std::vector<int>>::value << '\n';
std::cout << HasSize<int>::value << '\n';
return 0;
}Почему просто не выдать ошибку
Без SFINAE некорректная подстановка вызвала бы критическую ошибку компиляции, которую нельзя было бы обработать. SFINAE превращает её в спокойный результат «не кандидат».
#include <iostream>
#include <type_traits>
template <typename T>
typename std::enable_if<std::is_pointer<T>::value, bool>::type
isNull(T p) { return p == nullptr; }
int main() {
int x = 0;
std::cout << std::boolalpha << isNull(&x) << '\n';
return 0;
}Альтернатива — диспетчеризация по тегам
SFINAE может быть сложной для чтения. Диспетчеризация по тегам выбирает реализацию с помощью небольших типов-тегов и разрешения перегрузок.
#include <iostream>
#include <type_traits>
template <typename T>
void impl(T v, std::true_type) { std::cout << "integral: " << v << '\n'; }
template <typename T>
void impl(T v, std::false_type) { std::cout << "other: " << v << '\n'; }
template <typename T>
void handle(T v) { impl(v, std::is_integral<T>{}); }
int main() {
handle(5);
handle(2.5);
return 0;
}SFINAE и constexpr if
В C++17 конструкция if constexpr часто заменяет SFINAE для ветвления внутри одной функции, поскольку неиспользуемые ветви отбрасываются на этапе компиляции.
#include <iostream>
#include <type_traits>
template <typename T>
void describe(T v) {
if constexpr (std::is_integral<T>::value) {
std::cout << "integral: " << v << '\n';
} else {
std::cout << "other: " << v << '\n';
}
}
int main() {
describe(7);
describe(3.14);
return 0;
}Объединение всего изученного
SFINAE особенно полезен, когда на уровне перегрузок нужно выбрать один из отдельных шаблонов функций на основе свойств типа.
#include <iostream>
#include <type_traits>
template <typename T>
typename std::enable_if<std::is_signed<T>::value, T>::type
absValue(T v) { return v < 0 ? -v : v; }
int main() {
std::cout << absValue(-9) << '\n';
return 0;
}Быстрая проверка
Проверьте, насколько хорошо Вы понимаете SFINAE.
Итоги
Вы узнали о SFINAE:
- некорректная подстановка удаляет кандидата, не вызывая ошибку
- в сочетании с
enable_if,decltypeиvoid_tон позволяет условно включать функции - перегрузка
...служит запасным вариантом - современные альтернативы включают диспетчеризацию по тегам и
if constexpr
Далее Вы подробно изучите классические шаблоны с enable_if.
Часто задаваемые вопросы
Урок «SFINAE» бесплатный?
Да — полный текст урока «SFINAE» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс C++ Academy, подпишись на CoddyKit PRO. Курс C++ Academy содержит 4 уроков всего.
Чему я научусь в уроке «SFINAE»?
Условно подключайте функции Ты практикуешь C++ Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать C++ Academy?
Предыдущий опыт не требуется. C++ Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 3 из 4.
Сколько времени занимает урок «SFINAE»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке C++ Academy?
Да. Каждый урок C++ Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.