Класс 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)