Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

SFINAE — это легко

Anna | 24.06.2014 | нет комментариев

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

image

Здравствуйте, сотрудники.
Хочу рассказать о SFINAE, увлекательном и дюже пригодном (к сожалению*) механизме языка C , тот, что, впрочем, может представляться неподготовленному человеку крайне мозгоразрывающим. В реальности правило его применения довольно примитивен и ясен, будучи сформулирован в виде нескольких чётких расположений. Эта заметка рассчитана на читателей, владеющих базовыми умениями о образцах в C и знакомых, правда бы шапочно, с C 11.
* Отчего к сожалению? Правда применение SFINAE — увлекательный и прекрасный приём, переросший в обширно используемую идиому языка, значительно отменнее было бы иметь средства, очевидно описывающие работу с типами.

Вначале, на каждый случай, дюже коротко скажу о метапрограммировании в C . Метапрограммирование — это операции, изготавливаемые во время компиляции программы. Аналогично тому, как обыкновенные функции разрешают получать значения, при помощи метафункций получают типы и константы времени компиляции. Одно из особенно знаменитых использований метапрограммирования, правда вдалеке не исключительное — узнавать свойства типов. Всё это, разумеется, используется при разработке образцов: где-то бывает пригодно знать, имеем мы дело со трудным пользовательским классом с нетривиальным конструктором либо с обыкновенным int, где-то нужно установить, унаследован ли один тип от иного, либо дозволено ли преобразовать один тип в иной. Мы разглядим механизм использования SFINAE на классическом примере: проверке того, существует ли в классе функция-член с заданными типами доводов и возвращаемого значения. Я постараюсь детально и подробно пройти по каждому этапам создания проверочной метафункции и проследить откуда что берётся.

Сокращение SFINAE расшифровывается как substitution failure is not an error и обозначает следующее: при определении перегрузок функции ложные инстанциации образцов не вызывают ошибку компиляции, а отбрасываются из списка кандидатов на особенно подходящую перегрузку. Выражаясь по-человечески, это обозначает вот что:

  • Когда речь заходит о SFINAE, это непременно связано с перегрузкой функций.
  • Это работает при механическом итоге типов образца (type deduction) по доводам функции.
  • Некоторые перегрузки могут отбрасываться в том случае, когда их немыслимо инстанциировать из-за возникающей синтаксической ошибки; компиляция при этом продолжается как ни в чём не бывало, без ошибок.
  • Отбросить могут только образец.

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

Разглядим примитивный пример:

int difference(int val1, int val2)
{
    return val1 - val2;
}

template<typename T>
typename T::difference_type difference(const T& val1, const T& val2)
{
    return val1 - val2;
}

Функция difference отменно работает для целых доводов. А вот с пользовательскими типами данных начинаются тонкости. Итог вычитания не неизменно имеет тот же самый тип, что и операнды. Так, разность 2-х дат — промежуток времени, тот, что сам по себе датой не является. Если пользовательский тип MyDateимеет внутри себя определение typedef MyInterval difference_type; и оператор вычитания MyInterval operator - (const MyDate& rhs) const;, к нему применима шаблонная перегрузка. Вызовdifference(date1, date2) сумеет «увидеть» и шаблонную перегрузку, и версию, принимающую int, при этом шаблонная перегрузка будет сочтена больше подходящей.
Тип MyString, в котором нет difference_type, при подстановке вызовет ошибку: функция возвращала бы несуществующий тип.Подобно, конструкция val1 - val2 требует наличия бинарного оператора «минус» и тоже может породить синтаксическую ошибку. Вызов difference с доводами типа MyString сумеет «увидеть» только int-версию функции. Эта исключительная версия окажется довольно подходящей только если вMyString определён оператор реформирования в число. Получается, что шаблонная функция differenceпроверяет тип довода на одновременное выполнение сразу трёх условий: присутствие difference_type, присутствие оператора вычитания и вероятность приведения итога вычитания к типу difference_type(реформирование подразумевается оператором return). Типам, нарушающим правда бы одно условие, эта перегрузка не видна.

Испробуем же придумать, как сделать метафункцию, которая говорит нам, есть ли в каком-то типе способvoid foo(int). Рачительная STL, исключительно начиная с версии C 11, теснее определила для нас много пригодных метафункций, размещённых в основном в заголовках type_traits и limits, впрочем именно такой, какую мы дерзновенно замыслили сделать, там отчего-то нет. Метафункция традиционно выглядит как шаблонная конструкция без данных, внутри которой определён итог операции: данный через typedef тип с именем type либо статическая константа value. Такого соглашения придерживается STL, и причин оригинальничать у нас нет, следственно будем придерживаться установленного примера.

Дозволено сразу написать «скелет» нашей грядущей метафункции:

template<typename T> struct has_foo{};

Она определяет присутствие способа, из чего сразу ясно, что итог должен иметь булевский тип:

