Sfinae c что это
Перейти к содержимому

Sfinae c что это

  • автор:

Класс enable_if

Условно создает экземпляр типа для разрешения перегрузки SFINAE. Вложенное определение типа enable_if::type (синоним для Type ) существует, если и только если значение Condition равно true .

Синтаксис

template struct enable_if; 

Параметры

B
Значение, определяющее наличие результирующего типа.

T
Тип для создания экземпляра, если B есть true .

Замечания

Если B имеет true enable_if вложенный типdef с именем typedef с именем type, который является синонимом T .

Если B это false так, enable_if не имеет вложенного типа с именем «type».

Предоставляется следующий шаблон псевдонима.

template using enable_if_t = typename enable_if::type; 

В C++сбой подстановки параметров шаблона не является ошибкой в самом деле— это называется SFINAE (сбой подстановки не является ошибкой). Обычно enable_if используется для удаления кандидатов из разрешения перегрузки, т. е. функция отбраковывает набор перегрузки, чтобы одно определение было отброшено в пользу другого. Это соответствует поведению SFINAE. Дополнительные сведения о SFINAE см. в разделе «Сбой подстановки» не является ошибкой в Википедии.

Вот 4 примера сценариев.

  • Сценарий 1. Упаковка возвращаемого типа функции:
 template typename enable_if::type yourfunction(args) // The alias template makes it more concise: template enable_if_t yourfunction(args) 
  • Сценарий 2. Добавление параметра функции с аргументом по умолчанию:
 template your_return_type_if_present yourfunction(args, enable_if_t = BAR) 
  • Сценарий 3. Добавление параметра шаблона с аргументом по умолчанию:
 template > rest_of_function_declaration_goes_here 
  • Сценарий 4. Если функция содержит аргумент без шаблона, ее тип можно упаковать:
 template void your_function(const T& t, enable_if_t::value, const string&> s) 

Сценарий 1 не применяется к конструкторам и операторам преобразования, так как у них нет возвращаемых типов.

В сценарии 2 параметр остается без имени. Можно использовать ::type Dummy = BAR , но имя Dummy не играет роли, поэтому указание имени, вероятно, вызовет предупреждение о параметре без ссылки. Необходимо выбрать тип параметра функции FOO и аргумент по умолчанию BAR . Можно использовать int и 0 , но тогда пользователи кода смогут случайно передать функции дополнительное целое число, которое будет проигнорировано. Мы рекомендуем использовать void ** и значение 0 или nullptr , так как почти ничего нельзя преобразовать в тип void ** .

template your_return_type_if_present yourfunction(args, typename enable_if::type = nullptr) 

Сценарий 2 также подходит для обычных конструкторов. Но он не работает с операторами преобразования, так как они не могут принимать дополнительные параметры. Он также не работает для variadic конструкторов, так как добавление дополнительных параметров делает пакет параметров функции неисключаемой контекстом и тем самым побеждает назначение enable_if .

В сценарии 3 используется имя Dummy , но это необязательно. Подойдет и просто » typename = typename «, но, если вы считаете, что это выглядит странно, можно использовать имя-заглушку (только не применяйте имя, которое может использоваться в определении функции). Если не передать тип функции enable_if , по умолчанию используется тип void, и это вполне логично, так как Dummy не играет никакой роли. Это работает для всего, включая операторы преобразования и variadic конструкторы.

Сценарий 4 работает для конструкторов без возвращаемых типов, что устраняет ограничение упаковки сценария 1. Однако сценарий 4 применяется только для аргументов функции без шаблонов, которые не всегда доступны. (При использовании сценария 4 для аргументов функции на основе шаблона устранение аргументов шаблона не работает.)

enable_if — это мощное средство, которое может быть опасным при неправильном использовании. Так как цель функции — удалить кандидаты до разрешения перегрузки, при ее неправильном применении результаты могут быть очень запутанными. Вот несколько рекомендаций.

  • Не используйте enable_if выбор между реализациями во время компиляции. Не пишите одну функцию enable_if для CONDITION и другую для !CONDITION . Используйте шаблон отправки тегов, например, алгоритм может выбирать реализации в зависимости от силы указанных итераторов.
  • Не используйте enable_if для принудительного применения требований. Если вы хотите проверить параметры шаблона, а если проверка завершается ошибкой, вместо выбора другой реализации используйте static_assert .
  • Используйте enable_if при наличии набора перегрузок, который делает код неоднозначным. Чаще всего это происходит в конструкторах с неявным преобразованием.

