EXPLORATION 51

image

Partial Template Specialization

Explicit specialization requires you to specify a template argument for every template parameter, leaving no template parameters in the template header. Sometimes, however, you want to specify only some of the template arguments, leaving one or more template parameters in the header. C++ lets you do just that and more, but only for class templates, as this Exploration describes.

Degenerate Pairs

The standard library defines the std::pair<T, U> class template in the <utility> header. This class template is a trivial holder of a pair of objects. The template arguments specify the types of these two objects. Listing 51-1 depicts a common definition of this simple template. (I omitted some members that involve more advanced programming techniques, just to keep this Exploration manageable.)

Listing 51-1.  The std::pair Class Template

template<class T, class U>
struct pair
{
   typedef T first_type;
   typedef U second_type;
   T first;
   U second;
   pair();
   pair(T const& first, U const& second);
   template<class T2, class U2>
   pair(pair<T2, U2> const& other);
};

As you can tell, the pair class template doesn’t do much. The std::map class template can use std::pair to store keys and values. A few functions, such as std::equal_range, return a pair in order to return two pieces of information. In other words, pair is a useful, if dull, part of the standard library.

What happens if T or U is void?

_____________________________________________________________

Although void has popped up here and there, usually as a function’s return type, I haven’t discussed it much. The void type means “no type.” That’s useful for returning from a function, but you cannot declare an object with void type, nor does the compiler permit you to use void for a data member. Thus, pair<int, void> results in an error.

As you start to use templates more and more, you will find yourself in unpredictable situations. A template contains a template, which contains another template, and suddenly you find a template, such as pair, being instantiated with template arguments that you never imagined before, such as void. So let’s add specializations for pair that permit one or two void template arguments, just for the sake of completeness. (The standard permits specializations of library templates only if a template argument is a user-defined type. Therefore, specializing pair for the void type results in undefined behavior. If your compiler is picky, you can copy the definition of pair out of the <utility> header and into your own file, using a different namespace. Then proceed with the experiment. Most readers will be able to work in the std namespace without incurring the wrath of the compiler, at least to complete this exercise.)

Write an explicit specialization for pair<void, void>. It cannot store anything, but you can declare objects of type pair<void, void>. Compare your solution with Listing 51-2.

Listing 51-2.  Specializing pair<> for Two void Arguments

template<>
struct pair<void, void>
{
   typedef void first_type;
   typedef void second_type;
   pair(pair const&) = default;
   pair() = default;
   pair& operator=(pair const&) = default;
};

More difficult is the case of one void argument. You still need a template parameter for the other part of the pair. That calls for partial specialization.

Partial Specialization

When you write a template specialization that involves some, but not all, of the template arguments, it is called partial specialization. Some programmers call explicit specialization full specialization to help distinguish it from partial specialization. Partial specialization is explicit, so the phrase full specialization is more descriptive, and I will use it for the rest of this book.

Begin a partial specialization with a template header that lists the template parameters you are not specializing. Then define the specialization. As with full specialization, name the class that you are specializing by listing all the template arguments. Some of the template arguments depend on the specialization’s parameters, and some are fixed with specific values. That’s what makes this specialization partial.

As with full specialization, the definition of the specialization completely replaces the primary template for a particular set of template arguments. By convention, you keep the same interface, but the actual implementation is up to you.

Listing 51-3 shows a partial specialization of pair if the first template argument is void.

Listing 51-3.  Specializing pair for One void Argument

template<class U>
struct pair<void, U>
{
   typedef void first_type;
   typedef U second_type;
   U second;
   pair() = default;
   pair(pair const&) = default;
   pair(U const& second);
   template<class U2>
   pair(pair<void, U2> const& other);
};

Based on Listing 51-3, write a partial specialization of pair with a void second argument. Compare your solution with Listing 51-4.

Listing 51-4.  Specializing pair for the Other void Argument

template<class T>
struct pair<T, void>
{
   typedef T first_type;
   typedef void second_type;
   T first;
   pair() = default;
   pair(pair const&) = default;
   pair(T const& first);
   template<class T2>
   pair(pair<T2, void> const& other);
};

Regardless of the presence or absence of any partial or full specializations, you still use the pair template the same way: always with two type arguments. The compiler examines those template arguments and determines which specialization to use.

Partially Specializing Function Templates

You cannot partially specialize a function template. Full specialization is allowed, as described in the previous Exploration, but partial specialization is not. Sorry. Use overloading instead, which is usually better than template specialization anyway.

Value Template Parameters

Before I present the next example of partial specialization, I want to introduce a new template feature. Templates typically use types as parameters, but they can also use values. Declare a value template parameter with a type and optional name, much the same way that you would declare a function parameter. Value template parameters are limited to types for which you can specify a compile-time constant: bool, char, int, etc., but floating-point types, string literals, and classes are not allowed.

For example, suppose you want to modify the fixed class that you wrote for Exploration 47 so that the developer can specify the number of digits after the decimal place. While you’re at it, you can also use a template parameter to specify the underlying type, as shown in Listing 51-5.

Listing 51-5.  Changing fixed from a Class to a Class Template

template<class T, int N>
class fixed
{
public:
    typedef T value_type;
    static constexpr int places{N};
    static constexpr int places10{power10(N)};
    fixed();
    fixed(T const& integer, T const& fraction);
    fixed& operator=(fixed const& rhs);
    fixed& operator+=(fixed const& rhs);
    fixed& operator*=(fixed const& rhs);
    // and so on...
private:
    T value_; // scaled to N decimal places
};
 
template<class T, int N>
bool operator==(fixed<T,N> const& a, fixed<T,N> const& b);
template<class T, int N>
fixed<T,N> operator+(fixed<T,N> const& a, fixed<T,N> const& b);
template<class T, int N>
fixed<T,N> operator*(fixed<T,N> const& a, fixed<T,N> const& b);
// ... and so on...

The key challenge in converting the fixed class to a class template is defining places10 in terms of places. C++ has no exponentiation operator, but you can write a constexpr function to compute a simple power of 10. (Another possible solution is to use template trickery, but such an advanced topic will have to wait until Exploration 70.) Recall from the previous Exploration that a constexpr function must have only one statement: a return statement. Another limitation is that you cannot declare any local variables. A common technique, well-known in functional-programming circles, is to use a helper function that takes an additional argument to store the computation in progress. Call the helper function recursively. Here is a situation in which the conditional operator is necessary to halt the recursion. See Listing 51-6 for the power10 function and its helper.

Listing 51-6.  Computing a Power of 10 at Compile Time

/// Called from power10 to compute 10<sup>@p n</sup>, storing the result so far in @p result.
int constexpr power10_helper(int n, int result)
{
  return n == 0 ? result : power10_helper(n - 1, 10 * result);
}
 
/// Compute a power of 10 at compile time. The type T must support subtraction and multiplication.
int constexpr power10(int n)
{
  return power10_helper(n, 1);
}

Suppose you have an application that instantiates fixed<long, 0>. This degenerate case is no different from a plain long, but with overhead and complexity for managing the implicit decimal point. Suppose further that performance measurements of your application reveal that this overhead has a measurable impact on the overall performance of the application. Therefore, you decide to use partial specialization for the case of fixed<T, 0>. Use a partial specialization, so that the template still takes a template argument for the underlying type.

You might wonder why the application programmer doesn’t simply replace fixed<long, 0> with plain long. In some cases, that is the correct solution. Other times, however, the use of fixed<long, 0> might be buried inside another template. The issue, therefore, becomes one of which template to specialize. For the sake of this Exploration, we are specializing fixed.

Remember that any specialization must provide a full implementation. You don’t have to specialize the free functions too. By specializing the fixed class template, we get the performance boost we need. Listing 51-7 shows the partial specialization of fixed.

Listing 51-7.  Specializing fixed for N == 0

template<class T>
class fixed<T, 0>
{
public:
    typedef T value_type;
    static constexpr T places{0};
    static constexpr T places10{1};
    fixed() : value_{} {}
    fixed(T const& integer, T const&) : value_{integer} {}
    fixed& operator=(fixed const& rhs) { value_ = rhs; }
    fixed& operator+=(fixed const& rhs) { value_ += rhs; }
    fixed& operator*=(fixed const& rhs) { value_ *= rhs; }
    ... and so on...
private:
    T value_; // no need for scaling
};

The next Exploration introduces a language feature that helps you manage your custom types: namespaces.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset