Предупреждение: это действительно длинное объяснение, но, надеюсь, оно действительно объясняет не только то, что делает SFINAE, но дает некоторое представление о том, когда и почему вы можете его использовать.
Хорошо, чтобы объяснить это, нам, вероятно, нужно сделать резервную копию и объяснить шаблоны. Как все мы знаем, Python использует то, что обычно называют «утиной печатью» - например, когда вы вызываете функцию, вы можете передать объект X этой функции, если X обеспечивает все операции, используемые функцией.
В C ++ для нормальной (не шаблонной) функции требуется указать тип параметра. Если вы определили функцию как:
int plus1(int x) { return x + 1; }
Вы можете только применить эту функцию к int
. Тот факт, что он использует x
таким образом, что может точно так же применяться к другим типам, таким как long
или float
, не имеет значения - он в любом случае применим только к int
.
Чтобы приблизиться к набиранию уток в Python, вы можете создать шаблон:
template <class T>
T plus1(T x) { return x + 1; }
Теперь наш plus1
намного больше похож на тот, который был бы в Python - в частности, мы можем вызывать его одинаково хорошо для объекта x
любого типа, для которого определено x + 1
.
Теперь рассмотрим, например, что мы хотим записать некоторые объекты в поток. К сожалению, некоторые из этих объектов записываются в поток с использованием stream << object
, но другие вместо этого используют object.write(stream);
. Мы хотим иметь возможность обрабатывать любой из них без необходимости указания пользователем какой. Теперь специализация шаблона позволяет нам написать специализированный шаблон, поэтому, если бы это был один тип, который использовал синтаксис object.write(stream)
, мы могли бы сделать что-то вроде:
template <class T>
std::ostream &write_object(T object, std::ostream &os) {
return os << object;
}
template <>
std::ostream &write_object(special_object object, std::ostream &os) {
return object.write(os);
}
Это хорошо для одного типа, и если бы мы захотели достаточно сильно, мы могли бы добавить больше специализаций для всех типов, которые не поддерживают stream << object
- но как только (например) пользователь добавляет новый тип, который не поддерживает stream << object
, все снова ломается.
Нам нужен способ использовать первую специализацию для любого объекта, который поддерживает stream << object;
, но вторую для чего-либо еще (хотя мы можем иногда захотеть добавить третью для объектов, которые вместо этого используют x.print(stream);
).
Мы можем использовать SFINAE, чтобы сделать это определение. Чтобы сделать это, мы обычно полагаемся на несколько других странных деталей C ++. Одним из них является использование оператора sizeof
. sizeof
определяет размер типа или выражения, но он делает это полностью во время компиляции, рассматривая задействованные типы , без оценки самого выражения. Например, если у меня есть что-то вроде:
int func() { return -1; }
Я могу использовать sizeof(func())
. В этом случае func()
возвращает int
, поэтому sizeof(func())
эквивалентно sizeof(int)
.
Вторым интересным элементом, который часто используется, является тот факт, что размер массива должен быть положительным, , а не ноль.
Теперь, собрав их вместе, мы можем сделать что-то вроде этого:
// stolen, more or less intact from:
// /1656439/sfinae-sizeof-opredelit-skompilirovano-li-vyrazhenie
template<class T> T& ref();
template<class T> T val();
template<class T>
struct has_inserter
{
template<class U>
static char test(char(*)[sizeof(ref<std::ostream>() << val<U>())]);
template<class U>
static long test(...);
enum { value = 1 == sizeof test<T>(0) };
typedef boost::integral_constant<bool, value> type;
};
Здесь у нас есть две перегрузки test
. Второй из них принимает список переменных аргумента (...
), что означает, что он может соответствовать любому типу - но это также последний выбор, который компилятор сделает при выборе перегрузки, поэтому он будет только совпадать, если первый не . Другая перегрузка test
немного интереснее: она определяет функцию, которая принимает один параметр: массив указателей на функции, которые возвращают char
, где размер массива (по сути) sizeof(stream << object)
. Если stream << object
не является допустимым выражением, sizeof
даст 0, что означает, что мы создали массив нулевого размера, что недопустимо. Вот где сама SFINAE входит в картину. Попытка заменить тип, который не поддерживает operator<<
на U
, потерпит неудачу, потому что это приведет к массиву нулевого размера. Но это не ошибка - это просто означает, что функция исключена из набора перегрузки. Поэтому другая функция - единственная, которая может использоваться в таком случае.
Это затем используется в выражении enum
ниже - оно просматривает возвращаемое значение из выбранной перегрузки test
и проверяет, равно ли оно 1 (если оно есть, это означает, что функция возвращает char
был выбран, но в противном случае была выбрана функция, возвращающая long
).
В результате has_inserter<type>::value
будет l
, если мы могли бы использовать some_ostream << object;
скомпилировать, и 0
, еслине будет.Затем мы можем использовать это значение для управления специализацией шаблона, чтобы выбрать правильный способ выписать значение для определенного типа.