Подтвердить что ты не робот

Объяснить С++ SFINAE программисту, отличному от С++

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

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

4b9b3361

Ответ 1

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

Ответ 2

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

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

Ответ 3

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: или частичные специализированные шаблоны при обсуждении шаблонов классов

Ответ 4

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

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

Ответ 5

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