SFINAE
"Неудачная Замена Не Является Ошибкой - Substitution Failure Is Not An Error"
Это правило применяется во время разрешения перегрузки шаблонов функций: при сбое подстановки явно указанного или выведенного типа параметра шаблона специализация отбрасывается из набора перегрузки вместо того, чтобы вызывать ошибку компиляции.
Эта функция используется в метапрограммировании шаблонов.
Объяснение
Параметры шаблона функции подставляются (заменяются аргументами шаблона) дважды:
- явно указанные аргументы шаблона заменяются до вывода аргументов шаблона
- выведенные аргументы и аргументы, полученные из значений по умолчанию, заменяются после вывода аргументов шаблона
Замена происходит в
- всех типах, используемых в типе функции (включая тип возвращаемого значения и типы всех параметров)
- все типы, используемые в объявлениях параметров шаблона
|
(начиная с C++11) |
|
(начиная с C++20) |
Ошибка подстановки это любая ситуация, когда указанный выше тип или выражение были бы неправильно сформированы (с требуемой диагностикой), если бы они были записаны с использованием подставленных аргументов.
Только сбои в типах и выражениях в непосредственном контексте типа функции или её типов параметров шаблона или их спецификатор explicit (начиная с C++20) являются ошибками SFINAE. Если оценка подставленного типа/выражения вызывает побочный эффект, такой как создание экземпляра некоторой специализации шаблона, создание неявно определённой функции-элемента и т.д., ошибки в этих побочных эффектах рассматриваются как серьёзные ошибки. Лямбда-выражение не считается частью непосредственного контекста. (начиная с C++20)
| Этот раздел не завершён Причина: мини-пример, где это важно |
Подстановка выполняется в лексическом порядке и останавливается при возникновении ошибки.
|
Если имеется несколько объявлений с разным лексическим порядком (например, шаблон функции, объявленный с конечным типом возвращаемого значения, который должен быть заменён после параметра и повторно объявлен с обычным типом возвращаемого значения, который будет заменён перед параметром), и это может привести к созданию экземпляров шаблона в другом порядке или вообще без порядка, то программа некорректна; диагностика не требуется. |
(начиная с C++11) |
template<typename A>
struct B { using type = typename A::type; };
template<
class T,
class U = typename T::type, // Ошибка SFINAE, если B не имеет типа элемента
class V = typename B<T>::type> // серьёзная ошибка, если T не имеет типа элемента
// (гарантировано, что это не произойдет через CWG 1227
// потому что подстановка в аргумент шаблона
// по умолчанию U приведёт к сбою в первую очередь)
void foo (int);
template<class T>
typename T::type h(typename B<T>::type);
template<class T>
auto h(typename B<T>::type) -> typename T::type; // повторная объявление
template<class T>
void h(...) {}
using R = decltype(h<int>(0)); // неправильно сформировано, диагностика не требуется
Тип SFINAE
Ошибки следующего типа являются ошибками SFINAE:
|
(начиная с C++11) |
- попытка создать массив элементов void, массив ссылок, массив функций, массив отрицательного размера, массив нецелого размера или массив нулевого размера:
template<int I>
void div(char(*)[I % 2 == 0] = nullptr)
{
// эта перегрузка выбирается, когда I чётное
}
template<int I>
void div(char(*)[I % 2 == 1] = nullptr)
{
// эта перегрузка выбирается, когда I не чётное
}
- попытка использовать тип слева от оператора разрешения области видимости
::, и это не класс или перечисление:
template<class T>
int f(typename T::B*);
template<class T>
int f(T);
int i = f<int>(0); // использует вторую перегрузку
- попытка использовать элемент типа, где
- тип не содержит указанный элемент
- указанный элемент не является типом, где требуется тип
- указанный элемент не является шаблоном, где требуется шаблон
- указанный элемент не является нетипом, где требуется нетип
template<int I>
struct X {};
template<template<class T> class>
struct Z {};
template<class T>
void f(typename T::Y*) {}
template<class T>
void g(X<T::N>*) {}
template<class T>
void h(Z<T::template TT>*) {}
struct A {};
struct B { int Y; };
struct C { typedef int N; };
struct D { typedef int TT; };
struct B1 { typedef int Y; };
struct C1 { static const int N = 0; };
struct D1
{
template<typename T>
struct TT {};
};
int main()
{
// Вывод не работает в каждом из этих случаев:
f<A>(0); // A не содержит элемент Y
f<B>(0); // Y элемент B не является типом
g<C>(0); // N элемент C не является нетипом
h<D>(0); // TT элемент D не является шаблоном
// Вывод успешен в каждом из этих случаев:
f<B1>(0);
g<C1>(0);
h<D1>(0);
}
// сделать: необходимо продемонстрировать разрешение перегрузки, а не только отказ
- попытка создать указатель на ссылку
- попытка создать ссылку на void
- попытка создать указатель на элемент в T, где T не является классовым типом:
template<typename T>
class is_class
{
typedef char yes[1];
typedef char no[2];
template<typename C>
static yes& test(int C::*); // выбирается, если C является типом класса
template<typename C>
static no& test(...); // выбирается иначе
public:
static bool const value = sizeof(test<T>(nullptr)) == sizeof(yes);
};
- попытка присвоить недопустимый тип параметру шаблона, не являющемуся типом:
template<class T, T>
struct S {};
template<class T>
int f(S<T, T()>*);
struct X {};
int i0 = f<X>(0);
// сделать: необходимо продемонстрировать разрешение перегрузки, а не только отказ
- попытка выполнить недопустимое преобразование в
- в выражении аргумента шаблона
- в выражении, используемом в объявлении функции:
template<class T, T*> int f(int);
int i2 = f<int,1>(0); // не возможно преобразовать 1 в int*
// сделать: необходимо продемонстрировать разрешение перегрузки, а не только отказ
- попытка создать тип функции с параметром типа void
- попытка создать тип функции, который возвращает тип массива или тип функции
Выражение SFINAE
|
Только константные выражения, которые используются в типах (например, границы массива), должны были рассматриваться как SFINAE (а не серьёзные ошибки) до C++11. |
(до C++11) |
|
Следующие выражения ошибок являются ошибками SFINAE
struct X {};
struct Y { Y(X){} }; // X конвертируется в Y
template<class T>
auto f(T t1, T t2) -> decltype(t1 + t2); // перегрузка #1
X f(Y, Y); // перегрузка #2
X x1, x2;
X x3 = f(x1, x2); // вывод не удался в #1 (выражение x1 + x2 неправильно сформировано),
// только #2 находится в наборе перегрузки и вызывается
|
(начиная с C++11) |
SFINAE в частичных специализациях
Вывод и подстановка также происходят при определении того, генерируется ли специализация шаблона класса или переменной (начиная с C++14) некоторой частичной специализацией или первичным шаблоном. Компиляторы не рассматривают сбой подстановки как серьёзную ошибку во время такого определения, а вместо этого игнорируют соответствующее объявление частичной специализации, как если бы при разрешении перегрузки использовались шаблоны функций.
// первичный шаблон обрабатывает нессылочные типы:
template<class T, class = void>
struct reference_traits
{
using add_lref = T;
using add_rref = T;
};
// специализация распознаёт ссылочные типы:
template<class T>
struct reference_traits<T, std::void_t<T&>>
{
using add_lref = T&;
using add_rref = T&&;
};
template<class T>
using add_lvalue_reference_t = typename reference_traits<T>::add_lref;
template<class T>
using add_rvalue_reference_t = typename reference_traits<T>::add_rref;
Примечания: в настоящее время частичная специализация SFINAE формально не поддерживается стандартом (смотрите также CWG проблема 2054), однако LFTS требует, чтобы она работала, начиная с версии 2 (смотрите также идиома обнаружения).
Поддержка библиотеки
|
Стандартный библиотечный компонент std::enable_if позволяет создать ошибку подстановки, чтобы включить или отключить определённые перегрузки на основе условия, оцениваемого во время компиляции. Кроме того, многие свойства типов должны быть реализованы с помощью SFINAE, если соответствующие расширения компилятора недоступны. |
(начиная с C++11) |
|
Компонент стандартной библиотеки std::void_t это еще одна служебная метафункция, упрощающая приложения SFINAE с частичной специализацией. |
(начиная с C++17) |
Альтернативы
Где применимо, диспетчеризация тегов, if constexpr (начиная с C++17) и концептов (начиная с C++20) обычно предпочтительнее использования SFINAE.
|
|
(начиная с C++11) |
Примеры
Распространённой идиомой является использование выражения SFINAE для возвращаемого типа, где выражение использует оператор запятая, чьё левое подвыражение является тем, которое проверяется (приведение к void, чтобы гарантировать, что оператор запятая, определяемый пользователем, в возвращаемом типе не выбран), а правое подвыражение имеет тип, который должна возвращать функция.
#include <iostream>
// Эта перегрузка добавляется в набор перегрузок, если C является типом
// класса или ссылки на класс, а F является указателем на функцию-элемент C
template<class C, class F>
auto test(C c, F f) -> decltype((void)(c.*f)(), void())
{
std::cout << "(1) Вызвана перегрузка класса/ссылки на класс\n";
}
// Эта перегрузка добавляется в набор перегрузок, если C является типом
// указателя на класс, а F является указателем на функцию-элемент C
template<class C, class F>
auto test(C c, F f) -> decltype((void)((c->*f)()), void())
{
std::cout << "(2) Вызвана перегрузка указателя\n";
}
// Эта перегрузка всегда находится в наборе перегрузок: параметр
// с многоточием имеет самый низкий рейтинг разрешения перегрузки
void test(...)
{
std::cout << "(3) Вызвана всеобъемлющая перегрузка\n";
}
int main()
{
struct X { void f() {} };
X x;
X& rx = x;
test(x, &X::f); // (1)
test(rx, &X::f); // (1), создаёт копию x
test(&x, &X::f); // (2)
test(42, 1337); // (3)
}
Вывод:
(1) Вызвана перегрузка класса/ссылки на класс
(1) Вызвана перегрузка класса/ссылки на класс
(2) Вызвана перегрузка указателя
(3) Вызвана всеобъемлющая перегрузка
Отчёты о дефектах
Следующие изменения поведения были применены с обратной силой к ранее опубликованным стандартам C++:
| Номер | Применён | Поведение в стандарте | Корректное поведение |
|---|---|---|---|
| CWG 295 | c++98 | создание cv-квалифицированного типа функции может привести к сбою подстановки |
сделано не сбоем, отбросив cv-квалификацию |
| CWG 1227 | c++98 | порядок подстановки не указан | такой же, как лексический порядок |
| CWG 2322 | c++11 | объявления в разных лексических порядках приведут к тому, что экземпляры шаблона будут создаваться в другом порядке или вообще без порядка |
такой случай некорректен, диагностика не требуется |