Объяснить C ++ SFINAE программисту не на C ++

Что такое SFINAE в C ++?

Не могли бы вы объяснить это словами, понятными программисту, который не разбирается в C ++? Также, какому понятию в языке, подобном Python, соответствует SFINAE?

39
задан sbi 12 August 2011 в 22:58
поделиться

5 ответов

Предупреждение: это действительно объяснение, но, надеюсь, оно действительно объясняет не только то, что делает 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 << объект , все снова ломается.

Нам нужен способ использовать первую специализацию для любого объекта, поддерживающего 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: 
//     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 << объект) . Если stream << объект не является допустимым выражением, sizeof вернет 0, что означает, что мы создали массив нулевого размера, что недопустимо. Именно здесь на сцену выходит сама SFINAE. Попытка заменить тип, не поддерживающий operator << на U , завершится неудачей, поскольку приведет к созданию массива нулевого размера. Но это не ошибка - это просто означает, что функция исключена из набора перегрузки. Следовательно, в таком случае можно использовать только другую функцию.

Это затем используется в выражении enum ниже - оно просматривает возвращаемое значение из выбранной перегрузки test и проверяет, равно ли оно 1 (если это так, это означает, что была выбрана функция, возвращающая char , но в противном случае была выбрана функция, возвращающая long ).

В результате has_inserter :: value будет l , если мы сможем использовать some_ostream << объект; будет компилироваться, и ] 0 если бы не было.Затем мы можем использовать это значение для управления специализацией шаблона, чтобы выбрать правильный способ записи значения для определенного типа.

99
ответ дан 27 November 2019 в 02:06
поделиться

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

Я понятия не имею, есть ли в Python аналогичная функция, и не понимаю, почему программисту, не работающему на C ++, должна быть интересна эта функция. Но если вы хотите узнать больше о шаблонах, лучшая книга по ним - это C ++ Templates: The Complete Guide .

10
ответ дан 27 November 2019 в 02:06
поделиться

В Python нет ничего, что отдаленно напоминало бы SFINAE. В Python нет шаблонов, и уж тем более нет разрешения функций на основе параметров, как это происходит при разрешении специализаций шаблонов. Поиск функций в Python осуществляется исключительно по имени.

3
ответ дан 27 November 2019 в 02:06
поделиться

SFINAE - это принцип, который компилятор C ++ использует для фильтрации некоторых перегрузок шаблонных функций во время разрешения перегрузки (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 и 3, потому что int не равно complex или T * для некоторых T . Аналогично, f (std :: complex (1)) удалит второй вариант, а f ((int *) & x) удалит третий. Компилятор делает это, пытаясь вывести параметры шаблона из аргументов функции. Если вывод не удается (как в T * против int ), перегрузка сбрасывается.

Причина, по которой мы этого хотим, очевидна - мы можем захотеть сделать несколько разные вещи для разных типов (например, абсолютное значение комплекса вычисляется с помощью x * con (x) и дает реальное значение число, а не комплексное число, которое отличается от вычисления для чисел с плавающей запятой).

Если вы уже занимались декларативным программированием раньше, этот механизм похож на (Haskell):

f Complex x y = ...
f _           = ...

Способ, которым C ++ продвигает это дальше, заключается в том, что вывод может завершиться неудачно, даже если выведенные типы в порядке, но обратная подстановка в другой дают какой-то "бессмысленный" результат (подробнее об этом позже).Например:

template <class T> void f(T t, int(*)[sizeof(T)-sizeof(int)] = 0);

при выводе f ('c') (мы вызываем с одним аргументом, потому что второй аргумент неявный):

  1. компилятор сопоставляет T с char , который тривиально дает T как char
  2. , компилятор заменяет все T в объявлении как char s . Это дает void f (char t, int (*) [sizeof (char) -sizeof (int)] = 0) .
  3. Типом второго аргумента является указатель на массив int [sizeof (char) -sizeof (int)] . Размер этого массива может быть, например. -3 (в зависимости от вашей платформы).
  4. Массивы длины <= 0 недопустимы, поэтому компилятор отклоняет перегрузку. Ошибка замены не является ошибкой , компилятор не отклонит программу.

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

Есть и другие такие «бессмысленные» результаты, которые работают подобным образом, они перечислены в списке в стандарте (C ++ 03). В C ++ 0x область SFINAE расширена почти до любой ошибки типа.

Я не буду писать обширный список ошибок SFINAE, но некоторые из наиболее популярных:

  • выбор вложенного типа для типа, у которого его нет. например. typename T :: type для T = int или T = A , где A - это класс без вложенного типа с именем типа .
  • создание типа массива неположительного размера.В качестве примера см. этот литб-ответ
  • , создающий указатель члена на тип, не являющийся классом. например. int C :: * for C = int

Этот механизм не похож ни на что в других известных мне языках программирования. Если бы вы проделали то же самое в Haskell, вы бы использовали более мощные средства защиты, но невозможные в C ++.


1: или частичная специализация шаблонов, когда речь идет о шаблонах классов

7
ответ дан 27 November 2019 в 02:06
поделиться

Python вам совершенно не поможет. Но вы говорите, что уже в основном знакомы с шаблонами.

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

Какие типы ошибок допустимы - это важная деталь, которая только недавно была стандартизирована, но вы, похоже, не спрашиваете об этом.

5
ответ дан 27 November 2019 в 02:06
поделиться
Другие вопросы по тегам:

Похожие вопросы: