Department of Engineering

IT Services

C++ and Building Classes (a vector arithmetic example)

In C++ you don't have to wait for the next release to have new features - you can write them yourself. In this document we'll add a new type of variable to C++, trying to make it behave like the built-in types. At the end we'll show how to make the most of your code using Inheritance and Templates.
It's assumed that you've seen C++ classes before. Not all the code details will be explained, but with luck you should be able to adapt the code for your own purposes.

A new class

With C++ you can create variables of simple types (e.g. int) but you can also create more complicated types that more closely match the concepts that the program deals with. We'll develop types to help with 2D vectors (the kind of vectors that have a magnitude and direction). We'll use cartesian coordinates to represent them. We'll call our new type vec (the C++ standard library already had a type called vector so we'd better not use that name) and we'll give it 2 fields. Here's the code to create and use the new type.

class vec {
public:
  float x;
  float y;
};

int main() {
  vec v1;
  v1.x=3;
  v1.y=6;
}

The public: line makes the x and y fields "publicly" available - i.e. available from code that isn't part of the class. If the fields were private we could still create v1 but we couldn't set the x and y components the way shown here.

A useful constructor

When we create an integer we can set it to a value as we create it (e.g. int i=5; is possible). To do that for our new variables we need to define a constructor - a routine that gets called when a new object is created. Constructors have the same name as their class. We want one that accepts 2 input parameters. Here's the code

class vec {
public:
  // constructor with 2 parameters
  vec(float f1, float f2) {
    x=f1;
    y=f2;
  }
  float x;
  float y;
};

int main() {
  vec v1(3,6);
}

Another constructor

If we try to add the following line

   vec v2; 

to the main function we get a compiler error message rather like

     error: no matching function for call to 'vec::vec()'

which is a shame, because a similar line used to work. With the first program the compiler created a default constructor function, but now that we've written a constructor function, the compiler no longer creates any default constructors. Let's write another constructor for the no-parameter situation.

class vec {
public:
  // constructor with 2 parameters
  vec(float f1, float f2) {
    x=f1;
    y=f2;
  }

  // constructor with 0 parameters
  vec() {
  }

  // data
  float x;
  float y;
};

int main() {
  vec v1(3,6);
  vec v2;
}

Adding vectors using a stand-alone function

Now we'll start performing calculations. First we'll add 2 vectors by using a routine, and we'll print the answer.

#include <iostream>

using namespace std;

class vec {
public:
  // constructor with 2 parameters
  vec(float f1, float f2) {
    x=f1;
    y=f2;
  }

  // constructor with 0 parameters
  vec() {
  }

  // data
  float x;
  float y;
};

// Prototype for the new function
vec addvectors(vec v1, vec v2);

int main() {
  vec v1(3,6);
  vec v2(2,-2);
  vec v3=addvectors(v1,v2);
  cout << "v3 =(" << v3.x << "," << v3.y << ")" << endl;
}

vec addvectors(vec v1, vec v2) {
  vec result;
  result.x = v1.x+v2.x;
  result.y = v1.y+v2.y;
  return result;
};

This code should print v3 =(5,4)

Overloading an operator

The addvectors function works, but it's messier than using a '+' sign. Fortunately C++ lets us define what we want the plus sign to mean when used with user-defined objects. The new part of the code below specifies what should happen when an object of type vec is added to another object of type vec. Don't worry about all the details, just note that inside a class, this refers to the current object.

#include <iostream>

using namespace std;

class vec {
public:
  // constructor with 2 parameters
  vec(float f1, float f2) {
    x=f1;
    y=f2;
  }

  // constructor with 0 parameters
  vec() {
  }

  // data
  float x;
  float y;

  // The new code
  vec operator+(const vec& v1) {
    vec result;
    result.x = this->x + v1.x;
    result.y = this->y + v1.y;
    return result;
  };
};

int main() {
  vec v1(3,6);
  vec v2(2,-2);
  vec v3=v1+v2;
  cout << "v3 =(" << v3.x << "," << v3.y << ")" << endl;
}

This code produces the same output as the previous version, but the main routine is neater.

Overloading an output command

We can tidy up the cout line too. This time we need to overload the << operator when an ostream variable is used with a variable of type vec. Again, don't worry too much about the detail of the new code's first line - it's a little obscure because it has to deal with situations where a vec is printed out in the middle of several other things all using the same cout command.

#include <iostream>

using namespace std;

class vec {
public:
  // constructor with 2 parameters
  vec(float f1, float f2) {
    x=f1;
    y=f2;
  }

  // constructor with 0 parameters
  vec() {
  }

  // data
  float x;
  float y;

  vec operator+(const vec& v1) {
    vec result;
    result.x = this->x+v1.x;
    result.y = this->y+v1.y;
    return result;
  };
};

// new code
ostream &operator <<(ostream &stream, vec v) {
    cout << "(" << v.x << "," << v.y << ")";
    return stream;
};

