Namespaces
Variants

SFINAE

From cppreference.net
C++ language
General topics
Flow control
Conditional execution statements
Iteration statements (loops)
Jump statements
Functions
Function declaration
Lambda function expression
inline specifier
Dynamic exception specifications ( until C++17* )
noexcept specifier (C++11)
Exceptions
Namespaces
Types
Specifiers
constexpr (C++11)
consteval (C++20)
constinit (C++20)
Storage duration specifiers
Initialization
Expressions
Alternative representations
Literals
Boolean - Integer - Floating-point
Character - String - nullptr (C++11)
User-defined (C++11)
Utilities
Attributes (C++11)
Types
typedef declaration
Type alias declaration (C++11)
Casts
Memory allocation
Classes
Class-specific function properties
Special member functions
Templates
Miscellaneous

"Ошибка подстановки не является ошибкой"


Данное правило применяется во время разрешения перегрузки шаблонов функций: Когда подстановка явно указанного или выведенного типа для параметра шаблона завершается неудачей, специализация исключается из набора перегрузки вместо того, чтобы вызывать ошибку компиляции.

Эта функция используется в метапрограммировании шаблонов.

Содержание

Объяснение

Параметры шаблона функции подставляются (заменяются аргументами шаблона) дважды:

  • явно указанные аргументы шаблона подставляются до вывода аргументов шаблона
  • выведенные аргументы и аргументы, полученные из значений по умолчанию, подставляются после вывода аргументов шаблона

Замена происходит в

  • все типы, используемые в типе функции (который включает тип возвращаемого значения и типы всех параметров)
  • все типы, используемые в объявлениях параметров шаблона
  • все типы, используемые в списке аргументов шаблона частичной специализации
  • все выражения, используемые в типе функции
  • все выражения, используемые в объявлении параметра шаблона
  • все выражения, используемые в списке аргументов шаблона частичной специализации
(начиная с 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.

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