16.1.2. Class Templates

Image

A class template is a blueprint for generating classes. Class templates differ from function templates in that the compiler cannot deduce the template parameter type(s) for a class template. Instead, as we’ve seen many times, to use a class template we must supply additional information inside angle brackets following the template’s name (§ 3.3, p. 97). That extra information is the list of template arguments to use in place of the template parameters.

Defining a Class Template

As an example, we’ll implement a template version of StrBlob12.1.1, p. 456). We’ll name our template Blob to indicate that it is no longer specific to strings. Like StrBlob, our template will provide shared (and checked) access to the elements it holds. Unlike that class, our template can be used on elements of pretty much any type. As with the library containers, our users will have to specify the element type when they use a Blob.

Like function templates, class templates begin with the keyword template followed by a template parameter list. In the definition of the class template (and its members), we use the template parameters as stand-ins for types or values that will be supplied when the template is used:

template <typename T> class Blob {
public:
    typedef T value_type;
    typedef typename std::vector<T>::size_type size_type;
    // constructors
    Blob();
    Blob(std::initializer_list<T> il);
    // number of elements in the Blob
    size_type size() const { return data->size(); }
    bool empty() const { return data->empty(); }
    // add and remove elements
    void push_back(const T &t) {data->push_back(t);}
    // move version; see § 13.6.3 (p. 548)
    void push_back(T &&t) { data->push_back(std::move(t)); }
    void pop_back();
    // element access
    T& back();
    T& operator[](size_type i); // defined in § 14.5 (p. 566)
private:
    std::shared_ptr<std::vector<T>> data;
    // throws msg if data[i] isn't valid
    void check(size_type i, const std::string &msg) const;
};

Our Blob template has one template type parameter, named T. We use the type parameter anywhere we refer to the element type that the Blob holds. For example, we define the return type of the operations that provide access to the elements in the Blob as T&. When a user instantiates a Blob, these uses of T will be replaced by the specified template argument type.

With the exception of the template parameter list, and the use of T instead of string, this class is the same as the version we defined in § 12.1.1 (p. 456) and updated in § 12.1.6 (p. 475) and in Chapters 13 and 14.

Instantiating a Class Template

As we’ve seen many times, when we use a class template, we must supply extra information. We can now see that that extra information is a list of explicit template arguments that are bound to the template’s parameters. The compiler uses these template arguments to instantiate a specific class from the template.

For example, to define a type from our Blob template, we must provide the element type:

Blob<int> ia;                // empty Blob<int>
Blob<int> ia2 = {0,1,2,3,4}; // Blob<int> with five elements

Both ia and ia2 use the same type-specific version of Blob (i.e., Blob<int>). From these definitions, the compiler will instantiate a class that is equivalent to

template <> class Blob<int> {
    typedef typename std::vector<int>::size_type size_type;
    Blob();
    Blob(std::initializer_list<int> il);
    // ...
    int& operator[](size_type i);
private:
    std::shared_ptr<std::vector<int>> data;
    void check(size_type i, const std::string &msg) const;
};

When the compiler instantiates a class from our Blob template, it rewrites the Blob template, replacing each instance of the template parameter T by the given template argument, which in this case is int.

The compiler generates a different class for each element type we specify:

// these definitions instantiate two distinct Blob types
Blob<string> names; // Blob that holds strings
Blob<double> prices;// different element type

These definitions would trigger instantiations of two distinct classes: The definition of names creates a Blob class in which each occurrence of T is replaced by string. The definition of prices generates a Blob with T replaced by double.


Image Note

Each instantiation of a class template constitutes an independent class. The type Blob<string> has no relationship to, or any special access to, the members of any other Blob type.


References to a Template Type in the Scope of the Template
Image

In order to read template class code, it can be helpful to remember that the name of a class template is not the name of a type (§ 3.3, p. 97). A class template is used to instantiate a type, and an instantiated type always includes template argument(s).

What can be confusing is that code in a class template generally doesn’t use the name of an actual type (or value) as a template argument. Instead, we often use the template’s own parameter(s) as the template argument(s). For example, our data member uses two templates, vector and shared_ptr. Whenever we use a template, we must supply template arguments. In this case, the template argument we supply is the same type that is used to instantiate the Blob. Therefore, the definition of data

std::shared_ptr<std::vector<T>> data;

uses Blob’s type parameter to say that data is the instantiation of shared_ptr that points to the instantiation of vector that holds objects of type T. When we instantiate a particular kind of Blob, such as Blob<string>, then data will be

shared_ptr<vector<string>>

If we instantiate Blob<int>, then data will be shared_ptr<vector<int>>, and so on.

Member Functions of Class Templates

As with any class, we can define the member functions of a class template either inside or outside of the class body. As with any other class, members defined inside the class body are implicitly inline.

A class template member function is itself an ordinary function. However, each instantiation of the class template has its own version of each member. As a result, a member function of a class template has the same template parameters as the class itself. Therefore, a member function defined outside the class template body starts with the keyword template followed by the class’ template parameter list.

As usual, when we define a member outside its class, we must say to which class the member belongs. Also as usual, the name of a class generated from a template includes its template arguments. When we define a member, the template argument(s) are the same as the template parameter(s). That is, for a given member function of StrBlob that was defined as

ret-type StrBlob::member-name(parm-list)

the corresponding Blob member will look like

template <typename T>
ret-type Blob<T>::member-name(parm-list)

The check and Element Access Members

We’ll start by defining the check member, which verifies a given index:

template <typename T>
void Blob<T>::check(size_type i, const std::string &msg) const
{
    if (i >= data->size())
        throw std::out_of_range(msg);
}

Aside from the differences in the class name and the use of the template parameter list, this function is identical to the original StrBlob member.

The subscript operator and back function use the template parameter to specify the return type but are otherwise unchanged:

template <typename T>
T& Blob<T>::back()
{
    check(0, "back on empty Blob");
    return data->back();
}
template <typename T>
T& Blob<T>::operator[](size_type i)
{
    // if i is too big, check will throw, preventing access to a nonexistent element
    check(i, "subscript out of range");
    return (*data)[i];
}

In our original StrBlob class these operators returned string&. The template versions will return a reference to whatever type is used to instantiate Blob.

The pop_back function is nearly identical to our original StrBlob member:

template <typename T> void Blob<T>::pop_back()
{
    check(0, "pop_back on empty Blob");
    data->pop_back();
}

The subscript operator and back members are overloaded on const. We leave the definition of these members, and of the front members, as an exercise.

Blob Constructors

As with any other member defined outside a class template, a constructor starts by declaring the template parameters for the class template of which it is a member:

template <typename T>
Blob<T>::Blob(): data(std::make_shared<std::vector<T>>()) { }

Here we are defining the member named Blob in the scope of Blob<T>. Like our StrBlob default constructor (§ 12.1.1, p. 456), this constructor allocates an empty vector and stores the pointer to that vector in data. As we’ve seen, we use the class’ own type parameter as the template argument of the vector we allocate.

Similarly, the constructor that takes an initializer_list uses its type parameter T as the element type for its initializer_list parameter:

template <typename T>
Blob<T>::Blob(std::initializer_list<T> il):
              data(std::make_shared<std::vector<T>>(il)) { }

Like the default constructor, this constructor allocates a new vector. In this case, we initialize that vector from the parameter, il.

To use this constructor, we must pass an initializer_list in which the elements are compatible with the element type of the Blob:

Blob<string> articles = {"a", "an", "the"};

The parameter in this constructor has type initializer_list<string>. Each string literal in the list is implicitly converted to string.

Instantiation of Class-Template Member Functions

By default, a member function of a class template is instantiated only if the program uses that member function. For example, this code

// instantiates Blob<int> and the initializer_list<int> constructor
Blob<int> squares = {0,1,2,3,4,5,6,7,8,9};
// instantiates Blob<int>::size() const
for (size_t i = 0; i != squares.size(); ++i)
    squares[i] = i*i; // instantiates Blob<int>::operator[](size_t)

instantiates the Blob<int> class and three of its member functions: operator[], size, and the initializer_list<int> constructor.

If a member function isn’t used, it is not instantiated. The fact that members are instantiated only if we use them lets us instantiate a class with a type that may not meet the requirements for some of the template’s operations (§ 9.2, p. 329).


Image Note

By default, a member of an instantiated class template is instantiated only if the member is used.


Simplifying Use of a Template Class Name inside Class Code

There is one exception to the rule that we must supply template arguments when we use a class template type. Inside the scope of the class template itself, we may use the name of the template without arguments:

// BlobPtr throws an exception on attempts to access a nonexistent element
template <typename T> class BlobPtr
public:
    BlobPtr(): curr(0) { }
    BlobPtr(Blob<T> &a, size_t sz = 0):
            wptr(a.data), curr(sz) { }
    T& operator*() const
    { auto p = check(curr, "dereference past end");
      return (*p)[curr];  // (*p) is the vector to which this object points
    }
    // increment and decrement
    BlobPtr& operator++();        // prefix operators
    BlobPtr& operator--();
private:
    // check returns a shared_ptr to the vector if the check succeeds
    std::shared_ptr<std::vector<T>>
        check(std::size_t, const std::string&) const;
    // store a weak_ptr, which means the underlying vector might be destroyed
    std::weak_ptr<std::vector<T>> wptr;
    std::size_t curr;      // current position within the array
};

