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);
- Для сопоставления
f(1, 2)
с конкретной функцией компилятор отправит все функции с названиемf
в overload resolution. - Далее из списка исчезают кандидаты, у которых количество параметров не может совпасть с теми, что представлены в вызове.
- Потом отсекаются функции, типы параметров которых отличаются от переданных аргументов и для которых нет неявного преобразования.
- После этого идут несложные, но многословные правила поиска лучшей перегрузки, и побеждает
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)