Spec-Zone.ru › C++

SFINAE

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


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

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

Объяснение

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

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

Подстановка происходит в

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

Только константные выражения, используемые в типах (например, границы массива), должны были обрабатываться как SFINAE (а не как жёсткие ошибки) до C++11.

(до 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;

Поддержка библиотек

Компонент стандартной библиотеки 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>
 
// 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/w/cpp/language/sfinae

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API