Section 16.6. Template Specializations


16.6. Template Specializations

The rest of this chapter covers a somewhat advanced topic. It can be safely skipped on first reading.



It is not always possible to write a single template that is best suited for every possible template argument with which the template might be instantiated. In some cases, the general template definition is simply wrong for a type. The general definition might not compile or might do the wrong thing. At other times, we may be able to take advantage of some specific knowledge about a type to write a more efficient function than the one that is instantiated from the template.

Our compare function and our Queue class are both good examples of the problem: Neither works correctly when used with C-style character strings. Let's look again at our compare function template:

      template <typename T>      int compare(const T &v1, const T &v2)      {         if (v1 < v2) return -1;         if (v2 < v1) return 1;         return 0;      } 

If we call this template definition on two const char* arguments, the function compares the pointer values. It will tell us the relative positions in memory of these two pointers but says nothing about the contents of the arrays to which the pointers point.

To get be able to use compare with character strings, we would have to provide a specialized definition that knows how to compare C-style strings. The fact that these versions are specialized is transparent to users of these templates. Calls to a specialized function or use of a specialized class are indistinguishable from uses of a version instantiated from the general template.

16.6.1. Specializing a Function Template

A template spacialization is a separate definition in which the actual type(s) or value(s) of one or more template parameter(s) is (are) specified. The form of a specialization is:

  • The keyword template followed by an empty bracket pair (<>),

  • followed by the template name and a bracket pair specifying the template parameters(s) that this specialization defines,

  • the function parameter list,

  • and the function body.

The following program defines a specialization of compare when the template parameter type is bound to const char*:

      // special version of compare to handle C-style character strings      template <>      int compare<const char*>(const char* const &v1,                               const char* const &v2)      {          return strcmp(v1, v2);      } 

The declaration for the specialization must match that of the corresponding template. In this case, the template has one type parameter and two function parameters. The function parameters are const references to the type parameter. Here we are fixing the type parameter to const char*; our function parameters, therefore, are const references to a const char*.

Now when we call compare, passing it two character pointers, the compiler will call our specialized version. It will call the generic version for any other argument types (including plain char*):

      const char *cp1 = "world", *cp2 = "hi";      int i1, i2;      compare(cp1, cp2); // calls the specialization      compare(i1, i2);   // calls the generic version instantiated with int 

Declaring a Template Specialization

As with any function, we can declare a function template specialization without defining it. A template specialization declaration looks like the definition but omits the function body:

      // declaration of function template explicit specialization      template<>      int compare<const char*>(const char* const&,                               const char* const&); 

This declaration consists of an empty template parameter list (template<>) followed by the return type, the function name (optionally) followed by explicit template argument(s) specified inside a pair of angle brackets, and the function parameter list. A template specialization must always include the empty template parameter specifier, template<>, and it must include the function parameter list. If the template arguments can be inferred from the function parameter list, there is no need to explicitly specify the template arguments:

      // error: invalid specialization declarations      // missing template<>      int compare<const char*>(const char* const&,                               const char* const&);      // error: function parameter list missing      template<> int compare<const char*>;      // ok: explicit template argument const char* deduced from parameter types      template<> int compare(const char* const&,                             const char* const&); 

Function Overloading versus Template Specializations

Omitting the empty template parameter list, template<>, on a specialization may have surprising effects. If the specialization syntax is missing, then the effect is to declare an overloaded nontemplate version of the function:

      // generic template definition      template <class T>      int compare(const T& t1, const T& t2) { /* ... */ }      // OK: ordinary function declaration      int compare(const char* const&, const char* const&); 

The definition of compare does not define a template specialization. Instead, it declares an ordinary function with a return type and a parameter list that could match those of a template instantiation.

We'll look at the interaction of overloading and templates in more detail in the next section. For now, what's important to know is that when we define a nontemplate function, normal conversions are applied to the arguments. When we specialize a template, conversions are not applied to the argument types. In a call to a specialized version of a template, the argument type(s) in the call must match the specialized version function parameter type(s) exactly. If they don't, then the compiler will instantiate an instantiation for the argument(s) from the template definition.

Duplicate Definitions Cannot Always Be Detected

If a program consists of more than one file, the declaration for a template specialization must be visible in every file in which the specialization is used. A function template cannot be instantiated from the generic template definition in some files and be specialized for the same set of template arguments in other files.

As with other function declarations, declarations for template specializations should be included in a header file. That header should then be included in every source file that uses the specialization.



Ordinary Scope Rules Apply to Specializations

Before we can declare or define a specialization, a declaration for the template that it specializes must be in scope. Similarly, a declaration for the specialization must be in scope before that version of the template is called:

      // define the general compare template      template <class T>      int compare(const T& t1, const T& t2) { /* ... */ }      int main() {          // uses the generic template definition          int i = compare("hello", "world");          // ...      }      // invalid program: explicit specialization after call      template<>      int compare<const char*>(const char* const& s1,                               const char* const& s2)      { /* ... */ } 

This program is in error because a call that would match the specialization is made before the specialization is declared. When the compiler sees a call, it must know to expect a specialization for this version. Otherwise, the compiler is allowed to instantiate the function from the template definition.

A program cannot have both an explicit specialization and an instantiation for the same template with the same set of template arguments.



It is an error for a specialization to appear after a call to that instance of the template has been seen.

Exercises Section 16.6.1

Exercise 16.52:

Define a function template count to count the number of occurrences of some value in a vector.

Exercise 16.53:

Write a program to call the count function defined in the previous exercise passing it first a vector of doubles, then a vector of ints, and finally a vector of chars.

Exercise 16.54:

Introduce a specialized template instance of the count function to handle strings. Rerun the program you wrote to call the function template instantiations.


16.6.2. Specializing a Class Template

Our Queue class has a problem similar to the one in compare when used with C-style strings. In this case, the problem is in the push function. That function copies the value it's given to create a new element in the Queue. By default, copying a C-style character string copies only the pointer, not the characters. Copying a pointer in this case has all the problems that shared pointers have in other contexts. The most serious is that if the pointer points to dynamic memory, it's possible for the user to delete the array to which the pointer points.

Defining a Class Specialization

One way to provide the right behavior for Queue's of C-style strings is to define a specialized version of the entire class for const char*:

      /* definition of specialization for const char*       * this class forwards its work to Queue<string>;       * the push function translates the const char* parameter to a string       * the front functions return a string rather than a const char*       */      template<> class Queue<const char*> {      public:          // no copy control: Synthesized versions work for this class          // similarly, no need for explicit default constructor either          void push(const char*);          void pop()                  {real_queue.pop();}          bool empty() const          {return real_queue.empty();}          // Note: return type does not match template parameter type          std::string front()         {return real_queue.front();}          const std::string &front() const                                      {return real_queue.front();}      private:          Queue<std::string> real_queue; // forward calls to real_queue      }; 

This implementation gives Queue a single data element: a Queue of strings. The various members delegate their work to this memberfor example, pop is implemented by calling pop on real_queue.

This version of the class does not define the copy-control members. Its only data element has a class type that does the right thing when copied, assigned, or destroyed; we can use the synthesized copy-control members.

Our Queue class implements mostly, but not entirely, the same interface as the template version of Queue. The difference is that we return a string rather than a char* from the front members. We do so to avoid having to manage the character array that would be required if we wanted to return a pointer.

It is worth noting that a specialization may define completely different members than the template itself. If a specialization fails to define a member from the template, that member may not be used on objects of the specilization type. The member definitions of the class template are not used to create the definitions for the members of an explicit specialization.

A class template specialization ought to define the same interface as the template it specializes. Doing otherwise will surprise users when they attempt to use a member that is not defined.



Class Specialization Definition

When a member is defined outside the class specialization, it is not preceded by the tokens template<>.



Our class defines only one member outside the class:

      void Queue<const char*>::push(const char* val)      {          return real_queue.push(val);      } 

Although it does little obvious work, this function implicitly copies the character array to which val points. The copy is made in the call to real_queue.push, which creates a new string from the const char* argument. That argument uses the string constructor that takes a const char*. The string constructor copies the characters from the array pointed to by val into an unnamed string that will be stored in the element we push onto real_queue.

Exercises Section 16.6.2

Exercise 16.55:

The comments on the specialized version of Queue for const char* note that there is no need to define the default constructor or copy-control members. Explain why the synthesized members suffice for this version of Queue.

Exercise 16.56:

We explained the generic behavior of Queue if it is not specialized for const char*. Using the generic Queue template, explain what happens in the following code:

      Queue<const char*> q1;      q1.push("hi"); q1.push("bye"); q1.push("world");      Queue<const char*> q2(q1); // q2 is a copy of q1      Queue<const char*> q3;     // empty Queue      q1 = q3; 

In particular, say what the values of q1 and q2 are after the initialization of q2 and after the assignment to q3.

Exercise 16.57:

Our specialized Queue returns strings from the front function rather than const char*. Why do you suppose we did so? How might you implement the Queue to return a const char*? Discuss the pros and cons of each approach.


16.6.3. Specializing Members but Not the Class

If we look a bit more deeply at our class, we can see that we can simplify our code: Rather than specializing the whole template, we can specialize just the push and pop members. We'll specialize push to copy the character array and pop to free the memory we used for that copy:

      template <>      void Queue<const char*>::push(const char *const &val)      {           // allocate a new character array and copy characters from val           char* new_item = new char[strlen(val) + 1];           strncpy(new_item, val, strlen(val) + 1);           // store pointer to newly allocated and initialized element           QueueItem<const char*> *pt =               new QueueItem<const char*>(new_item);           // put item onto existing queue           if (empty())               head = tail = pt; // queue has only one element           else {               tail->next = pt;  // add new element to end of queue               tail = pt;           }      }      template <>      void Queue<const char*>::pop()      {           // remember head so we can delete it           QueueItem<const char*> *p = head;           delete head->item; // delete the array allocated in push           head = head->next; // head now points to next element           delete p;          // delete old head element      } 

Now, the class type Queue<const char*> will be instantiated from the generic class template definition, with the exception of the push and pop functions. When we call push or pop on a Queue<const char*>, then the specialized version will be called. When we use any other member, the generic one will be instantiated for const char* from the class template.

Specialization Declarations

Member specializations are declared just as any other function template specialization. They must start with an empty template parameter list:

      // push and pop specialized for const char*      template <>      void Queue<const char*>::push(const char* const &);      template <> void Queue<const char*>::pop(); 

These declarations should be placed in the Queue header file.

Exercises Section 16.6.3

Exercise 16.58:

The specialization of Queue presented in the previous subsection and the specialization in this subsection of push and pop apply only to Queues of const char*. Implement these two different ways of specializing Queue that could be used with plain char*.

Exercise 16.59:

If we go the route of specializing only the push function, what value is returned by front for a Queue of C-style character strings?

Exercise 16.60:

Discuss the pros and cons of the two designs: defining a specialized version of the class for const char* versus specializing only the push and pop functions. In particular, compare and contrast the behavior of front and the possibility of errors in user code corrupting the elements in the Queue.


16.6.4. Class-Template Partial Specializations

If a class template has more than one template parameter, we might want to specialize some but not all of the template parameters. We can do so using a class template partial specialization:

      template <class T1, class T2>      class some_template {          // ...      };      // partial specialization: fixes T2 as int and allows T1 to vary      template <class T1>      class some_template<T1, int> {          // ...      }; 

A class template partial specialization is itself a template. The definition of a partial specialization looks like a template definition. Such a definition begins with the keyword template followed by a template parameter list enclosed by angle brackets (<>). The parameter list of a partial specialization is a subset of the parameter list of the corresponding class template definition. The partial specialization for some_template has only one template type parameter named T1. The second template argument for T2 is known to be int. The template parameter list for the partial specialization only lists the parameters for which the template arguments are still unknown.

Using a Class-Template Partial Specialization

The partial specialization has the same name as the class template to which it correspondsnamely, some_template. The name of the class template must be followed by a template argument list. In the previous example, the template argument list is <T1,int>. Because the argument value for the first template parameter is unknown, the argument list uses the name of the template parameter T1 as a placeholder. The other argument is the type int, for which the template is partially specialized.

As with any other class template, a partial specialization is instantiated implicitly when used in a program:

      some_template<int, string> foo; // uses template      some_template<string, int> bar; // uses partial specialization 

Notice that the type of the second variable, some_template parameterized by string and int, could be instantiated from the generic class template definition as well as from the partial specialization. Why is it that the partial specialization is chosen to instantiate the template? When a parital specialization is declared, the compiler chooses the template definition that is the most specialized for the instantiation. When no partial specialization can be used, the generic template definition is used. The instantiated type of foo does not match the partial specialization provided. Thus, the type of foo must be instantiated from the general class template, binding int to T1 and string to T2. The partial specialization is only used to instantiate some_template types with a second type of int.

The definition of a partial specialization is completely disjointed from the definition of the generic template. The partial specialization may have a completely different set of members from the generic class template. The generic definitions for the members of a class template are never used to instantiate the members of the class template partial specialization.



C++ Primer
C Primer Plus (5th Edition)
ISBN: 0672326965
EAN: 2147483647
Year: 2006
Pages: 223
Authors: Stephen Prata

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net