Template Partial Specialization



Item 47. Template Partial Specialization

Let's get it straight: you can't partially specialize function templates. It's just not a part of the C++ language (although it may be some day). What you probably want to do is overload them (see Overloading Function Templates [58, 213]). Accordingly, we are considering only class templates in this item.

The way class template partial specialization works is straightforward. As with complete specialization, you first need a general caseor primary templateto specialize. Let's use our Heap template from Class Template Explicit Specialization [46, 155]:

template <typename T> class Heap;

Explicit specialization (also known colloquially as "complete" specialization) is used to customize a class template for a precise set of arguments. In Class Template Explicit Specialization [46, 155], we used it to provide customized implementations of Heap for const char * and char *. However, we still have a problem with Heaps of other pointer types, in that we'd like to order the Heap according to the values to which the pointer elements refer, rather than the value of the pointers themselves.

Heap<double *> readings; // primary template, T is double *

Because the type (double * ) does not match either of our character pointer complete specializations, the compiler will instantiate the primary template. We could provide complete specializations for double * and every other pointer type of interest, but this is onerous and ultimately unmaintainable. This is a job for partial specialization:

template <typename T>
class Heap<T *> {
  public:
    void push( const T *val );
    T *pop();

    bool empty() const { return h_.empty(); }
  private:
    std::vector<T *> h_;
};

The syntax of a partial specialization is similar to that of a complete specialization, but the template parameter list is not empty. Like a complete specialization, the class template name is a template-id and not a simple template name (see Template Terminology [45, 153]).

This partial specialization for pointers allows us to modify the implementation appropriately. For example, insertions can be made based on the value of the object pointed to, rather than the value of the pointer. First, let's whip up a comparator that compares two pointers by the values of what they point to (see STL Function Objects [20, 71]):

template <typename T>
struct PtrCmp : public std::binary_function<T *, T *, bool> {
    bool operator ()( const T *a, const T *b ) const
        { return *a < *b; }
};

Now let's use our comparator to implement a push operation with the correct behavior:

template <typename T>
void Heap<T *>::push( T *pval ) {
    if( pval ) {
        h_.push_back(pval);
        std::push_heap( h_.begin(), h_.end(), PtrCmp<T>() );
    }
}

Note that, unlike a complete specialization of a class template, a partial specialization is a template, and the template keyword and parameter list are required in definitions of its members.

Unlike our complete specializations, the parameter type of this version of Heap is not completely determined; it's only partially determined to be T *, where T is an unspecified type. That's what makes it a partial specialization. This partial specialization will be preferred to the primary template when instantiating a Heap with any (unqualified) pointer type. Further, the complete specializations of Heap for const char * and char * will be preferred to this partial specialization if the template argument type is const char * or char *.

Heap<std::string> h1; // primary, T is std::string
Heap<std::string *> h2; // partial spec, T is std::string
Heap<int **> h3; // partial spec, T is int *
Heap<char *> h4; // complete spec for char *
Heap<char **> h5; // partial spec, T is char *
Heap<const int *> h6; // partial spec, T is const int
Heap<int (*)()> h7; // partial spec, T is int ()

The complete set of rules for choosing among the various available partial specializations is rather involved, but most cases are straightforward. Generally, the most specific, most restricted candidate is chosen. The partial specialization mechanism is precise and allows us to select among candidates with high precision. For example, we could augment our set of partial specializations with one for pointers to const:

template <typename T>
class Heap<const T *> {
    //...
};
//...
Heap<const int *> h6; // different partial spec, now T is int

Note that, as we discussed in Class Template Explicit Specialization [46, 155], the compiler checks a class template specialization against the declaration of the primary template. If the template arguments match the primary template (in the case of Heap, if there is a single type name argument) the compiler will look for the complete or partial specialization that best matches the template arguments.

Here's a subtle and useful point: A complete or partial specialization of a primary template must be instantiated with the same number and kind of arguments as the primary, but its template parameter list does not have to have the same form as that of the primary. In the case of Heap, the primary takes a single type name parameter, so any complete or partial specialization of Heap must be instantiated with a single type name argument:

template <typename T> class Heap;

Therefore, a complete specialization of Heap still takes a single type name template argument, but the template parameter list differs from that of the primary because it is empty:

template <> class Heap<char *>;

A partial specialization of Heap must also take a single type name template argument, and its template parameter list may have a single type name parameter in its template header

template <typename T> class Heap<T *>;

but it doesn't have to.

template <typename T, int n> class Heap<T [n]>;

This partial specialization will be selected for a specialization of Heap with an array type. For example:

Heap<float *[6]> h8; // partial spec, T is float * and n is 6

Essentially, this partial specialization says, "This partial specialization takes a single type parameter like the primary template, but that parameter must have the form 'array of T of size n.'" Consider some more involved examples:

template <typename R, typename A1, typename A2>
class Heap<R (*)(A1,A2)>;


template <class C, typename T>
class Heap<T C::*>;

With the addition of these partial specializations, we can special case for Heaps of pointers to non-member functions that take two arguments and Heaps of pointers to data members (though why you'd want heaps of these things is anybody's guess):

Heap<char *(*)(int,int)> h9; // partial spec
                             // R is char *, A1 and A2 are int
Heap<std::string Name::*> h10; // partial spec
                               // T is string, C is Name