template<typename T> struct has_foo{
{
    static constexpr bool value = true;  // Теперь придумаем, что тут написать, а пока каждому отвечаем "да".
};

А вот сейчас нужно придумать, как сделать перегрузку, определяющую надобные нам свойства типа, и как получить из неё булевскую константу. Прелесть в том, что нам не необходимо давать тела перегрузкам: от того что каждая работа происходит в режиме компиляции за счёт манипуляций с типами, хватит одних объявлений.
Видимо, мы хотим, Дабы наша метафункция была применима для всякого типа. чай про всякий тип дозволено сказать, есть ли в нём желанный способ. Значит, has_foo не должна вызывать ошибки компиляции, какой бы параметр мы ни подставили. А оплошность-то произойдёт, если внезапно окажется, что Получается, что нам необходимо две перегрузки одной проверочной функции. Одна из них, «детектор» должна быть синтаксически верной только для типов, содержащих необходимый способ. Иная, «подложка», должна быть всеядной, то есть быть довольно подходящей для всяких подставленных типов. В то же время «детектор» должен иметь неоспоримое преобладание в «подходящести» перед «подложкой». Наименее приоритетным и в то же время максимально всеядным в определении перегрузок является эллипсис (троеточие, обозначающее переменное число доводов):

template<typename T> struct has_foo{
{
    void detect(...); // С "подложкой" всё легко.
    static constexpr bool value = true;  // Хорошо-хорошо, теснее скоро придумаем!
};

Сейчас нужно объявить «детектор». Это должен быть образец: того, что он теснее внутри шаблоной конструкции, неудовлетворительно! Необходим образец внутри образца [несколько секунд наслаждаемся одобрительными взорами со стороны героев фильма Inception]. К «подложке» это не относится, от того что её мы не будем выкидывать никогда. А вот для «детектора» воспользуемся волшебным словом decltype, которое определяет тип выражения, причём само выражение не вычисляется и в код не переводится. Подставим в качестве выражения вызов того самого способа, с доводами надобного типа. Тогда результатом decltypeбудет возвращаемый тип способа. А если способа с таким именем нет, либо он принимает другие типы доводов, то мы получим ту самую контролируемую ошибку, которую и хотели. Пускай «детектор» возвращает то же, что и foo:

template<typename T> struct has_foo{
{
    void detect(...);
    template<typename U> decltype(U().foo(42)) detect(const U&);
    static constexpr bool value = true;  // Сейчас верно скоро!
};

Если передадим в detect ссылку на const T&, получится, что U — тот же самый тип, что и T. Для проверки соответствия типа возвращаемого значения мы потом улучшим детектор либо придумаем что-то другое по ходу дела.
Впрочем постойте! Мы вызываем способ на свежесконструированном неизвестном объекте, причём сконструирован-то он по умолчанию. А что будет, если мы передадим в has_foo тип, у которого нет конструктора по умолчанию? Безусловно же, оплошность компиляции. Положительнее было бы объявить какую-нибудь функцию, возвращающую значение надобного типа. Вызываться она всё равно не будет, а необходимый результат будет достигнут. STL позаботилась и об этом: в заголовке utility есть функцияdeclval:

template<typename T> struct has_foo{
{
    void detect(...);
    template<typename U> decltype(std::declval<U>().foo(42)) detect(const U&);
    static constexpr bool value = true;  // Примерно готово!
};

Осталось только обучиться отличать «подложку» от «детектора». Здесь нам поможет всё тот же decltype. У «подложки» тип возвращаемого значения неизменно void, а у «детектора» — тип, возвращаемый способом, то есть в случае, когда способ соответствует нашим требованиям… тот же самый void. Так не пойдёт. Сменим-ка мы для «подложки» тип на int. Тогда проверка получается примитивный: если вызов detect на объекте Tимеет тип void, то сработал «детектор» и способ всецело соответствует нашим требованиям. Если тип иной, то либо сработала «подложка», либо способ существует, принимает те самые доводы, но возвращает что-то не то. Проверяем, насколько рачительна STL, и здесь же находим метафункцию проверки типов на равенствоis_same:

template<typename T> struct has_foo{
{
private:  // Спрячем от пользователя детали реализации.
    static int detect(...);  // Статическую функцию и вызывать проще.
    template<typename U> static decltype(std::declval<U>().foo(42)) detect(const U&);
public:
    static constexpr bool value = std::is_same<void, decltype(detect(std::declval<T>()))>::value;  // Вот видите, готово.
};

Ура, мы добились желаемого. Как видите, всё и в самом деле довольно легко. Отдадим подать уважения тем программистам, которые ухитрялись проделывать данный фокус в грозных условиях предыдущего эталона, значительно больше многословно и хитро из-за отсутствия таких пригодных штук, как declval.

SFINAE применяется настоль обширно, что даже в рачительную STL включили особую метафункциюenable_if. Её параметры — булевская константа и тип (по умолчанию void). Если передано true, то в метафункции присутствует тип type: тот, что передан вторым параметром. Если же передано false, то никакого type там нет, что и создаёт ту самую контролируемую ошибку. В свете соображений, перечисленных выше в опрятном списочке, нужно помнить, что enable_if сумеет «вычеркнуть» перегрузку функции только если она — образец, а также озаботиться тем, Дабы список «невычеркнутых» перегрузок никогда не оставался вовсе пустым. Дозволено использовать enable_if и в специализациях шаблонного класса, но в таком случае это теснее не SFINAE, а кое-что как бы static_assert.

Источник: programmingmaster.ru

Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB