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.
As an example, we’ll implement a template version of StrBlob
(§ 12.1.1, p. 456). We’ll name our template Blob
to indicate that it is no longer specific to string
s. 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.
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
.
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.
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.
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)
check
and Element Access MembersWe’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
ConstructorsAs 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
.
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).
By default, a member of an instantiated class template is instantiated only if the member is used.
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--();
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;
Inside the scope of a class template, we may refer to the template without specifying template argument(s).
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).
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
.
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.
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.
An instantiation of a class template defines a class type, and as with any other class type, we can define a typedef
(§ 2.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>
.
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 pair
s 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 pair
s 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 TemplatesLike 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.
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
.