SFINAE

SFINAE - substitution failed is not an error. Для того, чтобы сказать, что это такое, необходимо вспомнить, как разрешаются перегрузки функций.

void f(int, std::vector<int>);
void f(int, int);
void f(double, double);
void f(int, int, char, std::string, std::vector<int>);
void f(std::string);

f(1, 2);
  1. Для сопоставления f(1, 2) с конкретной функцией компилятор отправит все функции с названием f в overload resolution.
  2. Далее из списка исчезают кандидаты, у которых количество параметров не может совпасть с теми, что представлены в вызове.
  3. Потом отсекаются функции, типы параметров которых отличаются от переданных аргументов и для которых нет неявного преобразования.
  4. После этого идут несложные, но многословные правила поиска лучшей перегрузки, и побеждает f(int, int), так как она не требует преобразований аргументов.

Если бы обе подходили одинаково хорошо, то вызов был бы двусмысленным, о чём компилятор сообщил бы. Так, в общих чертах, и работает перегрузка методов в C++.

Добавим шаблонные функции!

template<typename T>
void function(T, T);

Теперь несколько изменится первая стадия:

Если компилятор встречает шаблонную функцию, имя которой совпадает с именем вызова, тогда он пытается вывести аргументы шаблона, на основании аргументов переданных в вызов (argument deduction).

И если все аргументы удаётся вывести, то шаблонная функция с выведенными аргументами добавляется в список кандидатов функций.

В нашем примере в конце все равно останется только нешаблонная f(int, int), так как при прочих равных нешаблонная функция всегда сильнее шаблонной.

А что происходит, если вывести аргументы шаблона не удалось? Тогда такая шаблонная функция просто не попадает в список overload resolution. Это и есть правило SFINAE. Но следует понимать, что рассматривается исключительно сигнатура функции и ничего больше.

Поэтому если подстановка аргументов даёт корректную функцию с точки зрения её сигнатуры, и функция побеждает в перегрузке, а потом оказывается, что в теле функции есть какие-то проблемы, с которыми компилятор справится не может, то компиляция будет завершена ошибкой - это называется hard error.

Примитивное SFINAE

Используя SFINAE мы можем получить рефлексию на этапе компиляции, узнавая свойства объектов, и в зависимости от этого разрешать или запрещать им использовать какие-то функции и прочее.

Следующая шаблонная шапка позволяет использовать функцию после нее только с типами, в которых объявлен тип с алиасом iterator. Если такого алиаса в типе объявлено не будет, то произойдет ошибка вывода шаблонных аргументов и данная функция не попадет в список overload resolution.

template <typename T, typename = typename T::iterator>

Подобным образом в шаблонных параметрах можно объявлять и другие ограничения на используемые шаблонные типы.

О сигнатуре шаблонной функции

Многим известно, что возвращаемый тип в функции не является частью её сигнатуры, но это не так для шаблонных функций. Это позволяет использовать, например, std::enable_if на возвращаемом типе.

Схематично, это выглядит так:

template <bool condition, class T = void>
struct enable_if;

template<class T>
struct enable_if<true, T> {
    typedef T type;
};

То есть, при передаче в шаблон true в структуре появляется поле type по умолчанию типа void, но если шаблон был вызван с параметром false, то этого поля не будет, а его использование в сигнатуре функции или в шапке шаблона приведет к неудаче вывода, и эта перегрузка не будет включена в список кандидатов.

Напишем надуманный пример, использующий enable_if в возвращаемом типе.

namespace {
    struct tag;
}

template <typename T>
enable_if<std::is_trivially_destructible_v<T>>::type f(T);

template <typename T>
enable_if<!std::is_trivially_destructible_v<T>, tag>::type f(T);

template <typename T>
constexpr bool is_void_f = std::is_same_v<decltype(f(std::declval<T>())), void>;

int main() {
    std::cout << is_void_f<int> << std::endl;
    std::cout << is_void_f<std::string> << std::endl;
} // output: 1 0

Применение SFINAE в бою

В заметке про if constexpr написан пример, позволяющий по члену класса has_foo судить о наличии соответствующего метода в классе. Попробуем написать метафункцию, определяющую существование в классе, например, метода void foo(int) с помощью новых знаний.

template<typename T>
struct has_foo {
    static constexpr bool value = true; // сейчас придумаем, что здесь написать
};

Осталось придумать перегрузку, определяющие нужные нам свойства типа, и как получить из нее булевскую константу.

Во-первых создадим всеядную функцию-подложку (иногда говорят, fallback), которую компилятор выберет, если не подойдет полезная перегрузка, присваивающая каким-то образом в value значение true.

template<typename T>
struct has_foo {
    struct dummy;
    static dummy detect(...); // fallback
    static constexpr bool value = true;  // ладно-ладно, уже скоро придумаем!
};

Теперь придумаем детектор. Здесь нам придется воспользоваться конструкциями decltype и std::declval, которые могут вызываться на этапе компиляции и проверять свойства без реальных вызовов конструкторов, функций и прочего.

template<typename T>
struct has_foo {
    struct dummy;
    static dummy detect(...); // fallback
    
    template<typename U>
    static decltype(std::declval<U>().foo(42)) detect(const U&); // detector
    
    static constexpr bool value = true;  // теперь точно скоро!
};

Теперь осталось воспользоваться детектором, чтобы поставить правильное значение поля value. Также упростим синтаксис для конечного пользователя.

template<typename T>
struct has_foo {
private:  // скроем детали реализации
    struct dummy;
    static dummy detect(...);
    
    template<typename U>
    static decltype(std::declval<U>().foo(42)) detect(const U&);
    
public:
    static constexpr bool value =
        std::is_same<void, decltype(detect(std::declval<T>()))>::value;
};

template <typename T>
constexpr bool has_foo_v = has_foo<T>::value;

Использование:

struct check1 {
    void foo(int);
};

struct check2 {};

int main() {
    std::cout << has_foo_v<check1> << std::endl; // 1
    std::cout << has_foo_v<check2> << std::endl; // 0
    // или
    std::cout << has_foo<check1>::value << std::endl;
    std::cout << has_foo<check2>::value << std::endl;
}

Заимствования:

SFINAE. Как много в этом слове (scrutator.me)

SFINAE — это просто / Хабр (habr.com)

SFINAE — Википедия (wikipedia.org)