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!