Пример

В этом примере объясняется, как функция std::make_pair() шаблона стандартной библиотеки C++ использует преимущества enable_if .

void func(const pair&); void func(const pair&); func(make_pair("foo", "bar")); 

В этом примере make_pair(«foo», «bar») возвращает pair . Разрешение перегрузок должно определить требуемую функцию func() . pair содержит конструктор с неявным преобразованием из pair . Это не новый элемент, он был представлен в C++98. Однако в C++98/03 сигнатура конструктора с неявным преобразованием всегда существует, даже если это pair(const pair&) . Процессу разрешения перегрузок все равно, что попытка создать экземпляр конструктора приведет к отрицательным последствиям, так как const char * нельзя неявно преобразовать в int . Процесс смотрит только на сигнатуры перед созданием экземпляра определений функций. Поэтому этот пример кода неоднозначен, так как существуют сигнатуры для преобразования pair в pair и pair .

В C++11 эта неоднозначность была устранена с помощью enable_if , чтобы конструкция pair(const pair&) существовала только, если const X& неявно преобразуется в A , а const Y& неявно преобразуется в B . Это позволяет разрешать перегрузку, чтобы определить, pair что не преобразуется в pair и что перегрузка, которая принимается pair , является жизнеспособной.

Требования

Заголовок.

Пространство имен std :

Как сделать SFINAE изящным и надежным

Сегодня у нас гостевой пост Адама Балаша (Ádám Balázs). Адам является инженером-программистом в Verizon Smart Communities Hungary и занимается разработкой видеоаналитики для встраиваемых систем. Одна из его страстей — оптимизации времени компиляции, поэтому он сразу согласился написать гостевой пост на эту тему. Вы можете найти Адама в онлайне на LinkedIn.

В серии статей о том, как сделать SFINAE изящным, мы увидели, как сделать наш SFINAE-шаблон довольно лаконичным и выразительным.

Просто взгляните на его оригинальную форму:

template class MyClass < public: void MyClass(T const& x)<>template void f(T&& x, typename std::enable_if::value, std::nullptr_t>::type = nullptr)<> >;

И сравните ее с этой более выразительной формой:

template using IsNotReference = std::enable_if_t>; template class MyClass < public: void f(T const& x)<>template> void f(T&& x)<> >;

Мы можем разумно полагать, что уже можно расслабиться и начать использовать его в производстве. Мы могли бы, он работает в большинстве случаев, но — как мы говорим об интерфейсах — наш код должен быть безопасным и надежным. Так ли это? Давайте попробуем взломать его!

Недостаток № 1: SFINAE можно обойти

Обычно SFINAE используется для отключения части кода в зависимости от условия. Это может быть очень полезно, если нам нужно реализовать, например, пользовательскую функцию abs по какой-либо причине (пользовательский арифметический класс, оптимизация для конкретного оборудования, в учебных целях и т. д.):

