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

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

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

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 << 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: 
//     http://stackru.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, что означает, что мы создали массив нулевого размера, что недопустимо. Вот где сама СФИНА входит в картину. Попытка заменить тип, который не поддерживает operator<< за U потерпит неудачу, потому что это произведет массив нулевого размера. Но это не ошибка - это просто означает, что функция исключена из набора перегрузки. Поэтому другая функция - единственная, которая может использоваться в таком случае.

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

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

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

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

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. компилятор заменяет все Ts в декларации как chars. Это дает 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: или частичная специализация шаблона при разговоре о шаблонах классов

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.

Другие вопросы по тегам