SFINAE
"Ошибка подстановки не является ошибкой"
Данное правило применяется во время разрешения перегрузки шаблонов функций: Когда
подстановка
явно указанного или
выведенного типа
для параметра шаблона завершается неудачей, специализация исключается из
набора перегрузки
вместо того, чтобы вызывать ошибку компиляции.
Эта функция используется в метапрограммировании шаблонов.
Содержание |
Объяснение
Параметры шаблона функции подставляются (заменяются аргументами шаблона) дважды:
- явно указанные аргументы шаблона подставляются до вывода аргументов шаблона
- выведенные аргументы и аргументы, полученные из значений по умолчанию, подставляются после вывода аргументов шаблона
Замена происходит в
- все типы, используемые в типе функции (который включает тип возвращаемого значения и типы всех параметров)
- все типы, используемые в объявлениях параметров шаблона
- все типы, используемые в списке аргументов шаблона частичной специализации
|
(начиная с C++11) |
|
(since C++20) |
Ошибка подстановки — это любая ситуация, когда тип или выражение выше были бы некорректно сформированы (с обязательной диагностикой), если бы они были записаны с использованием подставленных аргументов.
Только ошибки в типах и выражениях в непосредственном контексте типа функции или типов её параметров шаблона или её explicit specifier (since C++20) являются ошибками SFINAE. Если вычисление подставленного типа/выражения вызывает побочный эффект, такой как инстанцирование специализации шаблона, генерацию неявно определённой функции-члена и т.д., ошибки в этих побочных эффектах считаются жёсткими ошибками. lambda expression не считается частью непосредственного контекста. (since C++20)
|
Этот раздел не завершён
Причина: мини-пример, где это важно |
Подстановка происходит в лексикографическом порядке и останавливается при возникновении ошибки.
|
Если имеются множественные объявления с различным лексическим порядком (например, шаблон функции объявлен с завершающим возвращаемым типом, который подставляется после параметра, и переобъявлен с обычным возвращаемым типом, который подставляется до параметра), и это приводит к тому, что инстанцирования шаблонов происходят в другом порядке или не происходят вовсе, то программа является некорректной; диагностика не требуется. |
(since C++11) |
template<typename A> struct B { using type = typename A::type; }; template< class T, class U = typename T::type, // Ошибка SFINAE, если T не имеет члена type class V = typename B<T>::type> // Жесткая ошибка, если B не имеет члена type // (гарантированно не возникает согласно 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)); // некорректно, диагностика не требуется
Type SFINAE
Следующие ошибки типов являются ошибками SFINAE:
|
(since 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); } // todo: необходимо продемонстрировать разрешение перегрузки, а не только неудачу
- попытка создать указатель на ссылку
- попытка создать ссылку на 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); // todo: необходимо продемонстрировать разрешение перегрузки, а не просто ошибку
- попытка выполнить недопустимое преобразование в
-
- в выражении аргумента шаблона
- в выражении, используемом в объявлении функции:
template<class T, T*> int f(int); int i2 = f<int, 1>(0); // невозможно преобразовать 1 в int* // todo: необходимо продемонстрировать разрешение перегрузки, а не просто ошибку
- попытка создания типа функции с параметром типа void
- попытка создания типа функции, возвращающей тип массива или тип функции
Expression SFINAE
|
До C++11 только константные выражения, используемые в типах (такие как границы массивов), должны были обрабатываться как SFINAE (а не как критические ошибки). |
(until 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 |
(since C++11) |
SFINAE в частичных специализациях
Дедукция и подстановка также происходят при определении того, создана ли специализация шаблона класса или переменной (since 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;
Поддержка библиотек
|
Компонент стандартной библиотеки std::enable_if позволяет создавать ошибку подстановки для включения или отключения конкретных перегрузок на основе условия, вычисляемого во время компиляции. Кроме того, многие type traits должны быть реализованы с использованием SFINAE, если соответствующие расширения компилятора недоступны. |
(since C++11) |
|
Компонент стандартной библиотеки std::void_t представляет собой еще одну метафункцию-утилиту, упрощающую применение SFINAE в частичных специализациях. |
(since C++17) |
Альтернативы
В соответствующих случаях
tag dispatch
,
if constexpr
(since C++17)
, и
concepts
(since C++20)
обычно предпочтительнее использования SFINAE.
|
|
(начиная с C++11) |
Примеры
Распространённой идиомой является использование SFINAE в возвращаемом типе, где выражение использует оператор запятой, левое подвыражение которого проверяется (приводится к void, чтобы гарантировать, что пользовательский operator, для возвращаемого типа не будет выбран), а правое подвыражение имеет тип, который должна возвращать функция.
#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 | Applied to | Behavior as published | Correct behavior |
|---|---|---|---|
| CWG 295 | C++98 |
создание cv-квалифицированного типа функции
могло приводить к неудаче подстановки |
сделано не неудачей,
отбрасывая cv-квалификацию |
| CWG 1227 | C++98 | порядок подстановки не был определен | такой же, как лексический порядок |
| CWG 2054 | C++98 | подстановка в частичных специализациях не была корректно определена | определена |
| CWG 2322 | C++11 |
объявления в разных лексических порядках могли приводить к тому, что инстанцирования шаблонов
происходили в другом порядке или вообще не происходили |
такой случай является некорректным,
диагностика не требуется |