template < typename T >T myAbs( T val ) < return( ( val int main() < int a< std::numeric_limits< int >::max() >; std::cout

Эта программа выводит следующее, что выглядит вполне нормально:

a: 2147483647 myAbs( a ): 2147483647

Но мы можем вызвать нашу функцию abs с беззнаковыми аргументами T , и эффект будет катастрофическим:

nt main() < unsigned int a< std::numeric_limits< unsigned int >::max() >; std::cout

Действительно, теперь программа выводит:

a: 4294967295 myAbs( a ): 1

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

template < typename T >using IsSigned = std::enable_if_t < std::is_signed_v< T >>; template < typename T, typename = IsSigned< T >> T myAbs( T val )

Код работает должным образом: вызов myAbs с беззнаковым типом вызывает ошибку времени компиляции:

candidate template ignored: requirement ‘std::is_signed_v< unsigned int>‘ was not satisfied [with T = unsigned int]

Взлом SFINAE состояния

Тогда что не так с этой функцией? Чтобы ответить на этот вопрос, мы должны проверить, как myAbs реализует SFINAE.

template T > > T myAbs( T val );

myAbs — это шаблон функции с двумя типами параметров шаблона для ввода. Первый является фактическим типом аргумента функции, второй является анонимным типом назначенным по умолчанию IsSigned < T >(иначе Std::enable_if_t < std::is_signed_v < T >> или иначе Std::enable_if < std::is_signed_v < T>, void>::type , который является void или неудавшейся подстановкой).

Как мы можем вызвать myAbs ? Есть 3 способа:

int a< myAbs( -5 ) >; int b< myAbs< int >( -5 ) >; int c< myAbs< int, void >( -5 ) >;

Первый и второй вызовы незамысловаты, но третий вызывает интерес: что это за аргумент шаблона void ?

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

unsigned int d< myAbs< unsigned int, void >( 5u ) >; unsigned int e< myAbs< unsigned int, void >( std::numeric_limits< unsigned int >::max() ) >;

Этот код прекрасно компилируется, но приводит к катастрофическим результатам, для избежания которых, мы использовали SFINAE:

a: 4294967295 myAbs( a ): 1

Мы решим эту проблему — но сначала: есть ли другие недостатки? Что ж…

Недостаток № 2: У нас не может быть конкретных реализаций

Другое распространенное использование SFINAE — предоставление конкретных реализаций для определенных условий времени компиляции. Что, если мы не хотим полностью запретить вызов myAbs со значениями без знака и предоставляем тривиальную реализацию для этих случаев? Мы можем использовать if constexpr в C ++ 17 (мы рассмотрим это позже), или же мы можем:

 template < typename T >using IsSigned = std::enable_if_t < std::is_signed_v< T >>; template < typename T >using IsUnsigned = std::enable_if_t < std::is_unsigned_v< T >>; template < typename T, typename = IsSigned< T >> T myAbs( T val ) < return( ( val template < typename T, typename = IsUnsigned< T >> T myAbs( T val )
error: template parameter redefines default argument template < typename T, typename = IsUnsigned< T >> note: previous default template argument defined here template < typename T, typename = IsSigned< T >>

Ой-ой, стандарт C++ (C++ 17; §17.1.16) гласит следующее:

«Аргументы по умолчанию не должны предоставляться параметру шаблона двумя разными объявлениями в одной и той же области видимости».

Упс, это именно то, что мы сделали…

Почему бы не использовать обычный if?

Мы могли бы просто использовать if во время выполнения вместо этого:

template < typename T >T myAbs( T val ) < if( std::is_signed_v< T >) < return ( ( val else < return val; >>

Компилятор оптимизировал бы условие, потому что if (std::is_signed_v < T>) становится if (true) или if (false) после создания шаблона. Да, с нашей текущей реализацией myAbs это будет работать. Но в целом это накладывает огромное ограничение: операторы if и else должны быть действительными для каждого T . Что если мы немного изменим нашу реализацию:

template < typename T >T myAbs( T val ) < if( std::is_signed_v< T >) < return std::abs( val ); >else < return val; >> int main() < unsigned int a< myAbs( 5u ) >; >

Наш код сразу даст сбой:

error: call of overloaded ‘abs(unsigned int&)’ is ambiguous

Это ограничение — то, что устраняет SFINAE: мы можем написать код, который действителен только для подмножества T (в myAbs действителен только для беззнаковых типов или действителен только для знаковых типов).

Решение: еще одна форма для SFINAE

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

Что если мы используем наш SFINAE-код для объявления типа параметра шаблона вместо предоставления типа по умолчанию? Давайте попробуем:

template < typename T >using IsSigned = std::enable_if_t< std::is_signed_v< T >, bool >; template < typename T, IsSigned< T >= true > T myAbs( T val ) < return( ( val int main() < //int a< myAbs( 5u ) >; int b< myAbs< int >( 5u ) >; //int c< myAbs< unsigned int, true >( 5u ) >; >

Нам нужно, чтобы IsSigned был типом, отличным от void в допустимых случаях, потому что мы хотим предоставить значение по умолчанию для этого типа. Для типа void нет значения, поэтому мы должны использовать что-то другое: bool, int, enum, nullptr_t и т. д. Обычно я использую bool — в этом случае выражения выглядят осмысленно:

template < typename T, IsSigned< T >= true >

Оно работает! Для myAbs (5u) компилятор выдает ошибку, как и раньше:

candidate template ignored: requirement 'std::is_signed_v' was not satisfied [with T = unsigned int

Второй вызов — myAbs < int>(5u) — все еще действителен, мы сообщаем компилятору тип T явно, поэтому он преобразует 5u в int .

Наконец, мы больше не можем обводить myAbs вокруг пальца: myAbs < unsigned int, true>(5u) вызывает ошибку. Неважно, если мы предоставляем в вызове значение по умолчанию или нет, часть выражения SFINAE оценивается в любом случае, потому что компилятору нужен тип аргумента анонимного значения шаблона.

Мы можем перейти к следующей проблеме — но погодите минуту! Я думаю, что мы больше не переопределяем аргумент по умолчанию для того же параметра шаблона Какова была исходная ситуация?

template < typename T, typename = IsUnsigned< T >> T myAbs( T val ); template < typename T, typename = IsSigned< T >> T myAbs( T val );

Но теперь с текущим кодом:

template < typename T, IsUnsigned< T >= true > T myAbs( T val ); template < typename T, IsSigned< T >= true > T myAbs( T val );

Он выглядит очень похоже на предыдущий код, поэтому мы можем подумать, что это тоже не сработает, но на самом деле этот код не имеет той же проблемы. Что такое IsUnsigned < T>? Bool или неудавшаяся подстановка. А что такое IsSigned < T>? То же самое, но если одно из них Bool, другое — неудавшаяся подстановка.

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

Синтаксический сахар

Если вы предпочитаете enable_if с void , а не bool -версию, то у нас для вас есть хорошие новости — вы можете использовать следующий синтаксис:

template typename T > using IsSigned = std::enable_if_t std::is_signed_v T > >; template< typename T, IsSigned T >. > T myAbs( T val );

Как это работает? Это шаблонная функция с бестиповой частью с переменным количеством аргументов, которая принимает в качестве аргументов шаблона ноль или более значений типа IsSigned < T>(void или неудавшаяся подстановка).

Компилятор должен знать тип значений с переменным количеством аргументов, чтобы часть SFINAE нельзя было обойти. Фактический вызов остается таким же простым, как и раньше, потому что переменная часть принимает ноль фактических аргументов.

Старые версии C ++

Все вышеперечисленное работает с C++11, единственное отличие — многословность определений ограничений между стандартными версиями:

//C++11 template < typename T >using IsSigned = typename std::enable_if< std::is_signed< T >::value, bool >::type; //C++14 - std::enable_if_t template < typename T >using IsSigned = std::enable_if_t< std::is_signed< T >::value, bool >; //C++17 - std::is_signed_v template < typename T >using IsSigned = std::enable_if_t< std::is_signed_v< T >, bool >;

Но шаблон остается прежним:

template < typename T, IsSigned< T >= true >

В старом добром C++98 нет псевдонимов шаблонов, кроме того, шаблоны функций не могут иметь типы или значения по умолчанию. Мы можем вставить наш SFINAE-код в тип результата или только в список параметров функции. Рекомендуется второй вариант, потому что конструкторы не имеют типов результатов. Лучшее, что мы можем сделать, это что-то вроде этого:

template < typename T >T myAbs( T val, typename my_enable_if< my_is_signed< T >::value, bool >::type = true )

Просто для сравнения — современная версия C++:

template < typename T, IsSigned< T >= true > T myAbs( T val )

Версия C++98 уродлива, вводит бессмысленный параметр, но она работает — вы можете использовать ее, если это крайне необходимо. И да: my_enable_if и my_is_signed должны быть реализованы ( std :: enable_if и std :: is_signed были новыми в C++11).

Современное состояние

C++17 ввел if constexpr — способ для отбрасывания кода на основе условий во время компиляции. Оба оператора if и else должны быть синтаксически корректны, но условие будет оцениваться во время компиляции.

template < typename T >T myAbs( T val ) < if constexpr( std::is_signed_v< T >) < return( ( val else < if constexpr( std::is_unsigned_v< T >) < return val; >/*else < static_assert( false, "T must be signed or unsigned arithmetic type." ); >*/ > >

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

К счастью, существует лазейка: в шаблонных объектах отброшенные операторы не создаются, если условие не зависит от значения. Отлично!

Таким образом, единственная проблема с нашим кодом состоит в том, что он дает сбой во время определения шаблона. Если бы мы могли отложить оценку static_assert до времени создания шаблона, проблема была бы решена: он был бы создан тогда и только тогда, когда все наши условия false. Но как мы можем отложить static_assert до создания шаблона? Сделайте его условие зависимым от типа!

template < typename >inline constexpr bool dependent_false_v< false >; template < typename T >T myAbs( T val ) < if constexpr( std::is_signed_v< T >) < return( ( val else < if constexpr( std::is_unsigned_v< T >) < return val; >else < static_assert( dependent_false_v< T >, "Unsupported type" ); > > >

О будущем

Мы действительно уже близки, но нужно еще немного подождать, пока C++20 принесет окончательное решение: концепции (concept)! Это полностью изменит способ использования шаблонов (и SFINAE).

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

template < typename T >concept bool Arithmetic() < return std::is_arithmetic_v< T >; >

И как мы можем использовать концепции? Есть три способа:

// Многословная версия template < typename T >requires Arithmetic< T >() T myAbs( T val ); // Укороченная версия template < Arithmetic T >T myAbs( T val ); // ОГО Arithmetic myAbs( Arithmetic val );

Обратите внимание, что третья форма все еще объявляет функцию шаблона! Вот полная реализация myAbs в C++20:

template < typename T >concept bool Arithmetic() < return std::is_arithmetic_v< T >; > Arithmetic myAbs( Arithmetic val ) < if constexpr( std::is_signed_v< decltype( val ) >) < return( ( val else < return val; >> int main() < unsigned int a< myAbs( 5u ) >; int b< myAbs< int >( 5u ) >; //std::string c< myAbs( "d" ) >; >

Закомментированный вызов дает следующую ошибку:

error: cannot call function 'auto myAbs(auto:1) [with auto:1 = const char*]' constraints not satisfied within 'template concept bool Arithmetic() [with T = const char*]' concept bool Arithmetic()< ^~~~~~~~~~ 'std::is_arithmetic_v' evaluated to false

Я призываю всех смело использовать эти методы в производственном коде, время компиляции дешевле, чем время выполнения. Happy SFINAEing!

Как сделать SFINAE изящным и надежным

Сегодня у нас гостевой пост Адама Балаша (Ádám Balázs). Адам является инженером-программистом в Verizon Smart Communities Hungary и занимается разработкой видеоаналитики для встраиваемых систем. Одна из его страстей — оптимизации времени компиляции, поэтому он сразу согласился написать гостевой пост на эту тему. Вы можете найти Адама в онлайне на LinkedIn.

Просто взгляните на его оригинальную форму:

template class MyClass < public: void MyClass(T const& x)<>template void f(T&& x, typename std::enable_if::value, std::nullptr_t>::type = nullptr)<> >;

И сравните ее с этой более выразительной формой:

template using IsNotReference = std::enable_if_t>; template class MyClass < public: void f(T const& x)<>template> void f(T&& x)<> >;

Мы можем разумно полагать, что уже можно расслабиться и начать использовать его в производстве. Мы могли бы, он работает в большинстве случаев, но — как мы говорим об интерфейсах — наш код должен быть безопасным и надежным. Так ли это? Давайте попробуем взломать его!

Недостаток № 1: SFINAE можно обойти

Обычно SFINAE используется для отключения части кода в зависимости от условия. Это может быть очень полезно, если нам нужно реализовать, например, пользовательскую функцию abs по какой-либо причине (пользовательский арифметический класс, оптимизация для конкретного оборудования, в учебных целях и т. д.):

template < typename T >T myAbs( T val ) < return( ( val int main() < int a< std::numeric_limits< int >::max() >; std::cout

Эта программа выводит следующее, что выглядит вполне нормально:

a: 2147483647 myAbs( a ): 2147483647

Но мы можем вызвать нашу функцию abs с беззнаковыми аргументами T , и эффект будет катастрофическим:

nt main() < unsigned int a< std::numeric_limits< unsigned int >::max() >; std::cout

Действительно, теперь программа выводит:

a: 4294967295 myAbs( a ): 1

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

template < typename T >using IsSigned = std::enable_if_t < std::is_signed_v< T >>; template < typename T, typename = IsSigned< T >> T myAbs( T val )

Код работает должным образом: вызов myAbs с беззнаковым типом вызывает ошибку времени компиляции:

candidate template ignored: requirement 'std::is_signed_v< unsigned int>' was not satisfied [with T = unsigned int]

Взлом SFINAE состояния

Тогда что не так с этой функцией? Чтобы ответить на этот вопрос, мы должны проверить, как myAbs реализует SFINAE.

template < typename T, typename = IsSigned> T myAbs( T val );

myAbs — это шаблон функции с двумя типами параметров шаблона для ввода. Первый является фактическим типом аргумента функции, второй является анонимным типом назначенным по умолчанию IsSigned < T >(иначе std::enable_if_t < std::is_signed_v < T >> или иначе std::enable_if < std::is_signed_v < T>, void>::type , который является void или неудавшейся подстановкой).

Как мы можем вызвать myAbs ? Есть 3 способа:

int a< myAbs( -5 ) >; int b< myAbs< int >( -5 ) >; int c< myAbs< int, void >( -5 ) >;

Первый и второй вызовы незамысловаты, но третий вызывает интерес: что это за аргумент шаблона void ?

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

unsigned int d< myAbs< unsigned int, void >( 5u ) >; unsigned int e< myAbs< unsigned int, void >( std::numeric_limits< unsigned int >::max() ) >;

Этот код прекрасно компилируется, но приводит к катастрофическим результатам, для избежания которых, мы использовали SFINAE:

a: 4294967295 myAbs( a ): 1

Мы решим эту проблему — но сначала: есть ли другие недостатки? Что ж…

Недостаток № 2: У нас не может быть конкретных реализаций

Другое распространенное использование SFINAE — предоставление конкретных реализаций для определенных условий времени компиляции. Что, если мы не хотим полностью запретить вызов myAbs со значениями без знака и предоставляем тривиальную реализацию для этих случаев? Мы можем использовать if constexpr в C ++ 17 (мы рассмотрим это позже), или же мы можем:

 template < typename T >using IsSigned = std::enable_if_t < std::is_signed_v< T >>; template < typename T >using IsUnsigned = std::enable_if_t < std::is_unsigned_v< T >>; template < typename T, typename = IsSigned< T >> T myAbs( T val ) < return( ( val template < typename T, typename = IsUnsigned< T >> T myAbs( T val )
error: template parameter redefines default argument template < typename T, typename = IsUnsigned< T >> note: previous default template argument defined here template < typename T, typename = IsSigned< T >>

Ой-ой, стандарт C++ (C++ 17; §17.1.16) гласит следующее:

«Аргументы по умолчанию не должны предоставляться параметру шаблона двумя разными объявлениями в одной и той же области видимости».

Упс, это именно то, что мы сделали…

Почему бы не использовать обычный if?

Мы могли бы просто использовать if во время выполнения вместо этого:

template < typename T >T myAbs( T val ) < if( std::is_signed_v< T >) < return ( ( val else < return val; >>

Компилятор оптимизировал бы условие, потому что if (std::is_signed_v < T>) становится if (true) или if (false) после создания шаблона. Да, с нашей текущей реализацией myAbs это будет работать. Но в целом это накладывает огромное ограничение: операторы if и else должны быть действительными для каждого T . Что если мы немного изменим нашу реализацию:

template < typename T >T myAbs( T val ) < if( std::is_signed_v< T >) < return std::abs( val ); >else < return val; >> int main() < unsigned int a< myAbs( 5u ) >; >

Наш код сразу даст сбой:

error: call of overloaded ‘abs(unsigned int&)’ is ambiguous

Это ограничение — то, что устраняет SFINAE: мы можем написать код, который действителен только для подмножества T (в myAbs действителен только для беззнаковых типов или действителен только для знаковых типов).

Решение: еще одна форма для SFINAE

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

Что если мы используем наш SFINAE-код для объявления типа параметра шаблона вместо предоставления типа по умолчанию? Давайте попробуем:

template < typename T >using IsSigned = std::enable_if_t< std::is_signed_v< T >, bool >; template < typename T, IsSigned< T >= true > T myAbs( T val ) < return( ( val int main() < //int a< myAbs( 5u ) >; int b< myAbs< int >( 5u ) >; //int c< myAbs< unsigned int, true >( 5u ) >; >

Нам нужно, чтобы IsSigned был типом, отличным от void в допустимых случаях, потому что мы хотим предоставить значение по умолчанию для этого типа. Для типа void нет значения, поэтому мы должны использовать что-то другое: bool, int, enum, nullptr_t и т. д. Обычно я использую bool — в этом случае выражения выглядят осмысленно:

template < typename T, IsSigned< T >= true >

Оно работает! Для myAbs (5u) компилятор выдает ошибку, как и раньше:

candidate template ignored: requirement 'std::is_signed_v' was not satisfied [with T = unsigned int

Второй вызов — myAbs < int>(5u) — все еще действителен, мы сообщаем компилятору тип T явно, поэтому он преобразует 5u в int .

Наконец, мы больше не можем обводить myAbs вокруг пальца: myAbs < unsigned int, true>(5u) вызывает ошибку. Неважно, если мы предоставляем в вызове значение по умолчанию или нет, часть выражения SFINAE оценивается в любом случае, потому что компилятору нужен тип аргумента анонимного значения шаблона.

Мы можем перейти к следующей проблеме — но погодите минуту! Я думаю, что мы больше не переопределяем аргумент по умолчанию для того же параметра шаблона Какова была исходная ситуация?

template < typename T, typename = IsUnsigned< T >> T myAbs( T val ); template < typename T, typename = IsSigned< T >> T myAbs( T val );

Но теперь с текущим кодом:

template < typename T, IsUnsigned< T >= true > T myAbs( T val ); template < typename T, IsSigned< T >= true > T myAbs( T val );

Он выглядит очень похоже на предыдущий код, поэтому мы можем подумать, что это тоже не сработает, но на самом деле этот код не имеет той же проблемы. Что такое IsUnsigned < T>? Bool или неудавшаяся подстановка. А что такое IsSigned < T>? То же самое, но если одно из них Bool, другое — неудавшаяся подстановка.

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

Синтаксический сахар

UPD. Данный абзац был удален автором из-за обнаружившихся в нем ошибок.

Старые версии C ++

Все вышеперечисленное работает с C++11, единственное отличие — многословность определений ограничений между стандартными версиями:

//C++11 template < typename T >using IsSigned = typename std::enable_if< std::is_signed< T >::value, bool >::type; //C++14 - std::enable_if_t template < typename T >using IsSigned = std::enable_if_t< std::is_signed< T >::value, bool >; //C++17 - std::is_signed_v template < typename T >using IsSigned = std::enable_if_t< std::is_signed_v< T >, bool >;

Но шаблон остается прежним:

template < typename T, IsSigned< T >= true >

В старом добром C++98 нет псевдонимов шаблонов, кроме того, шаблоны функций не могут иметь типы или значения по умолчанию. Мы можем вставить наш SFINAE-код в тип результата или только в список параметров функции. Рекомендуется второй вариант, потому что конструкторы не имеют типов результатов. Лучшее, что мы можем сделать, это что-то вроде этого:

template < typename T >T myAbs( T val, typename my_enable_if< my_is_signed< T >::value, bool >::type = true )

Просто для сравнения — современная версия C++:

template < typename T, IsSigned< T >= true > T myAbs( T val )

Версия C++98 уродлива, вводит бессмысленный параметр, но она работает — вы можете использовать ее, если это крайне необходимо. И да: my_enable_if и my_is_signed должны быть реализованы ( std :: enable_if и std :: is_signed были новыми в C++11).

Современное состояние

C++17 ввел if constexpr — способ для отбрасывания кода на основе условий во время компиляции. Оба оператора if и else должны быть синтаксически корректны, но условие будет оцениваться во время компиляции.

template < typename T >T myAbs( T val ) < if constexpr( std::is_signed_v< T >) < return( ( val else < if constexpr( std::is_unsigned_v< T >) < return val; >/*else < static_assert( false, "T must be signed or unsigned arithmetic type." ); >*/ > >

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

К счастью, существует лазейка: в шаблонных объектах отброшенные операторы не создаются, если условие не зависит от значения. Отлично!

Таким образом, единственная проблема с нашим кодом состоит в том, что он дает сбой во время определения шаблона. Если бы мы могли отложить оценку static_assert до времени создания шаблона, проблема была бы решена: он был бы создан тогда и только тогда, когда все наши условия false. Но как мы можем отложить static_assert до создания шаблона? Сделайте его условие зависимым от типа!

template < typename >inline constexpr bool dependent_false_v< false >; template < typename T >T myAbs( T val ) < if constexpr( std::is_signed_v< T >) < return( ( val else < if constexpr( std::is_unsigned_v< T >) < return val; >else < static_assert( dependent_false_v< T >, "Unsupported type" ); > > >

О будущем

Мы действительно уже близки, но нужно еще немного подождать, пока C++20 принесет окончательное решение: концепции (concept)! Это полностью изменит способ использования шаблонов (и SFINAE).

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

template < typename T >concept bool Arithmetic() < return std::is_arithmetic_v< T >; >

И как мы можем использовать концепции? Есть три способа:

// Многословная версия template < typename T >requires Arithmetic< T >() T myAbs( T val ); // Укороченная версия template < Arithmetic T >T myAbs( T val ); // ОГО Arithmetic myAbs( Arithmetic val );

Обратите внимание, что третья форма все еще объявляет функцию шаблона! Вот полная реализация myAbs в C++20:

template < typename T >concept bool Arithmetic() < return std::is_arithmetic_v< T >; > Arithmetic myAbs( Arithmetic val ) < if constexpr( std::is_signed_v< decltype( val ) >) < return( ( val else < return val; >> int main() < unsigned int a< myAbs( 5u ) >; int b< myAbs< int >( 5u ) >; //std::string c< myAbs( "d" ) >; >

Закомментированный вызов дает следующую ошибку:

error: cannot call function 'auto myAbs(auto:1) [with auto:1 = const char*]' constraints not satisfied within 'template concept bool Arithmetic() [with T = const char*]' concept bool Arithmetic()< ^~~~~~~~~~ 'std::is_arithmetic_v' evaluated to false

Я призываю всех смело использовать эти методы в производственном коде, время компиляции дешевле, чем время выполнения. Happy SFINAEing!

  • Блог компании OTUS
  • Программирование
  • C++

Каким способом правильно писать SFINAE в шаблонах? C++

Мне сказали, что первый способ лучше, но не объяснили чем и в чем разница между ними. Сказали что-то про невозможность подменить типы. Я примерно догадываюсь о чем речь, но можете ли объяснить подробнее, что тут происходит?

Отслеживать
219k 15 15 золотых знаков 120 120 серебряных знаков 230 230 бронзовых знаков
задан 30 окт 2021 в 11:30
ComeInRage ComeInRage
1,590 6 6 серебряных знаков 14 14 бронзовых знаков

Второе вообще не является SFINAE, так как тип параметра шаблона по-умолчанию не является частью сигнатуры. Первое тоже не совсем SFINAE.

30 окт 2021 в 11:51

В вашем примере SFINAE участия не принимает. Для SFINAE надо (как минимум) два шаблона, из которых при подстановке аргумента шаблона посредством SFINAE будет выбираться один. см en.cppreference.com/w/cpp/language/sfinae

30 окт 2021 в 12:32

enable_if сам по себе не обязательно оборачивается SFINAE. Если шаблон только один и надо наложить условие на его параметры (как в вашем примере), то следует использовать static_assert и там концепты, а SFINAE при этом ни на каком этапе не понадобится.

30 окт 2021 в 12:46

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

30 окт 2021 в 18:02

"Если шаблон только один . следует использовать static_assert" Как-то сомнительно. Никогда не знаешь, кто захочет проверить твой шаблон SFINAE. 🙂 Не очень понял, почему enable_if_t противопоставляется концептам, ведь это почти одно и то же.

30 окт 2021 в 21:45

2 ответа 2

Сортировка: Сброс на вариант по умолчанию

Во-первых, в первом способе нельзя выключить проверку, явно указав шаблонный аргумент, как уже написал Qwertiy.

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

В-третьих, первый способ не дает криворукому программисту инстанцировать шаблон несколько раз, указав разные шаблонные аргументы. Для этого нужен именно nullptr_t , потому что у него только одно возможное значение. Некоторые пишут первый способ с int =0 , и он такой защиты уже не дает.

Но если вам доступен C++20, то лучше использовать requires вместо старомодного std::enable_if_t , или еще лучше - сокращенную запись с концептом, а-ля std::invocable <. >вместо typename .

requires и концпеты всем хорошы. У них те же плюсы, что и у enable_if_t , и плюс:

  • Синтаксис удобнее.
  • Перегруженные функции автоматически ранжируются по жесткости требований, если возможно. Пример:

template void foo(T) <> template void foo(T) <> 

    requires проверяется позже, чем обычный SFINAE. Очень редко, но это может мешать. Пример:

template std::make_unsigned_t foo(T t)

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *