EXPLORATION 49

image

Class Templates

A class can be a template , which makes all of its members templates. Every program in this book has used class templates, because much of the standard library relies on templates: the standard I/O streams, strings, vectors, and maps are all class templates. This Exploration takes a look at simple class templates.

Parameterizing a Type

Consider a simple point class, which stores an x and y coordinate. A graphics device driver might use int for the member types.

class point {
public:
   point(int x, int y) : x_{x}, y_{y} {}
   int x() const { return x_; }
   int y() const { return y_; }
private:
   int x_, y_;
};

On the other hand, a calculus tool probably prefers to use double.

class point {
public:
   point(double x, double y) : x_{x}, y_{y} {}
   double x() const { return x_; }
   double y() const { return y_; }
private:
   double x_, y_;
};

Imagine adding much more functionality to the point classes: computing distances between two point objects, rotating one point around another by some angle, etc. The more functionality you dream up, the more you must duplicate in both classes.

Wouldn’t your job be simpler if you could write the point class once and use that single definition for both of these situations and for others not yet dreamed of? Templates to the rescue. Listing 49-1 shows the point class template.

Listing 49-1.  The point Class Template

template<class T>
class point {
public:
   point(T x, T y) : x_{x}, y_{y} {}
   T x() const { return x_; }
   T y() const { return y_; }
   void move_to(T x, T y);           ///< Move to absolute coordinates (x, y)
   void move_by(T x, T y);           ///< Add (x, y) to current position
private:
   T x_, y_;
};
 
template<class T>
void point<T>::move_to(T x, T y)
{
  x_ = x;
  y_ = y;
}

Just as with function templates, the template keyword introduces a class template. The class template is a pattern for making classes, which you do by supplying template arguments; e.g., point<int>.

The member functions of a class template are themselves function templates, using the same template parameters, except that you supply the template arguments to the class, not the function, as you can see in the point<T>::move_to function. Write the move_by member function. Compare your solution with Listing 49-2.

Listing 49-2.  The move_by Member Function

template<class T>
void point<T>::move_by(T x, T y)
{
  x_ += x;
  y_ += y;
}

Every time you use a different template argument, the compiler generates a new class instance, with new member functions. That is, point<int>::move_by is one function, and point<double>::move_by is another, which is exactly what would happen if you had written the functions by hand. If two different source files both use point<int>, the compiler and linker ensure that they share the same template instance.

Parameterizing the rational Class

A simple point class is easy. What about something more complicated, such as the rational class? Suppose someone likes your rational class but wants more precision. You decide to change the type of the numerator and denominator from int to long. Someone else then complains that rational takes up too much memory and asks for a version that uses short as the base type. You could make three copies of the source code, one each for types short, int, and long. Or you could define a class template, as illustrated by the simplified rational class template in Listing 49-3.

Listing 49-3.  The rational Class Template

 1 #ifndef RATIONAL_HPP_
 2 #define RATIONAL_HPP_
 3 template<class T>
 4 class rational
 5 {
 6 public:
 7   typedef T value_type;
 8   rational() : rational{0} {}
 9   rational(T num) : numerator_{num}, denominator_{1} {}
10   rational(T num, T den);
11
13   void assign(T num, T den);
13
14   template<class U>
15   U convert()
16   const
17   {
18     return static_cast<U>(numerator()) / static_cast<U>(denominator());
19   }
20
21   T numerator() const { return numerator_; }
22   T denominator() const { return denominator_; }
23 private:
24   void reduce();
25   T numerator_;
26   T denominator_;
27 };
28
29 template<class T>
30 rational<T>::rational(T num, T den)
31 : numerator_{num}, denominator_{den}
32 {
33   reduce();
34 }
35
36 template<class T>
37 void rational<T>::assign(T num, T den)
38 {
39   numerator_ = num;
40   denominator_ = den;
41   reduce();
42 }
43
44 template<class T>
45 bool operator==(rational<T> const& a, rational<T> const& b)
46 {
47   return a.numerator() == b.numerator() and
48          a.denominator() == b.denominator();
49 }
50
51 template<class T>
52 inline bool operator!=(rational<T> const& a, rational<T> const& b)
53 {
54   return not (a == b);
55 }
56
57 #endif

The typedef of value_type (line 7) is a useful convention. Many class templates that use a template parameter as some kind of subordinate type expose the parameter under a well-defined name. For example, vector<char>::value_type is a typedef for its template parameter, namely, char.

Look at the definition of the constructor on line 29. When you define a member outside of its class template, you have to repeat the template header. The full name of the type includes the template parameter, rational<T> in this case. Inside the class scope, use only the class name, without the template parameter. Also, once the compiler sees the fully qualified class name, it knows it is inside the class scope, and you can also use the template parameter by itself, which you can see in the parameter declarations.

Because the name T is already in use, the convert member function (line 14) needs a new name for its template parameter. U is a common convention, provided you don’t take it too far. More than two or three single-letter parameters, and you start to need more meaningful names, just to help keep straight which parameter goes with which template.

In addition to the class template itself, you have to convert all the free functions that support the rational type to be function templates. Listing 49-3 keeps things simple by showing only operator== and operator!=. Other operators work similarly.

Using Class Templates

Unlike function templates, the compiler cannot deduce the template argument of a class template. This means you must supply the argument explicitly, inside angle brackets.

rational<short> zero{};
rational<int> pi1{355, 113};
rational<long> pi2{80143857L, 25510582L};

Notice anything familiar? Does rational<int> look like vector<int>? All the collection types, such as vector and map, are class templates. The standard library makes heavy use of templates throughout, and you will discover other templates when the time is right.

If a class template takes multiple arguments, separate the arguments with a comma, as in map<long, int>. A template argument can even be another template, such as

std::vector<std::vector<int>> matrix;

Starting with rational.hpp from Listing 49-3, add the I/O operators. (See Listing 35-4 for the non-template versions of the operators.) Write a simple test program that reads rational objects and echoes the values, one per line, to the standard output. Try changing the template argument to different types (short, int, long). Your test program might look something like Listing 49-4.

Listing 49-4.  Simple I/O Test of the rational Class Template

#include <iostream>
#include "rational.hpp"
 
int main()
{
  rational<int> r{};
  while (std::cin >> r)
    std::cout << r << ' ';
}

Now modify the test program to print only nonzero values. The program should look something like Listing 49-5.

Listing 49-5.  Testing rational Comparison Operator

#include <iostream>
#include "rational.hpp"
 
int main()
{
  rational<int> r{};
  while (std::cin >> r)
    if (r != 0)
      std::cout << r << ' ';
}

Remember that with the old rational class, the compiler knew how to construct a rational object from an integer. Thus, it could convert the 0 to rational(0) and then call the overloaded == operator to compare two rational objects. So everything is fine. Right? So why doesn’t it work?

Overloaded Operators

Remember from the previous Exploration that the compiler does not perform automatic type conversion for a function template. That means the compiler will not convert an int to a rational<int>. To solve this problem, you have to add some additional comparison operators, such as

template<class T> bool operator==(rational<T> const& lhs, T rhs);
template<class T> bool operator==(T lhs, rational<T> const& rhs);
template<class T> bool operator!=(rational<T> const&  lhs, T rhs);
template<class T> bool operator!=(T lhs, rational<T> const& rhs);

and so on, for all of the comparison and arithmetic operators. On the other hand, you have to consider whether that’s what you really want. To better understand the limitations of this approach, go ahead and try it. You don’t need all the comparison operators yet, just operator !=, so you can compile the test program. After adding the two new overloaded operator != functions, compile Listing 49-5 again, to be sure it works. Next, compile the test program with a template parameter of long. What happens?

_____________________________________________________________

_____________________________________________________________

Once again, the compiler complains that it can’t find any suitable function for the != operator. The problem is that an overloaded != operator exists for the template parameter, namely, type long, but the type of the literal 0 is int, not long. You can try to solve this problem by defining operators for all the built-in types, but that quickly gets out of hand. So your choices are the following:

  • Define only operators that take two rational arguments. Force the caller to convert arguments to the desired rational type.
  • Define operators in triples: one that takes two rational arguments and two others that mix one rational and one base type (T).
  • Define operators to cover all the bases—for the built-in types (signed char, char, short, int, long), plus some types that I haven’t covered yet. Thus, each operator requires 11 functions.

You might be interested in knowing how the C++ standard library addresses this issue. Among the types in the standard library is a class template, complex, which represents a complex number. The standardization committee opted for the second choice, that is, three overloaded function templates.

template<class T> bool operator==(complex<T> const& a, complex<T> const& b);
template<class T> bool operator==(complex<T> const& a, T const& b);
template<class T> bool operator==(T const& a, complex<T> const& b);

