close
Пространства имён
Варианты
Действия

SFINAE

Материал из cppreference.com
 
 
Язык С++
Общие темы
Управление потоком
Операторы условного выполнения
if
Операторы итерации (циклы)
Операторы перехода
Функции
Объявление функции
Выражение лямбда-функции
Спецификатор inline
Спецификации динамических исключений (до C++17*)
Спецификатор noexcept (C++11)
Исключения
Пространства имён
Типы
Спецификаторы
decltype (C++11)
auto (C++11)
alignas (C++11)
Спецификаторы длительности хранения
Инициализация
Выражения
Альтернативные представления
Литералы
Логические - Целочисленные - С плавающей запятой
Символьные - Строковые - nullptr (C++11)
Определяемые пользователем (C++11)
Утилиты
Атрибуты (C++11)
Types
Объявление typedef
Объявление псевдонима типа (C++11)
Casts
Неявные преобразования - Явные преобразования
static_cast - dynamic_cast
const_cast - reinterpret_cast
Выделение памяти
Классы
Свойства функции класса
explicit (C++11)
static
Специальные функции-элементы
Шаблоны
Разное
 
 

"Неудачная Замена Не Является Ошибкой - 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.

static_assert обычно предпочтительнее 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 объявления в разных лексических порядках приведут к тому, что
экземпляры шаблона будут создаваться в другом порядке или
вообще без порядка
такой случай некорректен, диагностика не
требуется