Careful readers will have noted that the prefix increment and decrement members of BlobPtr return BlobPtr&, not BlobPtr<T>&. When we are inside the scope of a class template, the compiler treats references to the template itself as if we had supplied template arguments matching the template’s own parameters. That is, it is as if we had written:

BlobPtr<T>& operator++();
BlobPtr<T>& operator--();

Using a Class Template Name outside the Class Template Body

When we define members outside the body of a class template, we must remember that we are not in the scope of the class until the class name is seen (§ 7.4, p. 282):

// postfix: increment/decrement the object but return the unchanged value
template <typename T>
BlobPtr<T> BlobPtr<T>::operator++(int)
{
    // no check needed here; the call to prefix increment will do the check
    BlobPtr ret = *this;  // save the current value
    ++*this;    // advance one element; prefix ++ checks the increment
    return ret;  // return the saved state
}

Because the return type appears outside the scope of the class, we must specify that the return type returns a BlobPtr instantiated with the same type as the class. Inside the function body, we are in the scope of the class so do not need to repeat the template argument when we define ret. When we do not supply template arguments, the compiler assumes that we are using the same type as the member’s instantiation. Hence, the definition of ret is as if we had written:

BlobPtr<T> ret = *this;


Image Note

Inside the scope of a class template, we may refer to the template without specifying template argument(s).


Class Templates and Friends

When a class contains a friend declaration (§ 7.2.1, p. 269), the class and the friend can independently be templates or not. A class template that has a nontemplate friend grants that friend access to all the instantiations of the template. When the friend is itself a template, the class granting friendship controls whether friendship includes all instantiations of the template or only specific instantiation(s).

One-to-One Friendship

The most common form of friendship from a class template to another template (class or function) establishes friendship between corresponding instantiations of the class and its friend. For example, our Blob class should declare the BlobPtr class and a template version of the Blob equality operator (originally defined for StrBlob in the exercises in § 14.3.1 (p. 562)) as friends.

In order to refer to a specific instantiation of a template (class or function) we must first declare the template itself. A template declaration includes the template’s template parameter list:

// forward declarations needed for friend declarations in Blob
template <typename> class BlobPtr;
template <typename> class Blob; // needed for parameters in operator==
template <typename T>
    bool operator==(const Blob<T>&, const Blob<T>&);
template <typename T> class Blob {
    // each instantiation of Blob grants access to the version of
    // BlobPtr and the equality operator instantiated with the same type
    friend class BlobPtr<T>;
    friend bool operator==<T>
           (const Blob<T>&, const Blob<T>&);
    // other members as in § 12.1.1 (p. 456)
};

We start by declaring that Blob, BlobPtr, and operator== are templates. These declarations are needed for the parameter declaration in the operator== function and the friend declarations in Blob.

The friend declarations use Blob’s template parameter as their own template argument. Thus, the friendship is restricted to those instantiations of BlobPtr and the equality operator that are instantiated with the same type:

Blob<char> ca; // BlobPtr<char> and operator==<char> are friends
Blob<int> ia;  // BlobPtr<int> and operator==<int> are friends

The members of BlobPtr<char> may access the nonpublic parts of ca (or any other Blob<char> object), but ca has no special access to ia (or any other Blob<int>) or to any other instantiation of Blob.

General and Specific Template Friendship

A class can also make every instantiation of another template its friend, or it may limit friendship to a specific instantiation:

// forward declaration necessary to befriend a specific instantiation of a template
template <typename T> class Pal;
class C {  //  C is an ordinary, nontemplate class
    friend class Pal<C>;  // Pal instantiated with class C is a friend to C
    // all instances of Pal2 are friends to C;
    // no forward declaration required when we befriend all instantiations
    template <typename T> friend class Pal2;
};
template <typename T> class C2 { // C2 is itself a class template
    // each instantiation of C2 has the same instance of Pal as a friend
    friend class Pal<T>;  // a template declaration for Pal must be in scope
    // all instances of Pal2 are friends of each instance of C2, prior declaration needed
    template <typename X> friend class Pal2;
    // Pal3 is a nontemplate class that is a friend of every instance of C2
    friend class Pal3;    // prior declaration for Pal3 not needed
};

To allow all instantiations as friends, the friend declaration must use template parameter(s) that differ from those used by the class itself.

Befriending the Template’s Own Type Parameter
Image

Under the new standard, we can make a template type parameter a friend:

template <typename Type> class Bar {
friend Type; // grants access to the type used to instantiate Bar
    //  ...
};

Here we say that whatever type is used to instantiate Bar is a friend. Thus, for some type named Foo, Foo would be a friend of Bar<Foo>, Sales_data a friend of Bar<Sales_data>, and so on.

