EXPLORATION 50

image

Template Specialization

The ability to write a template and then use that template multiple times, with different template arguments each time, is one of the great features of C++. Even better is the ability to carve out exceptions to the rule. That is, you can tell the compiler to use a template for most template arguments, except that for certain argument types, it should use a different template definition. This Exploration introduces this feature.

Instantiation and Specialization

Template terminology is tricky. When you use a template, it is known as instantiating the template. A template instanceis a concrete function or class that the compiler creates by applying the template arguments to the template definition. Another name for a template instance is a specialization. Thus, rational<int> is a specialization of the template rational<>.

Therefore, specialization is the realization of a template for a specific set of template arguments. C++ lets you define a custom specialization for one particular set of template arguments; that is, you can create an exception to the rule set down by the template. When you define the specialization instead of letting the compiler instantiate the template for you, it is known as explicit specialization. Thus, a specialization that the compiler creates automatically would be an implicit specialization. (Explicit specialization is also called full specialization, for reasons that will become clear in the next Exploration.) For example, suppose you define a simple class template, point, to represent an (x, y) coordinate and made it into a template to accept any numeric type as the type for x and y. Because some types are large, you decide to pass values by const reference whenever possible, as shown in Listing 50-1.

Listing 50-1.  The point Class Template

template<class T>
class point
{
public:
  typedef T value_type;
  point(T const& x, T const& y) : x_{x}, y_{y} {}
  point() : point{T{}, T{}} {}
  T const& x() const { return x_; }
  T const& y() const { return y_; }
  void move_absolute(T const& x, T const& y) {
    x_ = x;
    y_ = y;
  }
  void move_relative(T const& dx, T const& dy) {
    x_ += dx;
    y_ += dy;
  }
private:
  T x_;
  T y_;
};

The point template works with int, double, rational, and any type that behaves the same way as the built-in numeric types. If you had, say, an arbitrary-precision numeric type, you could use that too, and because such objects are potentially very large, passing by reference is a good choice for default behavior.

On the other hand, point<int> is a fairly common usage, especially in graphical user interfaces. In a mathematical context, point<double> might be more common. In either case, you might decide that passing values by reference is actually wasteful. You can define an explicit specialization for point<int> to pass arguments by value, as shown in Listing 50-2.

Listing 50-2.  The point<int> Specialization

template<>
class point<int>
{
public:
  typedef int value_type;
  point(int x, int y) : x_{x}, y_{y} {}
  point() : point{0, 0} {}
  int x() const { return x_; }
  int y() const { return y_; }
  void move_absolute(int x, int y) {
    x_ = x;
    y_ = y;
  }
  void move_relative(int dx, int dy) {
    x_ += dx;
    y_ += dy;
  }
private:
  int x_;
  int y_;
};

Start an explicit specialization with template<> (notice the empty angle brackets), which tells the compiler that you are writing an explicit specialization. Next is the definition. Notice how the class name is the specialized template name: point<int>. That’s how the compiler knows what you are specializing. Before you can specialize a template, you must tell the compiler about the class template, called the primary template, with a declaration of the class template name or a full definition of the class template. Typically, you would put the class template declaration, followed by its specializations, in a single header, in the right order.

Your explicit specialization completely replaces the template declaration for that template argument (or arguments; if the template takes multiple arguments, you must supply a specific value for each one). Although convention dictates that point<int> should define all the same members as the primary template, point<>, the compiler imposes no such limitation.

Write an explicit specialization for point<double>. Add a debugging statement to the primary template and to the specialization so you can prove to yourself that the compiler really does choose the specialization. Write a main program to use point<double> and point<short>, and check that the correct debugging statements execute. Listing 50-3 shows how the program looks when I write it.

Listing 50-3.  Specializing point<double> and Testing the Code

#include <iostream>
 
template<class T>
class point
{
public:
  typedef T value_type;
  point(T const& x, T const& y) : x_{x}, y_{y} {}
  point() : point{T{}, T{}} { std::cout << "point<>() "; }
  T const& x() const { return x_; }
  T const& y() const { return y_; }
  void move_absolute(T const& x, T const& y) {
    x_ = x;
    y_ = y;
  }
  void move_relative(T const& dx, T const& dy) {
    x_ += dx;
    y_ += dy;
  }
private:
  T x_;
  T y_;
};
 
template<>
class point<double>
{
public:
  typedef double value_type;
  point(double  x, double y) : x_(x), y_(y) {}
  point() : point{0.0, 0.0} { std::cout << "point<double> specialization "; }
  double x() const { return x_; }
  double y() const { return y_; }
  void move_absolute(double x, double y) {
    x_ = x;
    y_ = y;
  }
  void move_relative(double dx, double dy) {
    x_ += dx;
    y_ += dy;
  }
private:
  double x_;
  double y_;
};
 
int main()
{
  point<short> s;
  point<double> d;
  s.move_absolute(10, 20);
  d.move_absolute(1.23, 4.56);
}

