Объясните C ++ SFINAE программисту, не являющемуся C ++ - PullRequest
39 голосов
/ 04 августа 2010

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

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

Ответы [ 5 ]

98 голосов
/ 04 августа 2010

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

10 голосов
/ 04 августа 2010

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

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

7 голосов
/ 04 августа 2010

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

Причина, по которой мы хотим, это очевидна - мы можем захотеть сделать несколько разные вещи для разных типов (например, абсолютное значение комплекса вычисляется как x*conj(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 - класс без вложенного типа с именем type.
  • создание типа массива неположительного размера. Для примера см. ответ этого литба
  • создание указателя на тип, который не является классом. например. int C::* для C = int

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


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

5 голосов
/ 04 августа 2010

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

Самая фундаментальная конструкция SFINAE - это использование enable_if.Единственная сложность заключается в том, что class enable_if не инкапсулирует SFINAE, а просто раскрывает его.* здесь) и ряд параллельных, иначе противоречивых определений.Некоторые ошибки встречаются во всех, кроме одного определения, которое компилятор выбирает и использует, не жалуясь на другие.

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

3 голосов
/ 04 августа 2010

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

...