Virtual functions and abstract classes in C++

21. April 2012, 07:48

When humans write code we also create bugs. The only solution to this problem is to write less code. If we want to accomplish anything meaningful this means finding techniques that accomplishes more with less code. Code reuse is one of these techniques, and inheritence and abstract classes are one of the ways to reuse code in C++ (beyond simply using classes).

We’ll start this discussion with the a shape example. Let’s say you want to work with 2D shapes like circles, rectangles, triangles, etc. Maybe you need to draw them to the screen or maybe you need to tile them to a map. In any case, let’s say there a function called complexFunction that they all need to support. Your first draft of a Rectangle and Circle class might look like this:

class Rectangle {
  public:
    Rectangle(double l, double w) {
      length = l;
      width  = w;
    }
    ~Rectangle() {
      std::cerr<<"Rectangle is gone!\n";
    }
    double area() { return length*width;}
    void complexFunction() {
      std::cerr<<"This took a long time to write!\n";
    }
  private:
    double length, width;
};
class Circle {
  public:
    Circle(double r) {
      radius = r;
    }
    ~Circle() { std::cerr<<"Circle is gone!\n"; }
    double area() { return M_PI*radius*radius;}
    void complexFunction() {
      std::cerr<<"This took a long time to write!\n";
    }
  private:
    double radius;
};

This is okay I suppose, but notice that you have two copies of complexFunction. Now, you might be thinking to yourself that you can just copy and paste the code from the function, but what happens when you need to make a change? What if you have a bunch of shapes, Circle, Rectangle, Triangle, Trapezod, etc, and when you update the code you change it for almost all of them but you forgot to update Ellipse. Well, what will probably happen is that everything looks fine to you until you use an ellipse, at which point everything blows up in your face. There is a better solution to this, and it is something called an abstract class.

class Shape {
  public:
    Shape() { std::cerr<<"Shape is created!\n"; }
    //If the destructor is not virtual then the derived class'
    //destructor will not be called if you call delete on a shape*
    virtual ~Shape() { std::cerr<<"Shape is destroyed!\n"; }
    //This function is available to all derived classes, saving you lots of time
    void complexFunction() {
      std::cerr<<"You just saved a lot of time!\n";
    }
    /**
     * This is a "pure virtual" function -- because of this you cannot
     * create an object of type Shape directly, you must instead create
     * a derived class and implement this function.
     */
    virtual double area() = 0;
};
class Rectangle : public Shape { ...
class Circle : public Shape { ...

Like the comments say you cannot create an object of type shape, the compiler won’t let you. Now, notice the keyword virtual in the Shape class. There are two ways to use virtual. First, you can mark a function as being virtual which means that if the derived class overrides the function then the derived class’ function should always be called. Thus if you have a Shape* and you delete it, the destructor of the derived class will be called rather than Shape’s destructor. This would be important if something allocated memory, as in this version of the rectangle:

class Rectangle : public Shape {
  public:
    Rectangle(double l, double w) {
      length = l;
      width  = w;
      buff = new char[30];
    }
    ~Rectangle() {
      std::cerr<<"Rectangle is gone!\n";
      delete buff;
    }
    double area() { return length*width;}
  private:
    double length, width;
    char* buff;
};

The second use of the virtual keyword is to create a pure abstract function, as in:

    virtual double area() = 0;

As the comments say, you cannot create an instance of the Shape class because this function doesn’t exist. Instead you can inherit from the shape class and define this function for each child class. However, each of these derived types shares a base class so we can define cool functions like this one:

bool operator<(Shape& a, Shape& b) {
  return a.area() < b.area();
}
that allow us to compare any two Shape, regardless of their actual class (Rectangle, Circle, etc).

We can also use the Shape class to help us define a 3D shape:

class ThreeDShape {
  public:
    ThreeDShape (double h, Shape* s) : shape(s) { height = h;}
    ~ThreeDShape() {

If Shape did not declare its destructor virtual then we would need code that checks each Shape’s derived type to make sure the Rectangle gets to delete its memory:

      //check to make sure Rectangle's destructor is called (to avoid a memory leak)
      if (dynamic_cast<Rectangle*>(shape) != 0) {
        std::cerr<<"Shape is of type "<<typeid(dynamic_cast<Rectangle*>(shape)).name()<<'\n';
        std::cerr<<"I am explicitly deleting a rectangle!\n";
        delete dynamic_cast<Rectangle*>(shape);
      }
      else {
        delete shape;
      }

However, if we declare Shape’s destructor as virtual then the derived class’ destructor will be called whenever we delete a Shape object and we could just do this:

      delete shape;
    }
    double volume() {
      return height * shape->area();
    }
  private:
    double height;
    Shape* shape;
};

Now we probably want to make a few Circles and Rectangles, but what if we want to decide which one to generate randomly? We can make a “factory” function that produces Shape objects by randomly choosing Circle or Rectangle. Here is the code:

Shape* getNewShape() {
  if (rand() % 2) {
    return new Rectangle(rand() % 10 + 1, rand() % 10 + 1);
  }
  else {
    return new Circle(rand() % 10 + 1);
  }
}

Now, let’s see these in action. First, let’s include some headers:

#include <iostream>
#include <cmath>
#include <vector>
#include <stdlib.h>
#include <algorithm>
//The typeinfo header defines some function to access run
//time type identification (RTTI) information. Generally you will rely upon
//dynamic_cast if you want to be explicit about object types though.
#include <typeinfo>
int main() {
  Rectangle r(5,2);
  r.area();
  Circle c(5);
  c.area();

  std::vector<Shape*> shapes;
  for (int i = 0; i < 5; ++i) {
    shapes.push_back(getNewShape());
  }
  std::sort(shapes.begin(), shapes.end());
  for (int i = 0; i < 5; ++i) {
    std::cout<<shapes[i]->area()<<‘\n’;
    delete shapes[i];
  }

 for (int i = 0; i < 5; ++i) {
    ThreeDShape tds(rand()%10 + 1, getNewShape());
    std::cerr<<tds.volume()<<'\n';
    //The tds object goes out of scope with each iteration of the loop
  }
  //Remember that objects are cleaned up when they go out of scope, so you will
  //see the r and c variables' destructors at this point (after the loop).
  //A rule of thumb is to watch the curly braces -- when you see a { then a
  //new scope is opening and when you see the matching } the scope is closing.
}

Notice that things like sorting work because we could define the comparison operator.

Ben Firner

Programming Languages

---

Comment

Commenting is closed for this article.

---