Inapoi
Inainte
Cuprins
Template syntax
The template keyword tells the
compiler that the class definition that follows will manipulate one or more
unspecified types. At the time the actual class code is generated from the
template, those types must be specified so that the compiler can substitute
them.
To demonstrate the syntax, here’s a
small example that produces a
bounds-checked
array:
//: C16:Array.cpp
#include "../require.h"
#include <iostream>
using namespace std;
template<class T>
class Array {
enum { size = 100 };
T A[size];
public:
T& operator[](int index) {
require(index >= 0 && index < size,
"Index out of range");
return A[index];
}
};
int main() {
Array<int> ia;
Array<float> fa;
for(int i = 0; i < 20; i++) {
ia[i] = i * i;
fa[i] = float(i) * 1.414;
}
for(int j = 0; j < 20; j++)
cout << j << ": " << ia[j]
<< ", " << fa[j] << endl;
} ///:~
You can see that it looks like a normal
class except for the line
template<class T>
which says that T is the
substitution parameter, and that it represents a type name. Also, you see
T used everywhere in the class where you would normally see the specific
type the container holds.
In Array, elements are inserted
and extracted with the same function: the overloaded operator [ ]
. It
returns a reference, so it can be used on both sides of an equal sign (that is,
as both an lvalue and an
rvalue). Notice that if the index is out of
bounds, the require( ) function is used to
print a message. Since operator[] is an inline, you could
use this approach to guarantee that no array-bounds violations occur, then
remove the require( ) for the shipping code.
In main( ), you can see how
easy it is to create Arrays that hold different types of objects. When
you say
Array<int> ia;
Array<float> fa;
the compiler expands the Array
template (this is called
instantiation) twice, to
create two new generated
classes, which you can think
of as Array_int and Array_float. (Different compilers may decorate
the names in different ways.) These are classes just like the ones you would
have produced if you had performed the substitution by hand, except that the
compiler creates them for you as you define the objects ia and fa.
Also note that duplicate class
definitions
are either avoided by the compiler or merged by the
linker.
Non-inline function definitions
Of course, there are times when
you’ll want to have
non-inline
member function definitions. In this case, the compiler needs to see the
template declaration before the member function definition. Here’s
the example above, modified to show the non-inline member
definition:
//: C16:Array2.cpp
// Non-inline template definition
#include "../require.h"
template<class T>
class Array {
enum { size = 100 };
T A[size];
public:
T& operator[](int index);
};
template<class T>
T& Array<T>::operator[](int index) {
require(index >= 0 && index < size,
"Index out of range");
return A[index];
}
int main() {
Array<float> fa;
fa[0] = 1.414;
} ///:~
Any reference to a template’s class
name must be accompanied by its template argument list,
as in Array<T>::operator[]. You can imagine that internally, the
class name is being decorated with the arguments in the template argument list
to produce a unique class name identifier for each template
instantiation.
Header files
Even if you create non-inline function
definitions, you’ll usually want to put all declarations and
definitions for a template into a header file. This may seem to violate the
normal header file rule of “Don’t put in anything that allocates
storage,” (which prevents multiple definition errors at link time), but
template definitions are special. Anything preceded by
template<...> means the compiler won’t allocate storage for
it at that point, but will instead wait until it’s told to (by a template
instantiation), and that somewhere in the compiler and linker there’s a
mechanism for removing multiple definitions of an
identical template. So you’ll almost always put the entire template
declaration and definition in the header file, for ease of
use.
There are times when you may need to
place the template definitions in a separate cpp file to satisfy special
needs (for example, forcing template instantiations to exist in only a single
Windows dll file). Most compilers have some mechanism to allow this;
you’ll have to investigate your particular compiler’s documentation
to use it.
Some people feel that putting all of the
source code for your implementation in a header file makes it possible for
people to steal and modify your code if they buy a library from you. This might
be an issue, but it probably depends on the way you look at the problem: Are
they buying a product or a service? If it’s a product, then you have to do
everything you can to protect it, and probably you don’t want to give
source code, just compiled code. But many people see software as a service, and
even more than that, a subscription service. The customer wants your expertise,
they want you to continue maintaining this piece of reusable code so that they
don’t have to – so they can focus on getting their job done.
I personally think most customers will treat you as a valuable resource and will
not want to jeopardize their relationship with you. As for the few who want to
steal rather than buy or do original work, they probably can’t keep up
with you
anyway.
IntStack as a template
Here is the container and iterator from
IntStack.cpp, implemented as a generic container class using
templates:
//: C16:StackTemplate.h
// Simple stack template
#ifndef STACKTEMPLATE_H
#define STACKTEMPLATE_H
#include "../require.h"
template<class T>
class StackTemplate {
enum { ssize = 100 };
T stack[ssize];
int top;
public:
StackTemplate() : top(0) {}
void push(const T& i) {
require(top < ssize, "Too many push()es");
stack[top++] = i;
}
T pop() {
require(top > 0, "Too many pop()s");
return stack[--top];
}
int size() { return top; }
};
#endif // STACKTEMPLATE_H ///:~
Notice
that a template makes certain assumptions about the objects it is holding. For
example, StackTemplate assumes there is some sort of assignment operation
for T inside the push( ) function. You could say that a
template “implies an interface” for the types it is capable of
holding.
Another
way to say this is that templates provide a kind of weak typing mechanism
for C++, which is ordinarily a strongly-typed language. Instead of insisting
that an object be of some exact type in order to be acceptable, weak typing
requires only that the member functions that it wants to call are
available for a particular object. Thus, weakly-typed code can be applied
to any object that can accept those member function calls, and is thus much more
flexible[63].
Here’s the revised example to test
the template:
//: C16:StackTemplateTest.cpp
// Test simple stack template
//{L} fibonacci
#include "fibonacci.h"
#include "StackTemplate.h"
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
int main() {
StackTemplate<int> is;
for(int i = 0; i < 20; i++)
is.push(fibonacci(i));
for(int k = 0; k < 20; k++)
cout << is.pop() << endl;
ifstream in("StackTemplateTest.cpp");
assure(in, "StackTemplateTest.cpp");
string line;
StackTemplate<string> strings;
while(getline(in, line))
strings.push(line);
while(strings.size() > 0)
cout << strings.pop() << endl;
} ///:~
The only difference is in the creation of
is. Inside the template argument list you specify the type of object the
stack and iterator should hold. To demonstrate the genericness of the template,
a StackTemplate is also created to hold string. This is tested by
reading in lines from the source-code
file.
Constants in
templates
Template arguments are not restricted to
class types; you can also use built-in types. The values of these arguments then
become compile-time constants for that particular instantiation of the template.
You can even use default values for these arguments. The following example
allows you to set the size of the Array class during instantiation, but
also provides a default value:
//: C16:Array3.cpp
// Built-in types as template arguments
#include "../require.h"
#include <iostream>
using namespace std;
template<class T, int size = 100>
class Array {
T array[size];
public:
T& operator[](int index) {
require(index >= 0 && index < size,
"Index out of range");
return array[index];
}
int length() const { return size; }
};
class Number {
float f;
public:
Number(float ff = 0.0f) : f(ff) {}
Number& operator=(const Number& n) {
f = n.f;
return *this;
}
operator float() const { return f; }
friend ostream&
operator<<(ostream& os, const Number& x) {
return os << x.f;
}
};
template<class T, int size = 20>
class Holder {
Array<T, size>* np;
public:
Holder() : np(0) {}
T& operator[](int i) {
require(0 <= i && i < size);
if(!np) np = new Array<T, size>;
return np->operator[](i);
}
int length() const { return size; }
~Holder() { delete np; }
};
int main() {
Holder<Number> h;
for(int i = 0; i < 20; i++)
h[i] = i;
for(int j = 0; j < 20; j++)
cout << h[j] << endl;
} ///:~
As before, Array is a checked
array of objects and prevents you from indexing out of bounds. The class
Holder is much like Array except that it has a pointer to an
Array instead of an embedded object of type Array. This pointer is
not initialized in the constructor; the initialization is delayed until the
first access. This is called
lazy initialization; you
might use a technique like this if you are creating a lot of objects, but not
accessing them all, and want to save storage.
You’ll notice that the size
value in both templates is never stored internally in the class, but it is used
as if it were a data member inside the member
functions.
 |
|