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

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

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

5 ответов


предупреждение: это действительно длинное объяснение, но, надеюсь, это действительно объясняет не только то, что SFINAE делает, но дает некоторое представление о том, когда и почему вы можете использовать его.

хорошо, чтобы объяснить это, нам, вероятно, нужно создать резервную копию и немного объяснить шаблоны. Как мы все знаем, Python использует то, что обычно называют duck typing-например, когда вы вызываете функцию, вы можете передать объект 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; }

теперь наш!--10--> намного больше похоже на Python - в частности, мы можем вызвать его одинаково хорошо для объекта x любого типа, для которого x + 1 определяется.

Теперь, рассмотрим, например, что мы хотим записать некоторые объекты в поток. К сожалению, некоторые из этих объектов записываются в поток с помощью stream << object, но и другим использовать . Мы хотим быть в состоянии справиться ни один без необходимости указания пользователем. Теперь специализация шаблона позволяет нам написать специализированный шаблон, так что если бы это было один тип, который используется 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://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 был выбран, но в противном случае функция returning ).

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


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

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


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 и три, потому что 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 as char
  2. компилятор заменяет все Ts в декларации как chars. Это дает void f(char t, int(*)[sizeof(char)-sizeof(int)] = 0).
  3. тип второго аргумента-указатель на array int [sizeof(char)-sizeof(int)]. Размер этого массива может быть например. -3 (в зависимости от вашей платформы).
  4. массивы длины <= 0 недопустимы, поэтому компилятор сбрасывает перегрузку. Ошибка Подстановки Не Является Ошибкой, компилятор не будет отклонять программу.

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

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

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

  • выбор вложенного типа типа, у которого его нет. например. typename T::type на T = int или T = A здесь A - класс без вложенного типа с именем type.
  • создание типа массива непозитивного размера. Для пример, см. этот litb это
  • создание указателя на тип, не класс. например. 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.