A few weeks ago, I wrote an entry on SFINAE[^], and I mentioned enable_if
. At that point, I had never been able to successfully apply enable_if
. I knew this construct was possible because of SFINAE, however, I was letting the differences between SFINAE and template specialization confuse me. I now have a better understanding and wanted to share my findings.
What Is It?
As I mentioned before, enable_if
is a construct that is made possible because of SFINAE. enable_if
is a meta-function that can help you conditionally remove functions from the overload resolution performed on the type-traits supplied to the function.
You can find examples of its usage in the Standard C++ Library within the containers themselves, such as std::vector
:
template< class _Iter>
typename enable_if< _Is_iterator<_Iter>::value,
void>::type
assign(_Iter _First, _Iter _Last)
{ ... }
void
assign(size_type _Count, const value_type& _Val)
{ ... }
What does the previous block of code accomplish? The assign call will always exist if you want to specify a count and supply a value that is a value_type
. However, the conditional version will only be declared if the parameterized type has a type_trait that evaluates to an iterator-type. This adds flexibility to definition of this function, and allows many different instantiations of this function; as long as the type is an iterator.
Also note that the template definition has a completely different signature than the other non-parameterized version of assign. This is an example of overload resolution. I think it is important to point this out, because I was getting caught up into how it should be used. As I perused the Internet forums, it seems that many others are confused on this point as well.
How It Works
Let's continue to look at the std::vector
. If the parameterized type is not an iterator type, the enable_if
construct will not allow the parameterized type to exist. Because this occurs in the template parameter selection phase, it uses SFINAE to eliminate this function as a candidate to participate in the overloaded selection of assign.
Let's simplify this example even further. Imagine the second version of assign
does not exist; the only definition is the parameterized version that uses enable_if
. Because it is a template, potentially many instantiations of this function will be produced. However, the use of enable_if
ensures that the function will only be generated for types that pass the iterator type-trait test.
This protects the code from attempting to generate the assign function when integers, floats or even user defined object types. Essentially, this usage is a form of concept checking that forces a compiler error if the type is not an iterator. This prevents invalid code with types that appears to compile cleanly from getting a chance to run if it will produce undefined behavior.
Another Example
Previously, when I attempted to use enable_if
, I tried to apply it in a way that reduced to this:
class Sample
{
public:
template< typename = typename std::enable_if< true >::type >
int process()
{
return 1;
}
template< typename = typename std::enable_if< false >::type >
int process()
{
return 0;
}
};
Hopefully, something appears wrong to you in the example above. If we eliminate the template declarations, and the function implementations, this is what remains:
int process()
int process()
When the compiler processes the object, it may not instantiate every function, however, it does catalog every definition. Even though there is a definition for a false case that should not result in the instantiation of that version, it does not affect the overload resolution of the functions. The enable_if
definition needs to contain a type that is dependent on the expression. This line will always be invalid:
Types of Usage
There are four places enable_if
can be used:
Function Return Type
This is a common form. However, it cannot be used with constructors and destructors. Typically, one of the input parameters must be dependent on a parameterized type to enable multiple function signatures. Overload resolution cannot occur on the return-type alone.
template< typename T >
typename std::enable_if< std::is_integral< T >::value, T >::type
gcd(T lhs, T rhs)
{
}
Function Argument
This format looks a little awkward to me. However, it works for all cases except operator overloading.
template< typename T >
T gcd(T lhs, T rhs, typename std::enable_if< std::is_integral< T >::value, T >::type* = 0)
{
}
Class Template Parameter
This example is from cppreference.com, and it indicates that static_assert
may be a better solution that the class template parameter. Nonetheless, here it is:
template< class T, class Enable = void>
class A;
template< class T>
class A< T , typename std::enable_if<std t="">::value >::type> {
};
A< double > a1;
Plate Parameter
This is probably the cleanest and most universal form the enable_if
construct can be used. There are no explicit types for the user to define because the template parameter will be deduced. While the same amount of code is required in this form, the declaration is moved from the function parameter list to the template parameter list.
Personally, I like this form the most.
template< typename T,
typename std::enable_if< std::is_integral< T >::value,
T >::type*=nullptr >
T gcd(T lhs, T rhs)
{
}
Summary
When learning new tools, it is often difficult to see how they are applied. We spent years practicing in math, as we attempted to solve the numerous story problems with each new mathematic abstraction taught to use. Writing code and using libraries is very similar. enable_if
is a construct that is best used to help properly define functions that meet concept criteria for a valid implementation. Think of it as a mechanism to help eliminate invalid overload possibilities rather than a tool to allow the selection between different types and your application of this meta-function should become simpler.