SFINAE
«Ошибка подстановки — не ошибка»
Это правило применяется при разрешении перегрузки шаблонов функций: если при подстановке явно заданного или выведенного типа вместо параметра шаблона возникает ошибка, специализация исключается из набора перегрузок, а не приводит к ошибке компиляции.
Эта возможность используется в метапрограммировании шаблонов.
Пояснение
Подстановка параметров шаблона функции (их замена аргументами шаблона) выполняется дважды:
- явно заданные аргументы шаблона подставляются до выведения аргументов шаблона
- выведенные аргументы и аргументы, полученные из значений по умолчанию, подставляются после выведения аргументов шаблона
Подстановка выполняется в
- всех типах, используемых в типе функции (включая возвращаемый тип и типы всех параметров)
- всех типах, используемых в объявлениях параметров шаблона
- всех типах, используемых в списке аргументов шаблона частичной специализации
| (начиная с C++11) |
| (начиная с C++20) |
Ошибка подстановки — это любая ситуация, в которой приведённый выше тип или выражение были бы некорректны (и потребовали бы диагностики), если бы были записаны с использованием подставленных аргументов.
Ошибками SFINAE считаются только ошибки в типах и выражениях, находящихся в непосредственном контексте типа функции или типов её параметров шаблона либо её спецификатора explicit(начиная с C++20). Если вычисление подставленного типа или выражения приводит к побочному эффекту, например к инстанцированию некоторой специализации шаблона, генерации неявно определённой функции-члена и т. д., ошибки в этих побочных эффектах считаются безусловными ошибками. Лямбда-выражение не считается частью непосредственного контекста.(начиная с C++20)
Подстановка выполняется в лексическом порядке и прекращается при обнаружении ошибки.
| Если существуют несколько объявлений с разным лексическим порядком (например, шаблон функции объявлен с завершающим возвращаемым типом, который подставляется после параметра, а затем объявлен повторно с обычным возвращаемым типом, который подставлялся бы до параметра), и это привело бы к инстанцированию шаблонов в другом порядке или к отсутствию инстанцирования, программа некорректна; диагностика не требуется. | (начиная с C++11) |
template<typename A>
struct B { using type = typename A::type; };
template<
class T,
class U = typename T::type, // SFINAE failure if T has no member type
class V = typename B<T>::type> // hard error if B has no member type
// (guaranteed to not occur via CWG 1227 because
// substitution into the default template argument
// of U would fail first)
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; // redeclaration
template<class T>
void h(...) {}
using R = decltype(h<int>(0)); // ill-formed, no diagnostic required
SFINAE для типов
Следующие ошибки типов являются ошибками SFINAE:
| (начиная с C++11) |
- попытка создать массив void, массив ссылок, массив функций, массив отрицательного размера, массив нецелочисленного размера или массив нулевого размера:
template<int I>
void div(char(*)[I % 2 == 0] = nullptr)
{
// this overload is selected when I is even
}
template<int I>
void div(char(*)[I % 2 == 1] = nullptr)
{
// this overload is selected when I is odd
}
- попытка использовать тип слева от оператора разрешения области видимости
::, если он не является классом или перечислением:
template<class T> int f(typename T::B*); template<class T> int f(T); int i = f<int>(0); // uses second overload
- попытка использовать член типа, если
- тип не содержит указанный член
- указанный член не является типом, хотя требуется тип
- указанный член не является шаблоном, хотя требуется шаблон
- указанный член не является нетиповым параметром, хотя требуется нетиповый параметр
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()
{
// Deduction fails in each of these cases:
f<A>(0); // A does not contain a member Y
f<B>(0); // The Y member of B is not a type
g<C>(0); // The N member of C is not a non-type
h<D>(0); // The TT member of D is not a template
// Deduction succeeds in each of these cases:
f<B1>(0);
g<C1>(0);
h<D1>(0);
}
// todo: needs to demonstrate overload resolution, not just failure
- попытка создать указатель на ссылку
- попытка создать ссылку на 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::*); // selected if C is a class type
template<typename C>
static no& test(...); // selected otherwise
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);
// todo: needs to demonstrate overload resolution, not just failure
- попытка выполнить недопустимое преобразование
- в выражении аргумента шаблона
- в выражении, используемом в объявлении функции:
template<class T, T*> int f(int); int i2 = f<int, 1>(0); // can’t conv 1 to int* // todo: needs to demonstrate overload resolution, not just failure
- попытка создать тип функции с параметром типа void
- попытка создать тип функции, возвращающей тип массива или тип функции
SFINAE для выражений
| До C++11 требовалось считать ошибками SFINAE (а не безусловными ошибками) только константные выражения, используемые в типах (например, границы массива). | (до C++11) |
| Следующие ошибки выражений являются ошибками SFINAE
struct X {};
struct Y { Y(X){} }; // X is convertible to Y
template<class T>
auto f(T t1, T t2) -> decltype(t1 + t2); // overload #1
X f(Y, Y); // overload #2
X x1, x2;
X x3 = f(x1, x2); // deduction fails on #1 (expression x1 + x2 is ill-formed)
// only #2 is in the overload set, and is called
| (начиная с C++11) |
SFINAE в частичных специализациях
Выведение и подстановка также выполняются при определении того, создаётся ли специализация шаблона класса или переменной(начиная с C++14) некоторой частичной специализацией или основным шаблоном. При таком определении ошибка подстановки не считается безусловной ошибкой, а приводит к игнорированию соответствующего объявления частичной специализации, как при разрешении перегрузки с участием шаблонов функций.
// primary template handles non-referenceable types:
template<class T, class = void>
struct reference_traits
{
using add_lref = T;
using add_rref = T;
};
// specialization recognizes referenceable types:
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, если подходящие расширения компилятора недоступны. | (начиная с C++11) |
| Компонент стандартной библиотеки | (начиная с C++17) |
Альтернативы
Если это применимо, обычно предпочтительнее использовать диспетчеризацию по тегам, if constexpr(начиная с C++17) и концепты (начиная с C++20) вместо SFINAE.
|
| (начиная с C++11) |
Примеры
Распространённый приём — использовать SFINAE для выражений в возвращаемом типе, где выражение использует оператор запятая: левое подвыражение — это проверяемое выражение (приведённое к void, чтобы пользовательский оператор запятая для возвращаемого типа не был выбран), а правое подвыражение имеет тип, который должна возвращать функция.
#include <iostream>
// This overload is added to the set of overloads if C is
// a class or reference-to-class type and F is a pointer to member function of C
template<class C, class F>
auto test(C c, F f) -> decltype((void)(c.*f)(), void())
{
std::cout << "(1) Class/class reference overload called\n";
}
// This overload is added to the set of overloads if C is a
// pointer-to-class type and F is a pointer to member function of C
template<class C, class F>
auto test(C c, F f) -> decltype((void)((c->*f)()), void())
{
std::cout << "(2) Pointer overload called\n";
}
// This overload is always in the set of overloads: ellipsis
// parameter has the lowest ranking for overload resolution
void test(...)
{
std::cout << "(3) Catch-all overload called\n";
}
int main()
{
struct X { void f() {} };
X x;
X& rx = x;
test(x, &X::f); // (1)
test(rx, &X::f); // (1), creates a copy of x
test(&x, &X::f); // (2)
test(42, 1337); // (3)
}
Вывод:
(1) Class/class reference overload called (1) Class/class reference overload called (2) Pointer overload called (3) Catch-all overload called
Отчёты об ошибках стандарта
Следующие отчёты об ошибках, изменяющие поведение, были применены задним числом к ранее опубликованным стандартам C++.
| DR | Применён к | Опубликованное поведение | Корректное поведение |
|---|---|---|---|
| CWG 295 | C++98 | создание cv-квалифицированного типа функции могло привести к ошибке подстановки | больше не приводит к ошибке, cv-квалификация отбрасывается |
| CWG 1227 | C++98 | порядок подстановки не был определён | соответствует лексическому порядку |
| CWG 2054 | C++98 | подстановка в частичных специализациях была описана некорректно | описана корректно |
| CWG 2322 | C++11 | объявления с разным лексическим порядком приводили к инстанцированию шаблонов в другом порядке или к отсутствию инстанцирования | такой случай некорректен, диагностика не требуется |
© cppreference.com
Licensed under the Creative Commons Attribution-ShareAlike Unported License v3.0.
https://en.cppreference.com/cpp/language/sfinae