Interested Article - SFINAE
- 2020-05-13
- 1
SFINAE ( англ. substitution failure is not an error , «неудавшаяся подстановка — не ошибка») — правило языка C++ , связанное с шаблонами и перегрузкой функций . Широко применяется «не по назначению» — для рефлексии при компиляции : в зависимости от свойств типа компиляция идёт по тому или другому пути.
Правило SFINAE гласит: Если не получается рассчитать окончательные типы/значения шаблонных параметров функции, компилятор не выбрасывает ошибку, а ищет другую подходящую перегрузку. Ошибка будет в трёх случаях:
- Не нашлось ни одной подходящей перегрузки.
- Нашлось несколько таких перегрузок, и компилятор не может решить, какую взять.
- Перегрузка нашлась, она оказалась шаблонной, и при инстанцировании шаблона случилась ошибка.
История
Правило существовало ещё в C++98 , и было придумано, чтобы программа не выдавала ошибок, если где-то в заголовочных файлах оказался одноимённый шаблон, далёкий от контекста. Но впоследствии оно оказалось удобно для рефлексии при компиляции. Саму аббревиатуру SFINAE придумал Дэвид Вандервурд, автор книги «Шаблоны C++» (2002).
В
Boost
добавили несложный шаблон
enable_if
, действующий на правиле SFINAE и позволяющий инстанцировать шаблон при определённых условиях.
В стандарте
C++11
правило SFINAE было несколько уточнено, концептуально не меняясь. Туда же вошёл и шаблон
enable_if
(вообще у Boost позаимствованы
chrono
,
random
,
filesystem
и многое другое).
В
C++17
добавили конструкцию
if
constexpr
()
, несколько снизившую надобность в SFINAE.
В
C++20
появилась конструкция
explicit
(
true
)
. С одной стороны, константа в скобках — тоже часть подстановки, и если её не получится рассчитать, это будет неудавшаяся подстановка. С другой — она также снижает надобность в SFINAE.
Изначальное назначение
Предположим, надо вызвать функцию
f(1, 2);
Есть такие версии этой функции:
(1) void f(int, std::vector<int>);
(2) void f(int, int);
(3) void f(double, double);
(4) void f(int, int, char, std::string, std::vector<int>);
(5) void f(std::string);
(6) void f(...);
Компилятор собирает эти функции в список и находит лучшую по определённым правилам — производит разрешение перегрузки ( англ. overload resolution ).
- Сначала компилятор отбрасывает функции, которые не подходят по количеству параметров — (4) и (5).
- Затем отбрасываются шаблонные подстановки, где не удалось рассчитать типы входных параметров и возврата — таковых нет.
- Потом отбрасывается функция (1) — для неё нет подходящего преобразования типов.
- И уж из (2), (3) и (6) по довольно сложным правилам компилятор выбирает (2) — оба типа точно совпадают. Если бы такого абсолютного победителя не было, компилятор выдал бы ошибку, указав, между какими вариантами он колеблется.
Шаг 2, связанный с шаблонными функциями, пока не задействован. Добавим к нашему списку ещё две функции.
(7) template<typename T>
void f(T, T);
(8) template<typename T>
void f(T, typename T::iterator);
Функция 7 будет отброшена на четвёртом шаге, потому что нешаблонная функция всегда «сильнее» шаблонной.
Шаблон 8 далёк от нашей задачи, так как рассчитан на некий класс, имеющий внутри тип
iterator
.
Второй шаг и есть SFINAE
: компилятор говорит, что
T = int
, пробует подставить
int
в шаблон, и отбрасываются те шаблоны, где подстановка не привела к успеху. Поэтому
неудавшаяся подстановка — не ошибка
.
Пример рефлексии при компиляции через SFINAE
Этот пример компилируется даже на C++03 .
#include <iostream>
#include <vector>
#include <set>
template<typename T>
class DetectFind
{
struct Fallback { int find; }; // add member name "find"
struct Derived : T, Fallback { };
template<typename U, U> struct Check;
typedef char Yes[1]; // typedef for an array of size one.
typedef char No[2]; // typedef for an array of size two.
template<typename U>
static No& func(Check<int Fallback::*, &U::find> *);
template<typename U>
static Yes& func(...);
public:
typedef DetectFind type;
enum { value = sizeof(func<Derived>(0)) == sizeof(Yes) };
};
int main()
{
std::cout << DetectFind<std::vector<int> >::value << ' '
<< DetectFind<std::set<int> >::value << std::endl;
return 0;
}
Принцип действия: в строке
sizeof
(
func
<
Derived
>
(
0
))
происходит разрешение перегрузки, и конкретный тип
Check
<
int
Fallback
::*
,
&
U
::
find
>
*
сильнее, чем переменные аргументы
...
. Из-за того, что
func
под
sizeof
, нет нужды инстанцировать шаблонные функции, достаточно подставить типы — потому у функций только заголовки без тел. Вторая функция, возвращающая тип
Yes
, подставится всегда, а что же с первой?
Она подставится, если шаблонный тип
Check
будет существовать (поскольку
Check
под указателем, точный тип не важен, главное — существование). Первый параметр шаблона — тип, второй — константа этого типа. В качестве типа берётся указатель на
int
-поле объекта
Fallback
(по факту — смещение от начала объекта до поля), в качестве константы — указатель на поле
find
. Константа будет определена и иметь нужный тип, если единственное поле
Derived
::
find
взято у объекта
Fallback
— то есть отсутствует другой
find
, позаимствованный у
T
.
Примечания
Ссылки
- (англ.) . cppreference.com. Дата обращения: 9 января 2020. 6 мая 2021 года.
- На русском
- OldFisher. . habr.com (13 декабря 2013). Дата обращения: 9 января 2020. 1 марта 2021 года.
- ixSci. (12 декабря 2016). Дата обращения: 9 января 2020. 31 декабря 2019 года.
-
Адам Балаш.
.
habr.com
(10 сентября 2019). Дата обращения: 9 января 2020.
19 сентября 2020 года.
- Оригинал: Ádám Balázs. (англ.) . Fluent C++ (23 августа 2019). Дата обращения: 9 января 2020. 3 декабря 2019 года.
- 2020-05-13
- 1