Approaches to function SFINAE in C++
I am using function SFINAE heavily in a project and am not sure if there are any differences between the following two approaches (other than style):
#include <cstdlib>
#include <type_traits>
#include <iostream>
template <class T, class = std::enable_if_t<std::is_same_v<T, int>>>
void foo()
{
std::cout << "method 1" << std::endl;
}
template <class T, std::enable_if_t<std::is_same_v<T, double>>* = 0>
void foo()
{
std::cout << "method 2" << std::endl;
}
int main()
{
foo<int>();
foo<double>();
std::cout << "Done...";
std::getchar();
return EXIT_SUCCESS;
}
The program output is as expected:
method 1
method 2
Done...
I have seen method 2 used more often in stackoverflow, but I prefer method 1.
Are there any circumstances when these two approaches differ?
I have seen method 2 used more often in stackoverflow, but I prefer method 1.
Suggestion: prefer method 2.
Both methods work with single functions. The problem arises when you have more than a function, with the same signature, and you want enable only one function of the set.
Suppose that you want enable foo()
, version 1, when bar<T>()
(pretend it's a constexpr
function) is true
, and foo()
, version 2, when bar<T>()
is false
.
With
template <typename T, typename = std::enable_if_t<true == bar<T>()>>
void foo () // version 1
{ }
template <typename T, typename = std::enable_if_t<false == bar<T>()>>
void foo () // version 2
{ }
you get a compilation error because you have an ambiguity: two foo()
functions with the same signature (a default template parameter doesn't change the signature).
But the following solution
template <typename T, std::enable_if_t<true == bar<T>(), bool> = true>
void foo () // version 1
{ }
template <typename T, std::enable_if_t<false == bar<T>(), bool> = true>
void foo () // version 2
{ }
works, because SFINAE modify the signature of the functions.
Unrelated observation: there is also a third method: enable/disable the return type (except for class/struct constructors, obviously)
template <typename T>
std::enable_if_t<true == bar<T>()> foo () // version 1
{ }
template <typename T>
std::enable_if_t<false == bar<T>()> foo () // version 2
{ }
As method 2, method 3 is compatible with selection of alternative functions with same signature.
In addition to max66's answer, another reason to prefer method 2 is that with method 1, you can (accidentally) pass an explicit type parameter as the second template argument and defeat the SFINAE mechanism completely. This could happen as a typo, copy/paste error, or as an oversight in a larger template mechanism.
#include <cstdlib>
#include <type_traits>
#include <iostream>
// NOTE: foo should only accept T=int
template <class T, class = std::enable_if_t<std::is_same_v<T, int>>>
void foo(){
std::cout << "method 1" << std::endl;
}
int main(){
// works fine
foo<int>();
// ERROR: subsitution failure, as expected
// foo<double>();
// Oops! also works, even though T != int :(
foo<double, double>();
return 0;
}
Live demo here