This solution works well enough, and later in the book, you’ll learn techniques to reduce the amount of work involved in defining all these functions.

Another dimension to this problem is the literal 0. Using a literal of type int is fine when you know the base type of rational is also int. How do you express a generic zero for use in a template? The same issue arises when testing for a zero denominator. That was easy when you knew that the type of the denominator was int. When working with templates, you don’t know the type. Recall from Listing 45-6 that the division assignment operator checked for a zero divisor and threw an exception in that case. If you don’t know the type T, how do you know how to express the value zero? You can try using the literal 0 and hope that T has a suitable constructor (single argument of type int). A better solution is to invoke the default constructor for type T, as shown in Listing 49-6.

Listing 49-6.  Invoking a Default Constructor of a Template Parameter

template<class T>
rational<T>& rational<T>::operator/=(rational const& rhs)
{
  if (rhs.numerator() == T{})
    throw zero_denominator("divide by zero");
  numerator_ *= rhs.denominator();
  denominator_ *= rhs.numerator();
  if (denominator_ < T{})
  {
    denominator_ = -denominator_;
    numerator_ = -numerator_;
  }
  reduce();
  return *this;
}

If the type T is a class type, T{} yields an object that is initialized using T’s default constructor. If T is a built-in type, the value of T{} is zero (i.e., 0, 0.0, or false). Initializing the local variables in the input operator is a little trickier.

Mixing Types

As you know, you can assign an int value to a long object or vice versa. It seems reasonable, therefore, that you should be able to assign a rational<int> value to a rational<long> object. Try it. Write a simple program to perform an assignment that mixes base types. Your program might look a little bit like Listing 49-7, but many other programs are equally reasonable.

Listing 49-7.  Trying to Mix rational Base Types

#include "rational.hpp"
 
int main()
{
  rational<int> little{};
  rational<long> big{};
  big = little;
}

What happens when you compile your program?

_____________________________________________________________

_____________________________________________________________

The only assignment operator for the new rational class template is the compiler’s implicit operator. Its parameter type is rational<T> const, so the base type of the source expression must be the same as the base type of the assignment target. You can fix this easily with a member function template. Add the following declaration to the class template:

template<class U>
rational& operator=(rational<U> const& rhs);

Inside the rational class template, the unadorned name, rational, means the same thing as rational<T>. The complete name of the class includes the template argument, so the proper name of the constructor is rational<T>. Because rational means the same as rational<T>, I was able to shorten the constructor name and many other uses of the type name throughout the class template definition. But the assignment operator’s parameter is rational<U>. It uses a completely different template argument. Using this assignment operator, you can freely mix different rational types in an assignment statement.

Write the definition of the assignment operator. Don’t worry about overflow that might result from assigning large values to small. It’s a difficult problem and distracts from the main task at hand, which is practicing writing class templates and function templates. Compare your solution with Listing 49-8.

Listing 49-8.  Defining the Assignment Operator Function Template

template<class T>
template<class U>
rational<T>& rational<T>::operator=(rational<U> const& rhs)
{
  assign(rhs.numerator(), rhs.denominator());
  return *this;
}

The first template header tells the compiler about the rational class template. The next template header tells the compiler about the assignment operator function template. Note that the compiler will be able to deduce the template argument for U from the type of the assignment source (rhs). After adding this operator to the rational class template, you should now be able to make your test program work.

Add a member template constructor that works similarly to the assignment operator. In other words, add to rational a constructor that looks like a copy constructor but isn’t really. A copy constructor copies only objects of the same type, or rational<T>. This new constructor copies rational objects with a different base type, rational<U>. Compare your solution with Listing 49-9.

Listing 49-9.  Defining a Member Constructor Template

template<class T>
template<class U>
rational<T>::rational(rational<U> const& copy)
: numerator_{copy.numerator()}, denominator_{copy.denominator()}
{}

Notice how the template headers stack up. The class template header comes first, followed by the constructor template header. Finish the rational.hpp header by completing all the operators.The new file is too big to include here, but as always, you can download the completed file from the book’s web site.

Programming with templates and type parameters opens a new world of programming power and flexibility. A template lets you write a function or class once and lets the compiler generate actual functions and classes for different template arguments. Sometimes, however, one size does not fit all, and you have to grant exceptions to the rule. The next Exploration takes a look at how you do that by writing template specializations.

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

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