Templates in C++: Variable Types
Dr. Lawlor, CS
202, CS, UAF
One thing that comes up fairly often is having to swap two variables
around: you need to move a into b, and b into a. Here's how
you'd do this with integers, which is pretty simple:
void exchange(int &a,int &b) {
int tmp=a;
a=b;
b=tmp;
}
int foo(void) {
int a=2,b=3; exchange(a,b);
return a;
}
(Try this in NetRun now!)
But the weird part about "exchange" is that the code doesn't really
care what the data type is for a and b, it just needs them to be the
same type, and "tmp" to have the same type as both of them. The
code to "exchange" two floats or strings looks identical to this, but
uses different types. So they added an extraordinarily powerful
technique in C++ called "templates" lets you write one piece of code
that's substituted with the appropriate data type at runtime.
For example, here's how you can exchange two variables of *any*
type. The compiler is usually smart enough to look at the
function's parameters, and figure out what SOMECRAZYTYPE means for that
particular call:
template <typename SOMECRAZYTYPE>
void exchange(SOMECRAZYTYPE &a,SOMECRAZYTYPE &b) {
SOMECRAZYTYPE tmp=a;
a=b;
b=tmp;
}
int foo(void) {
int a=2,b=3; exchange(a,b); // SOMECRAZYTYPE==int
float x=2.7,y=3.6; exchange(x,y); // SOMECRAZYTYPE==float
cout<<"After exchange, x="<<x<<" and y="<<y<<"\n";
string bob="smith", ted="jones"; exchange(bob,ted); // SOMECRAZYTYPE==string
cout<<"After exchange, bob="<<bob<<" and ted="<<ted<<"\n";
return a;
}
(Try this in NetRun now!)
SOMECRAZYTYPE works almost like a "type variable", that has some actual
value at runtime. Note that the compiler generates three separate
copies of the "exchange" code here: one copy for ints, a separate
function for floats, and yet another function for strings.
Now, there are times when the compiler can't figure out what
SOMECRAZYTYPE means. For example, this is a compile error,
because the compiler can't figure out whether to use "int" or "string":
int foo(void) {
int a=3; string b="cincinnati";
exchange(a,b); // SOMECRAZYTYPE == ??
return a;
}
(Try this in NetRun now!)
Instead of letting the compiler figure it out, you can specify the
template's "instantiation" type explicitly if you want, using this
function<type> syntax:
template <typename SOMECRAZYTYPE>
void exchange(SOMECRAZYTYPE &a,SOMECRAZYTYPE &b) {
SOMECRAZYTYPE tmp=a;
a=b;
b=tmp;
}
int foo(void) {
int a=2,b=3; exchange<int>(a,b); // SOMECRAZYTYPE==int
float x=2.7,y=3.6; exchange<float>(x,y); // SOMECRAZYTYPE==float
cout<<"After exchange, x="<<x<<" and y="<<y<<"\n";
string bob="smith", ted="jones"; exchange<string>(bob,ted); // SOMECRAZYTYPE==string
cout<<"After exchange, bob="<<bob<<" and ted="<<ted<<"\n";
return a;
}
(Try this in NetRun now!)
There's an existing standard template library function named "swap"
that works the same as our "exchange". The same exact idea above
can be used to make a templated function "maximum" that returns the
greater of its two arguments--the same function can be instantiated for
integers, floats, doubles, or even a user-defined type. There's
an existing function "max" that does this already!
Here's another example. Let's say I'd like to be able to print
stuff to the screen surrounded by "$$$". I don't care what the
stuff is, but I do know it has to be printable. To do this, I
take the thing to print as a templated type, here "BENJAMINS",
and print it. This magically works with anything: ints, strings,
even some new user-defined type, or that funky formatter object "setw".
template <typename BENJAMINS>
void moneyprint(const BENJAMINS &b) {
cout<<" $$$ "<<b<<" $$$ \n";
}
int foo(void) {
moneyprint(17);
moneyprint("ohio");
moneyprint(setw(30));
return 0;
}
(Try this in NetRun now!)
I can even take two separate, unrelated template types. The compiler figures them both out.
template <typename PRIMUS,typename SECUNDUS>
void printboth(const PRIMUS &thing1,const SECUNDUS &thing2) {
cout<<thing1<<" "<<thing2<<"\n";
}
int foo(void) {
printboth(19,"carrot");
printboth("bananna",4.567);
return 0;
}
(Try this in NetRun now!)
You can also template a class. For example, if I'd like to have
versions of 3D vectors built from both "float" (smaller storage space)
and "double" (higher precision"), I can build a templated version of my
"vec3" class:
template <typename REAL>
class vec3 {
public:
REAL x,y,z;
vec3(REAL val) {x=y=z=val;}
};
int foo(void) {
vec3<float> compact(2.3);
vec3<double> precise(3.45678);
return (int)compact.y;
}
(Try this in NetRun now!)
Unlike functions, which take parameters, classes don't take parameters,
so you have to specify the type ypu want to instance
explicitly. Sometimes it's handy to write a separate
"helper" function that figures out the appropriate types, and makes a
class of that type. For example, if we wanted "dostuff" to return
three separate values (of different types!), we can build a "triplet"
template:
// Stores three separate objects: a, b, and c.
template <typename A,typename B,typename C>
class triplet {
public:
A a; B b; C c;
triplet(const A& newa,const B& newb, const C& newc)
:a(newa), b(newb), c(newc) {}
};
// Makes a triplet (helper function)
template <typename A,typename B,typename C>
triplet<A,B,C> make_triplet(const A& newa,const B& newb, const C& newc)
{
return triplet<A,B,C>(newa,newb,newc);
}
// Returns three values
triplet<int,string,double> dostuff(void) {
return make_triplet(3,string("phil"),3.141592);
}
int foo(void) {
return dostuff().a;
}
(Try this in NetRun now!)
This is quite similar to the standard library class "pair", and the corresponding helper "make_pair".
Templates can combine with inheritance in a variety of increasingly
strange ways. For example, here we define a 2D superclass with
basic vector operations. The superclass is templated on the data
type T, because we might want vectors of floats, ints, etc. The
superclass is *also* templated on the Child class type, which allows
child classes to inherit working operators from the parent--if the
parent's operators returned a parent object, then you couldn't multiply
two children to get a new child. Here, "Vector2i" inherits the
operator+ and operator<< from Vector2dSuperclass, but defines its
own constructors and operator&.
/// Templated superclass, for vectors of other types:
/// Vector's coordinates are of type T
/// Vector operations return type Child
template <class T,class Child>
class Vector2dSuperclass {
public:
// Components of our vector
T x,y;
// Here's why we took Child as a template: all our operators return Child
Child operator+(const Child &b) const {return Child(x+b.x,y+b.y);}
Child operator-(const Child &b) const {return Child(x-b.x,y-b.y);}
Child operator*(const T scale) const
{return Child(x*scale,y*scale);}
friend ostream &operator<<(ostream &s,const Child &b) {
s<<b.x<<","<<b.y;
return s;
}
};
/// Vector2i is an integer cartesian vector in 2-space-- an x and y.
class Vector2i : public Vector2dSuperclass<int,Vector2i> {
public:
/// Simple 1-value constructor
Vector2i(const int init) {x=y=init;}
/// 2-value constructor
Vector2i(const int Nx,const int Ny) {x=Nx;y=Ny;}
// Only integers have bitwise-AND operators:
Vector2i operator&(const Vector2i &b) const {return Vector2i(x&b.x,y&b.y);}
};
int foo(void) {
Vector2i a(1,2), b(2,3);
Vector2i c=a+b; cout<<"c="<<c<<"\n";
Vector2i d=a&b; cout<<"d="<<d<<"\n";
return 0;
}
(Try this in NetRun now!)
Strange, but this "Curiously Recurring Template Pattern" actually works pretty well!
The Standard Template Library
Templates allow a whole new style of C++ code, where you don't have to
worry so much about types. There's a huge variety of existing
templated classes and functions as part of the "Standard Template
Library" (STL). In fact, we're going to spend the rest of
the *semester* going through different parts of the standard template
library!