Каким способом правильно писать SFINAE в шаблонах? C++
Заметил, что SFINAE в шаблонах пишет по-разному. Двумя способами:
template
<
typename Func,
std::enable_if_t<std::is_invocable_v<Func>, std::nullptr_t> = nullptr
>
И:
template
<
typename Func,
typename = std::enable_if_t<std::is_invocable_v<Func>, std::nullptr_t>
>
Мне сказали, что первый способ лучше, но не объяснили чем и в чем разница между ними. Сказали что-то про невозможность подменить типы. Я примерно догадываюсь о чем речь, но можете ли объяснить подробнее, что тут происходит?
Ответы (2 шт):
Вызываем как положено doSmth([]{})
Первый код: https://godbolt.org/z/P4YGP95PY
#include <iostream>
template
<
typename Func,
std::enable_if_t<std::is_invocable_v<Func>, std::nullptr_t> = nullptr
>
void doSmth(Func&& callback)
{
std::cout << (void*)&callback << std::endl;
}
int main()
{
doSmth([]{});
return 0;
}
Второй: https://godbolt.org/z/sxvvP7G9v
#include <iostream>
template
<
typename Func,
typename = std::enable_if_t<std::is_invocable_v<Func>, std::nullptr_t>
>
void doSmth(Func&& callback)
{
std::cout << (void*)&callback << std::endl;
}
int main()
{
doSmth([]{});
return 0;
}
Передаём мусор: doSmth(1)
Первый код не компилируется: https://godbolt.org/z/E7T7385a4
<source>: In function 'int main()':
<source>:15:9: error: no matching function for call to 'doSmth(int)'
15 | doSmth(1);
| ~~~~~~^~~
<source>:8:6: note: candidate: 'template<class Func, typename std::enable_if<is_invocable_v<Func>, std::nullptr_t>::type <anonymous> > void doSmth(Func&&)'
8 | void doSmth(Func&& callback)
| ^~~~~~
<source>:8:6: note: template argument deduction/substitution failed:
<source>:6:65: error: no type named 'type' in 'struct std::enable_if<false, std::nullptr_t>'
6 | std::enable_if_t<std::is_invocable_v<Func>, std::nullptr_t> = nullptr
| ^~~~~~~
Второй код не компилируется: https://godbolt.org/z/d8j4K4GeE
<source>: In function 'int main()':
<source>:15:9: error: no matching function for call to 'doSmth(int)'
15 | doSmth(1);
| ~~~~~~^~~
<source>:8:6: note: candidate: 'template<class Func, class> void doSmth(Func&&)'
8 | void doSmth(Func&& callback)
| ^~~~~~
<source>:8:6: note: template argument deduction/substitution failed:
In file included from /opt/compiler-explorer/gcc-11.2.0/include/c++/11.2.0/bits/move.h:57,
from /opt/compiler-explorer/gcc-11.2.0/include/c++/11.2.0/bits/nested_exception.h:40,
from /opt/compiler-explorer/gcc-11.2.0/include/c++/11.2.0/exception:148,
from /opt/compiler-explorer/gcc-11.2.0/include/c++/11.2.0/ios:39,
from /opt/compiler-explorer/gcc-11.2.0/include/c++/11.2.0/ostream:38,
from /opt/compiler-explorer/gcc-11.2.0/include/c++/11.2.0/iostream:39,
from <source>:1:
/opt/compiler-explorer/gcc-11.2.0/include/c++/11.2.0/type_traits: In substitution of 'template<bool _Cond, class _Tp> using enable_if_t = typename std::enable_if::type [with bool _Cond = false; _Tp = std::nullptr_t]':
<source>:6:3: required from here
/opt/compiler-explorer/gcc-11.2.0/include/c++/11.2.0/type_traits:2514:11: error: no type named 'type' in 'struct std::enable_if<false, std::nullptr_t>'
2514 | using enable_if_t = typename enable_if<_Cond, _Tp>::type;
| ^~~~~~~~~~~
А теперь обходим защиту:
Только второй код: https://godbolt.org/z/9538fab81
doSmth<int, int>(1);
Код успешно компилируется и выводит:
0x7ffd6828091c
А вот способа такое провернуть с первым кодом я придумать не могу.
Во-первых, в первом способе нельзя выключить проверку, явно указав шаблонный аргумент, как уже написал Qwertiy.
Во-вторых, первый способ позволяет перегружать функцию, так чтобы отличалось только условие. Если попробовать это со вторым способом, компилятор будет ругаться на повторное определение функции.
В-третьих, первый способ не дает криворукому программисту инстанцировать шаблон несколько раз, указав разные шаблонные аргументы. Для этого нужен именно nullptr_t, потому что у него только одно возможное значение. Некоторые пишут первый способ с int =0, и он такой защиты уже не дает...
Но если вам доступен C++20, то лучше использовать requires вместо старомодного std::enable_if_t, или еще лучше - сокращенную запись с концептом, а-ля std::invocable<...> вместо typename.
requires и концпеты всем хорошы. У них те же плюсы, что и у enable_if_t, и плюс:
Синтаксис удобнее.
Перегруженные функции автоматически ранжируются по жесткости требований, если возможно. Пример:
template <std::integral T> void foo(T) {} template <std::signed_integral T> void foo(T) {}Вызов
foo(1)скомпилируется. Хотя у обоих функций условия выполняются, будет выбрана вторая, потому что у нее более жесткое условие. А если попробовать то же самое сenable_if_t, компилятор не сможет выбрать функцию и будет ругаться.
Минус один:
requiresпроверяется позже, чем обычный SFINAE. Очень редко, но это может мешать. Пример:template <std::integral T> std::make_unsigned_t<T> foo(T t) {return t;}Эта функция написана плохо: ошибку
foo(1.f)не отловить SFINAE, потому чтоstd::make_unsignedсломается до того, как компилятор проверит концепт.Классический SFINAE здесь бы сработал, потому что был бы проверен до инстанцирования
make_unsigned.