Что такое SFINAE в С++?
Не могли бы вы объяснить это словами, понятными программисту, который не разбирается в С++? Кроме того, какая концепция на языке, подобном Python, соответствует SFINAE?
Что такое SFINAE в С++?
Не могли бы вы объяснить это словами, понятными программисту, который не разбирается в С++? Кроме того, какая концепция на языке, подобном Python, соответствует SFINAE?
Предупреждение: это действительно длинное объяснение, но, надеюсь, оно действительно объясняет не только то, что делает SFINAE, но и дает представление о том, когда и почему вы можете его использовать.
Хорошо, чтобы объяснить это, нам, вероятно, нужно немного создать резервные копии и объяснить шаблоны. Как мы все знаем, Python использует то, что обычно называют утиным типом - например, когда вы вызываете функцию, вы можете передать объект X этой функции, если X предоставляет все операции, используемые этой функцией.
В С++ для нормальной (не шаблонной) функции требуется указать тип параметра. Если вы определили такую функцию, как:
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 для выполнения этого определения. Для этого мы обычно полагаемся на пару других нечетных деталей С++. Один из них - использовать оператор sizeof
. sizeof
определяет размер типа или выражения, но он делает это полностью во время компиляции, просматривая типы, не оценивая само выражение. Например, если у меня есть что-то вроде:
int func() { return -1; }
Я могу использовать sizeof(func())
. В этом случае func()
возвращает int
, поэтому sizeof(func())
эквивалентно sizeof(int)
.
Второй интересный элемент, который часто используется, заключается в том, что размер массива должен быть положительным, не.
Теперь, объединив их, мы можем сделать что-то вроде этого:
// stolen, more or less intact from:
// http://stackoverflow.com/questions/2127693/sfinae-sizeof-detect-if-expression-compiles
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
, если он не будет. Затем мы можем использовать это значение для управления специализацией шаблона, чтобы выбрать правильный способ выписать значение для определенного типа.
Если у вас есть некоторые перегруженные функции шаблона, некоторые из возможных кандидатов для использования могут не поддаваться компиляции при выполнении подстановки шаблона, потому что замещаемая вещь может не иметь правильного поведения. Это не считается ошибкой программирования, неудачные шаблоны просто удаляются из набора, доступного для этого конкретного параметра.
Я понятия не имею, есть ли у Python аналогичная функция, и на самом деле не понимаю, почему программист, не относящийся к С++, должен заботиться об этой функции. Но если вы хотите узнать больше о шаблонах, лучшая книга на них С++ Templates: Полное руководство.
SFINAE - это принцип, который компилятор С++ использует для отфильтровывания некоторых перегруженных функций шаблонов во время разрешения перегрузки (1)
Когда компилятор разрешает конкретный вызов функции, он рассматривает набор доступных объявлений функций и функций, чтобы выяснить, какой из них будет использоваться. В принципе, для этого есть два механизма. Можно назвать синтаксическим. Приведенные объявления:
template <class T> void f(T); //1
template <class T> void f(T*); //2
template <class T> void f(std::complex<T>); //3
разрешающая f((int)1)
удалит версии 2 и три, потому что int
не равно complex<T>
или T*
для некоторого T
. Аналогично, f(std::complex<float>(1))
удалит второй вариант, а f((int*)&x)
удалит третий. Компилятор делает это, пытаясь вывести параметры шаблона из аргументов функции. Если сбой вычета (как в T*
против int
), перегрузка отбрасывается.
Причина, по которой мы хотим этого, очевидна - мы можем сделать несколько разные вещи для разных типов (например, абсолютное значение комплекса вычисляется с помощью x*conj(x)
и дает действительное число, а не комплексное число, которое отличная от вычисления для поплавков).
Если вы уже сделали какое-то декларативное программирование, этот механизм похож на (Haskell):
f Complex x y = ...
f _ = ...
Способ, которым С++ делает это дополнительно, заключается в том, что вывод может завершиться неудачно, даже если выведенные типы в порядке, но обратная подстановка в другую дает некоторый "бессмысленный" результат (подробнее об этом позже). Например:
template <class T> void f(T t, int(*)[sizeof(T)-sizeof(int)] = 0);
при выводе f('c')
(мы вызываем один аргумент, потому что второй аргумент неявный):
T
с char
, который дает тривиально T
как char
T
в объявлении как char
s. Это дает void f(char t, int(*)[sizeof(char)-sizeof(int)] = 0)
.int [sizeof(char)-sizeof(int)]
. Размер этого массива может быть, например. -3 (в зависимости от вашей платформы).<= 0
являются недопустимыми, поэтому компилятор отбрасывает перегрузку. Ошибка подстановки не является ошибкой, компилятор не отклонит программу.В конце, если осталось больше одной перегрузки функции, компилятор использует сравнение последовательностей преобразований и частичный порядок шаблонов, чтобы выбрать тот, который является "лучшим".
Есть больше таких "бессмысленных" результатов, которые работают так, они перечислены в списке в стандарте (С++ 03). В С++ 0x область SFINAE распространяется на почти любую ошибку типа.
Я не буду писать обширный список ошибок SFINAE, но некоторые из самых популярных:
typename T::type
для T = int
или T = A
, где A
- класс без вложенного типа, называемый type
.int C::*
для C = int
Этот механизм не похож ни на что в других языках программирования, о которых я знаю. Если бы вы сделали аналогичную вещь в Haskell, вы бы использовали защитные устройства, которые являются более мощными, но невозможными в С++.
1: или частичные специализированные шаблоны при обсуждении шаблонов классов
Python вам совсем не поможет. Но вы говорите, что уже знакомы с шаблонами.
Наиболее фундаментальной конструкцией SFINAE является использование enable_if
. Единственная сложная часть состоит в том, что class enable_if
не инкапсулирует SFINAE, а просто предоставляет ее.
template< bool enable >
class enable_if { }; // enable_if contains nothing…
template<>
class enable_if< true > { // … unless argument is true…
public:
typedef void type; // … in which case there is a dummy definition
};
template< bool b > // if "b" is true,
typename enable_if< b >::type function() {} //the dummy exists: success
template< bool b >
typename enable_if< ! b >::type function() {} // dummy does not exist: failure
/* But Substitution Failure Is Not An Error!
So, first definition is used and second, although redundant and
nonsensical, is quietly ignored. */
int main() {
function< true >();
}
В SFINAE существует некоторая структура, которая устанавливает условие ошибки (class enable_if
здесь) и количество параллельных, иначе противоречащих друг другу определений. Некоторая ошибка возникает во всех, кроме одного определения, которые компилятор выбирает и использует, не жалуясь на других.
Какие ошибки допустимы, это важная деталь, которая только недавно была стандартизирована, но вы, похоже, не спрашиваете об этом.
В Python нет ничего, что бы отдаленно напоминало SFINAE. У Python нет шаблонов, и, конечно же, нет разрешения на основе параметров, как это происходит при разрешении специализированных шаблонов. Поиск функции выполняется исключительно по имени в Python.