0Pricing
C++ Academy · Урок

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 — локальная установка не требуется.

Все уроки этого курса

  1. Полная специализация
  2. Частичная специализация
  3. SFINAE
  4. Шаблоны enable_if
← Назад к C++ Academy