int main() {
  vec v1(3,6);
  vec v2(2,-2);
  vec v3=v1+v2;
  cout << "v3 =" << v3 << endl;
}

Let's review the situation so far. To prepare for working with vectors, we've developed a new type called vec which initially just contained data. We added functions to it to make this new type behave like the standard types that C++ provides. The code in the functions isn't too hard to understand, but the initial line of each function is. Fortunately you can copy and adapt them without having to understand all the nuances. The important thing is that the code in the main looks tidy now - the notation doesn't get in the way of what we want to do in future.

The lower level code's incomplete though. Here are some things that you could try adding by adapting the supplied code

  • Add a function to the vec class that returns the vector's magnitude.
  • Overload the subtraction operator
  • If v1=v1+v2; works, then v1+=v2; should work too.
  • Overload the multiplication operator so that you can multiply a vector by a float
  • Write some code so that you can use '==' to check if 2 vectors are the same

Rather than complete these details, this document will look at some further class features that are rather artificial in this context but are useful elsewhere.

Inheritance

The vector we've created is a "free" vector, not fixed to any location. If we want to create a new class locationvec containing location information we could copy the code of vec and add 2 more components. It would mean we'd have 2 copies of many functions though - for example, we'd have the same output code in vec as we'd have in locationvec. If we changed one version to make the output [5,4] rather than (5,4) we'd have to remember to change the other copy of the code too.

Alternatively we could add this fragment to our code

class locationvec: public vec {
public:
   float xlocation;
   float ylocation;
};

This inherits the features of vec, adding 2 fields to them, so the following would become possible

   locationvec v3;
   v3.x=2;
   v3.y=4;
   v3.xlocation=0;
   v3.ylocation=0;

Not everything is inherited from vec (the constructors aren't for example - we can't do locationvec v3(2,4); as the code stands). We might decide to write new versions of the inherited routines to use with locationvec objects (we might want to print out the extra location information for example) but at least we've avoided needless code duplication.

Templates

Suppose we wanted to have a class like vec but with strictly integer coordinates. We could copy the vec code and change all the floats to ints, but again we'd have 2 similar versions of essentially the same code - if we find a bug in one version we'd have to remember to change the other version too. Here's where templates come in useful. The following code is based on the code for vec but instead of float it has T, and before each defined unit there's a template<class T> line. What this does is to make T a (sort of) variable that can take on the "value" of a typename. The existance of the vec<int> line in the main routine will cause code to be generated for vec as if T had the "value" int, and vec<float> causes another version of the code to be generated that can cope with floats.

#include <iostream>

using namespace std;

template<class T>
class vec {
public:
  vec(T f1, T f2) {
    x=f1;
    y=f2;
  }
  vec() {
  }
  T x;
  T y;

  vec operator+(const vec& v1) {
    vec result;
    result.x = v1.x+this->x;
    result.y = v1.y+this->y;
    return result;
  };
};

template<class T>
ostream &operator <<(ostream &stream, vec<T> v)
  {
    cout << "(" << v.x << "," << v.y << ")";
    return stream;
};

int main() {
  vec<int> v1(3,6);
  vec<int> v2(2,-2);
  vec<int> v3=v1+v2;
  cout << "v3 =" << v3 << endl;

  vec<float> v4(3.9,6.7);
  vec<float> v5(2.0,-2.2);
  vec<float> v6=v4+v5;
  cout << "v6 =" << v6 << endl;
}

And finally ...

By combining the features mentioned above we can produce the following code. Though not easy to read, it shows how a function (like the output function here) can follow the "write once, use many times" principle. The same source code for producing output is used for vec and locationvec, and works if the fields are ints, floats, etc.

#include <iostream>

using namespace std;

// create a vec type
template<class T>
class vec {
public:
  vec(T f1, T f2) {
    x=f1;
    y=f2;
  }
  vec() {
  }
  T x;
  T y;

  vec operator+(const vec& v1) {
    vec result;
    result.x = v1.x+this->x;
    result.y = v1.y+this->y;
    return result;
  };
};

template<class T>
ostream &operator <<(ostream &stream, vec<T> v)
  {
    cout << "(" << v.x << "," << v.y << ")";
    return stream;
};

// create a locationvec type from the vec type
template<class T>
class locationvec: public vec<T> {
public: 
   // this constructor passes its arguments to the constructor of the
   // inherited vec
   locationvec(T f1,T f2): vec<T>(f1,f2) {}
   T xlocation;
   T ylocation;
};

int main() {
  // print the sum of 2 "int" vecs
  vec<int> v1(3,6);
  cout << "v1+v1 =" << v1+v1 << endl;

  // print the sum of 2 "float" vecs
  vec<float> v2(3.9,6.7);
  cout  << "v2+v2 =" << v2+v2 << endl;

  // print the sum of 2 "long" locationvecs
  locationvec<long> v3(2,4);
  cout  << "v3+v3 =" << v3+v3 << endl;
}