Friday, October 18, 2013

Partial template specialization and boost::enable_if

After dabbling with templates for a while, one question that nearly always comes up among beginning C++ developers is 'how do I partially specialize this template'? Now, unless you happen to be referring to a class or struct template, you're generally out of luck. That is, you cannot partially specialize a method or function template like so:

template< typename A, typename B >
void myMethod( A a, B b )
{}

template< typename A >
void myMethod< A, float >( A a, float b )
{ }

Fortunately, you CAN specialize class templates, e.g.:

template< typename A, typename B >
struct myStruct
{
   static void fn( A a, B b ) { }
}

template< typename A >
struct myStruct< A, float >
{
   static void fn( A a, float b ) { }
}

which means that in many cases you're able to perform your partial specialization by breaking the desired functionality out into a class or struct. So you'd be able to do this:

template< typename A, typename B >
void myMethod( A a, B b )
{
    myStruct< A, B >::fn( a, b );
}

and let the struct deal with the partial specialization.

Now this is all good and well in many cases, but what if you wanted to specialize your template on some more complex, or abstract, predicate than a simple type? Say, do something special for all integral types, or all floating point types? Or something even more complex like one specialization for when a  type is either X, Y or Z, and has a virtual destructor?
Enter boost::enable_if, and perhaps as importantly, Boost's type traits. Beginning with the former, this nearly trivial little construct (in the boost namespace):

template
struct enable_if_c {
  typedef T type;
};

template 
struct enable_if_c {};

among other things lets you specialize your templates on any condition that can be expressed as an "integral constant" (think of it as any integer that is known at compile time). Also, the more generic

template 
struct enable_if : public enable_if_c {};

does exactly the same, but instead of directly taking an integral constant it assumes a template parameter type that has an integral constant member 'value' defined. This allows you to wrap your integral constant logic into classes, and perhaps more importantly to use any of a slew of Boost's extensive collection of 'type traits' classes, such as boost::is_same, boost::is_float, and boost::is_integral.

So how do you actually implement any of this in practical terms? Returning to our previous example, all we really need to do is extend the myStruct template by one more template argument:

template< typename A, typename B, class Enable = void >
struct myStruct
{
   static void fn( A a, B b ) { }
}

Then, a predicate-driven specialization can be created e.g. like so:

template< typename A, typename B >
struct myStruct< A, B, boost::enable_if< boost::is_same< A, B > >::type >
{
   static void fn( A a, A b ) { }
}

So what happened here? Basically, since we added the third template parameter 'Enable' (which obviously could have been called anything, like 'MumboJumbo' or 'C') with a default 'void' argument,  IFF the predicate is_same holds true for some types A and B, the enclosing enable_if template will see a 'true' value member and thus its specialization for 'false' won't trigger, and so its type will be defined, and thus our newly created specialization will trigger (which in this particular instance means we can use A as the type of both arguments to fn()). In any other case - that is, unless A and B are of the same type - is_same will evaluate to 'false', which means enable_if will not have a type member defined and thus the specialization will not be considered.

Let's try something slightly more complex!

template< typename A, typename B >
struct myStruct< A, B, boost::enable_if_c< boost::is_float< A >::value || boost::is_integral< A >::value >::type >
{
   static void fn( A a, B b ) { return a + static_cast< A >( b ); }

}

In this example, I assert that both A and B are floating point or integral types, and so I can safely cast B into A (barring overflowing integral types) and add the results. Note that, in order to be able to apply an OR operation to the underlying integral constants, I've resorted to accessing the raw ::value members of each predicate and passing the resulting integral constant to the enable_if_c template. Another option would have been to use boost::ice_or which is actually recommended for certain older compilers, but since it's hidden in type_traits/detail I'm hesitant to using it in portable and/or future-proof code.


No comments:

Post a Comment