It is worth noting that even though a friend ordinarily must be a class or a function, it is okay for Bar to be instantiated with a built-in type. Such friendship is allowed so that we can instantiate classes such as Bar with built-in types.

Template Type Aliases

An instantiation of a class template defines a class type, and as with any other class type, we can define a typedef2.5.1, p. 67) that refers to that instantiated class:

typedef Blob<string> StrBlob;

This typedef will let us run the code we wrote in § 12.1.1 (p. 456) using our template version of Blob instantiated with string. Because a template is not a type, we cannot define a typedef that refers to a template. That is, there is no way to define a typedef that refers to Blob<T>.

Image

However, the new standard lets us define a type alias for a class template:

template<typename T> using twin = pair<T, T>;
twin<string> authors; // authors is a pair<string, string>

Here we’ve defined twin as a synonym for pairs in which the members have the same type. Users of twin need to specify that type only once.

A template type alias is a synonym for a family of classes:

twin<int> win_loss;  // win_loss is a pair<int, int>
twin<double> area;   // area is a pair<double, double>

Just as we do when we use a class template, when we use twin, we specify which particular kind of twin we want.

When we define a template type alias, we can fix one or more of the template parameters:

template <typename T> using partNo = pair<T, unsigned>;
partNo<string> books;  // books is a pair<string, unsigned>
partNo<Vehicle> cars;  // cars is a pair<Vehicle, unsigned>
partNo<Student> kids;  // kids is a pair<Student, unsigned>

Here we’ve defined partNo as a synonym for the family of types that are pairs in which the second member is an unsigned. Users of partNo specify a type for the first member of the pair but have no choice about second.

static Members of Class Templates

Like any other class, a class template can declare static members (§ 7.6, p. 300):

template <typename T> class Foo {
public:
   static std::size_t count() { return ctr; }
   // other interface members
private:
   static std::size_t ctr;
   // other implementation members
};

Here Foo is a class template that has a public static member function named count and a private static data member named ctr. Each instantiation of Foo has its own instance of the static members. That is, for any given type X, there is one Foo<X>::ctr and one Foo<X>::count member. All objects of type Foo<X> share the same ctr object and count function. For example,

// instantiates static members Foo<string>::ctr and Foo<string>::count
Foo<string> fs;
// all three objects share the same Foo<int>::ctr and Foo<int>::count members
Foo<int> fi, fi2, fi3;

As with any other static data member, there must be exactly one definition of each static data member of a template class. However, there is a distinct object for each instantiation of a class template. As a result, we define a static data member as a template similarly to how we define the member functions of that template:

template <typename T>
size_t Foo<T>::ctr = 0; // define and initialize ctr

As with any other member of a class template, we start by defining the template parameter list, followed by the type of the member we are defining and the member’s name. As usual, a member’s name includes the member’s class name, which for a class generated from a template includes its template arguments. Thus, when Foo is instantiated for a particular template argument type, a separate ctr will be instantiated for that class type and initialized to 0.

As with static members of nontemplate classes, we can access a static member of a class template through an object of the class type or by using the scope operator to access the member directly. Of course, to use a static member through the class, we must refer to a specific instantiation:

Foo<int> fi;                 // instantiates Foo<int> class
                             // and the static data member ctr
auto ct = Foo<int>::count(); // instantiates Foo<int>::count
ct = fi.count();             // uses Foo<int>::count
ct = Foo::count();           // error: which template instantiation?

Like any other member function, a static member function is instantiated only if it is used in a program.


Exercises Section 16.1.2

Exercise 16.9: What is a function template? What is a class template?

Exercise 16.10: What happens when a class template is instantiated?

Exercise 16.11: The following definition of List is incorrect. How would you fix it?

template <typename elemType> class ListItem;
template <typename elemType> class List {
public:
    List<elemType>();
    List<elemType>(const List<elemType> &);
    List<elemType>& operator=(const List<elemType> &);
    ~List();
    void insert(ListItem *ptr, elemType value);
private:
    ListItem *front, *end;
};

Exercise 16.12: Write your own version of the Blob and BlobPtr templates. including the various const members that were not shown in the text.

Exercise 16.13: Explain which kind of friendship you chose for the equality and relational operators for BlobPtr.

Exercise 16.14: Write a Screen class template that uses nontype parameters to define the height and width of the Screen.

Exercise 16.15: Implement input and output operators for your Screen template. Which, if any, friends are necessary in class Screen to make the input and output operators work? Explain why each friend declaration, if any, was needed.

Exercise 16.16: Rewrite the StrVec class (§ 13.5, p. 526) as a template named Vec.


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

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