If you really want to get fancy, include the <typeinfo> header, and, in the primary template, call typeid(T).name() to obtain a string that describes the type T. The exact contents of the string depend on the implementation. The typeid keyword returns a typeinfo object (defined in <typeinfo>) that describes a type, or you can apply the keyword to an expression to obtain information on the expression’s type. You can’t do much with a typeinfo object. It’s not a reflection mechanism, but you can call the name() member function to get an implementation-defined name. This is a handy debugging technique when you have a complicated template situation, and you aren’t quite sure what the compiler thinks the template argument is. Thus, write the constructor as follows:

point() : x_(), y_() { std::cout << "point<" << typeid(T).name() << ">()
"; }

One compiler I have prints

point<short>()
point<double> specialization

Another prints

point<s>()
point<double> specialization

Custom Comparators

The map container lets you provide a custom comparator. The default behavior is for map to use a template class, std::less<>, which is a functor that uses the < operator to compare keys. If you want to store a type that cannot be compared with <, you can specialize std::less for your type. For example, suppose you have a person class, which stores a person’s name, address, and telephone number. You want to store a person in a map, ordered by name. All you need to do is write a template specialization, std::less<person>, as shown in Listing 50-4.

Listing 50-4.  Specializing std::less to Compare person Objects by Name

#include <functional>
#include <iostream>
#include <map>
#include <string>
 
class person {
public:
   person() : name_{}, address_{}, phone_{} {}
   person(std::string const& name,
          std::string const& address,
          std::string const& phone)
   : name_{name}, address_{address}, phone_{phone}
   {}
   std::string const& name()    const { return name_; }
   std::string const& address() const { return address_; }
   std::string const& phone()   const { return phone_; }
private:
   std::string name_, address_, phone_;
};
 
namespace std {
   template<>
   struct less<person> {
      bool operator()(person const& a, person const& b) const {
         return a.name() < b.name();
      }
   };
}
 
int main()
{
   std::map<person, int> people;
   people[person{"Ray", "123 Erewhon", "555-5555"}] = 42;
   people[person{"Arthur", "456 Utopia", "123-4567"}]= 10;
   std::cout << people.begin()->first.name() << ' ';
}

You are allowed to specialize templates that are defined in the std namespace, but you cannot add new declarations to std. The std::less template is declared in the <functional> header. This header defines comparator templates for all the relational and equality operators, and some more besides. Consult a language reference for details. What matters right now is what the std::less primary template looks like, that is, the primary template that C++ uses when it cannot find an explicit specialization (such as std::less<person>). Write the definition of a class template, less, that would serve as a primary template to compare any comparable objects with the < operator. Compare your solution with Listing 50-5.

Listing 50-5.  The Primary std::less Class Template

template<class T>
struct less
{
   bool operator()(T const& a, T const& b) const { return a < b; }
};

Take a peek into your standard library’s <functional> header. It might be in another file that <functional> includes, and it might be more complicated than Listing 50-5, but you should be able to find something that you can recognize and understand. (For example, the C++ standard also mandates member typedefs for the argument and return type.)

Specializing Function Templates

You can specialize a function template, but you should prefer overloading to templates. For example, let’s keep working with the template form of absval (Exploration 48). Suppose you have an arbitrary-precision integer class, integer, and it has an efficient absolute value function (that is, it simply clears the sign bit, so there’s no need to compare the value with zero). Instead of the template form of absval, you want to use the efficient method for taking the absolute value of integer. Although C++ permits you to specialize the absval<> function template, a better solution is to override the absval function (not template).

integer absval(integer i)
{
   i.clear_sign_bit();
   return i;
}

When the compiler sees a call to absval, it examines the type of the argument. If the type matches the parameter type used in a non-template function, the compiler arranges to call that function. If it can’t match the argument type with the parameter type, it checks template functions. The precise rules are complicated, and I will discuss them later in the book. For now, just remember that the compiler prefers non-template functions to template functions, but it will use a template function instead of a non-template function if it can’t find a good match between the argument types and the parameter types of the non-template function.

Sometimes, however, you have to write a template function, even if you just want to overload the absval function. For example, suppose you want to improve the absolute value function for the rational<T> class template. There is no need to compare the entire value with zero; just compare the numerator, and avoid unnecessary multiplications.

template<class T>
rational<T> absval(rational<T> const& r)
{
  if (r.numerator() < 0) // to avoid unnecessary multiplications in operator<
    return -r;
  else
    return r;
}

When you call absval, pass it an argument in the usual way. If you pass an int, double, or other built-in numeric type, the compiler instantiates the original function template. If you pass an integer object, the compiler calls the overloaded non-template function, and if you pass a rational object, the compiler instantiates the overloaded function template.

Traits

Another use of specialization is to define a template that captures the characteristics, or traits, of a type. You’ve already seen one example of a traits template: std::numeric_limits. The <limits> header defines a class template named std::numeric_limits. The primary template is rather dull, saying that the type has zero digits of precision, a radix of zero, and so on. The only way this template makes any sense is to specialize it. Thus, the <limits> header also defines explicit specializations of the template for all the built-in types. Thus, you can discover the smallest int by calling std::numeric_limits<int>::min() or determine the floating-point radix of double with std::numeric_limits<double>::radix, and so on. Every specialization declares the same members, but with values that are particular to the specialization. (Note that the compiler does not enforce the fact that every specialization declares the same members. The C++ standard mandates this requirement for numeric_limits, and it is up to the library author to implement the standard correctly, but the compiler provides no help.)

You can define your own specialization when you create a numeric type, such as rational. Defining a template of a template involves some difficulties that I will cover in the next Exploration, so for now, go back to Listing 46-10 and the old-fashioned non-template rational class, which hard-coded int as the base type. Listing 50-6 shows how to specialize numeric_limits for this rational class.

Listing 50-6.  Specializing numeric_limits for the rational Class

namespace std {
template<>
class numeric_limits<rational>
{
public:
  static constexpr bool is_specialized{true};
  static constexpr rational min() noexcept { return rational(numeric_limits<int>::min()); }
  static constexpr rational max() noexcept { return rational(numeric_limits<int>::max()); }
  static rational lowest() noexcept { return -max(); }
  static constexpr int digits{ 2 * numeric_limits<int>::digits };
  static constexpr int digits10{ numeric_limits<int>::digits10 };
  static constexpr int max_digits10{ numeric_limits<int>::max_digits10 };
  static constexpr bool is_signed{ true };
  static constexpr bool is_integer{ false };
  static constexpr bool is_exact{ true };
  static constexpr int radix{ 2 };
  static constexpr bool is_bounded{ true };
  static constexpr bool is_modulo{ false };
  static constexpr bool traps{ std::numeric_limits<int>::traps };
 
  static rational epsilon() noexcept
     { return rational(1, numeric_limits<int>::max()-1); }
  static rational round_error() noexcept
     { return rational(1, numeric_limits<int>::max()); }
 
  // The following are meaningful only for floating-point types.
  static constexpr int min_exponent{ 0 };
  static constexpr int min_exponent10{ 0 };
  static constexpr int max_exponent{ 0 };
  static constexpr int max_exponent10{ 0 };
  static constexpr bool has_infinity{ false };
  static constexpr bool has_quiet_NaN{ false };
  static constexpr bool has_signaling_NaN{ false };
  static constexpr float_denorm_style has_denorm {denorm_absent};
  static constexpr bool has_denorm_loss {false};
  // The following are meant only for floating-point types, but you have
  // to define them, anyway, even for nonfloating-point types. The values
  // they return do not have to be meaningful.
  static constexpr rational infinity() noexcept { return max(); }
  static constexpr rational quiet_NaN() noexcept { return rational(); }
  static constexpr rational signaling_NaN() noexcept { return rational(); }
  static constexpr rational denorm_min() noexcept { return rational(); }
  static constexpr bool is_iec559{ false };
  static constexpr bool tinyness_before{ false };
  static constexpr float_round_style round_style{ round_toward_zero };
};
 
} // namespace std

This example has a few new things. They aren’t important right now, but in C++, you have to get all the tiny details right, or the compiler voices its stern disapproval. The first line, which starts namespace std, is how you put names in the standard library. You are not allowed to add new names to the standard library (although the standard does not require a compiler to issue an error message if you break this rule), but you are allowed to specialize templates that the standard library has already defined. Notice the opening curly brace for the namespace, which has a corresponding closing curly brace on the last line of the listing. (This topic will be covered in more depth in Exploration 52.)

The member functions all have noexcept between their names and bodies. This tells the compiler that the function does not throw any exceptions (recall from Exploration 45). If the function does actually throw an exception at runtime, the program terminates unceremoniously, without calling any destructors. (This topic is covered in more depth in Exploration 60.)

The constexpr specifier is similar to const, but it tells the compiler that the value is a compile-time constant. In order for a function to be constexpr, the compiler imposes a number of restrictions. In particular, the function body must be a single return statement and no other statements. Any functions that it calls must also be constexpr. Function parameter and return type must be built-in or types that can be constructed with constexpr constructors. A constexpr function cannot call a non-constexpr function. If any restriction is violated, the function cannot be declared constexpr. Thus, the gcd() function cannot be constexpr, so reduce( ) cannot be constexpr, so the two-argument constructor cannot be constexpr. The value of being able to write a function that is called at compile time is extremely useful, and we will return to constexpr in the future.

image Tip  When writing a template for the first time, start with a non-template version. It is much easier to debug a non-template function or class. Once you get the non-template version working, then change it into a template.

Template specialization has many other uses, but before we get carried away, the next Exploration takes a look at a particular kind of specialization, where your specialization still requires template parameters, called partial specialization.

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

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