Lesson 5 - Abstract Methods

by Zoran Horvat

In previous chapter we have defined virtual functions and here we will first provide a short wrap-up before we proceed to the next level. Suppose that there is a Move function in the Shape type which linearly translates the shape in 2D space. Base implementation can only move shape’s location and hope for the best. Subtype then might decide to add more to the implementation. Fortunately, Ellipse and Rectangle are defined relatively to shape’s location so in their cases base implementation of the Move method is actually complete.

Below is the implementation of virtual function Move in plain C.

/* Partial listing of shape.h */
#include "vtable.h"

#ifndef SHAPE_H
#define SHAPE_H

#define VT_SHAPE_PRINTOUT 0
#define VT_SHAPE_PRINTOUT1 1
#define VT_SHAPE_MOVE 2
#define VT_SHAPE_DESTRUCTOR 3
#define VT_SHAPE_END 4
...
typedef void (*vcall_Shape_Move)(struct Shape*, float deltaX, float deltaY);
...
void Shape_Move(struct Shape *_this, float deltaX, float deltaY);
...
#endif
/* Partial listing of shape.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "shape.h"
...
void Shape_Move(struct Shape *_this, float deltaX, float deltaY)
{
    _this->locationX += deltaX;
    _this->locationY += deltaY;
}
...
/* Listing of vtable.c */
#include "vtable.h"
#include "shape.h"
#include "ellipse.h"
#include "rectangle.h"

void vtable_Initialize()
{

    vtable_Shape[VT_SHAPE_PRINTOUT] = Shape_PrintOut;
    vtable_Shape[VT_SHAPE_PRINTOUT1] = Shape_PrintOut1;
    vtable_Shape[VT_SHAPE_MOVE] = Shape_Move;
    vtable_Shape[VT_SHAPE_DESTRUCTOR] = Shape_Destructor;

    vtable_Shape_Ellipse[VT_SHAPE_PRINTOUT] = Ellipse_PrintOut;
    vtable_Shape_Ellipse[VT_SHAPE_PRINTOUT1] = Ellipse_PrintOut1;
    vtable_Shape_Ellipse[VT_SHAPE_MOVE] = Shape_Move;
    vtable_Shape_Ellipse[VT_SHAPE_DESTRUCTOR] = Ellipse_Destructor;

    vtable_Shape_Rectangle[VT_SHAPE_PRINTOUT] = Rectangle_PrintOut;
    vtable_Shape_Rectangle[VT_SHAPE_PRINTOUT1] = Rectangle_PrintOut1;
    vtable_Shape_Rectangle[VT_SHAPE_MOVE] = Shape_Move;
    vtable_Shape_Rectangle[VT_SHAPE_DESTRUCTOR] = Rectangle_Destructor;

}

Note how all three distinct virtual method tables are populated with the same function pointer. Whichever the object supplied, dynamic dispatch mechanism would procure the same function pointer for the upcoming call.

In object-oriented languages like C++ and C# there is nothing to do when subclass decides not to override the inherited virtual method - subclass simply skips the method definition. This instructs the compiler to populate subclass's virtual method table with function pointer inherited from the base class. Below is the Move virtual function implementation in C++:

// Partial listing of shape.hpp
#ifndef SHAPE_HPP
#define SHAPE_HPP

class Shape
{
public:
    ...
    virtualvoid Move(float deltaX, float deltaY);
    ...
};

#endif
// Listing of shape.cpp
#include "shape.hpp"
...
void Shape::Move(float deltaX, float deltaY)
{
    locationX += deltaX;
    locationY += deltaY;
}

C# version is almost the same:

// Partial listing of Shape.cs
using System;

namespace Geometry
{
    public class Shape
    {
        ...
        public virtual void Move(float deltaX, float deltaY)
        {
            locationX += deltaX;
            locationY += deltaY;
        }
        ...
    }
}

In both cases we did not have to bother with derived classes - their definitions remain the same and they all inherit default implementation of the Move virtual method. The situation will become a bit more complicated if we decide to add a method which moves the shape beyond some specific point (let its name be MoveBeyond). All parts of the shape should then fall to the right and above the given point. For point (0, 0), which denotes coordinate origin, all points of the shape would have to have positive coordinate values, meaning that complete shape is within the first quadrant in Cartesian coordinate system. This function, when implemented in the base type, could simply move shape’s location (or only one of its components, depending on the argument) so that at least the location satisfies the requirement. In some cases (e.g. Rectangle) that would be sufficient because rectangle as we have defined it extends to the right and upwards from the location point. In other cases (e.g. Ellipse) additional work would be required as the shape has intrinsic habit to spread all around its designated location point.

Implementation of the MoveBeyond method in plain C requires both Shape and Ellipse types to define its body. Virtual method tables would then be constructed so that Shape and Rectangle types point to Shape's implementation, while Ellipse points to its own specific implementation.

/* Partial listing of shape.h */
#include "vtable.h"

#ifndef SHAPE_H
#define SHAPE_H

#define VT_SHAPE_PRINTOUT 0
#define VT_SHAPE_PRINTOUT1 1
#define VT_SHAPE_MOVE 2
#define VT_SHAPE_MOVEBEYOND 3
#define VT_SHAPE_DESTRUCTOR 4
#define VT_SHAPE_END 5
...
typedef void (*vcall_Shape_MoveBeyond)(struct Shape*, float minX, float minY);
...
void Shape_MoveBeyond(struct Shape *_this, float minX, float minY);
...
#endif
/* Partial listing of shape.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "shape.h"
...
void Shape_MoveBeyond(struct Shape *_this, float minX, float minY)
{
    if (_this->locationX < minX)
        _this->locationX = minX;
    if (_this->locationY < minY)
        _this->locationY = minY;
}
...
/* Listing of ellipse.h */
#include "shape.h"
...
void Ellipse_MoveBeyond(struct Ellipse *_this, float minX, float minY);
...
#endif
/* Listing of ellipse.c */
#include "ellipse.h"
#include <stdio.h>
...
void Ellipse_MoveBeyond(struct Ellipse *_this, float minX, float minY)
{
    Shape_MoveBeyond((struct Shape*)_this, minX + _this->radiusX, minY + _this->radiusY);
}
...
/* Listing of vtable.c */
#include "vtable.h"
#include "shape.h"
#include "ellipse.h"
#include "rectangle.h"

void vtable_Initialize()
{

    vtable_Shape[VT_SHAPE_PRINTOUT] = Shape_PrintOut;
    vtable_Shape[VT_SHAPE_PRINTOUT1] = Shape_PrintOut1;
    vtable_Shape[VT_SHAPE_MOVE] = Shape_Move;
    vtable_Shape[VT_SHAPE_MOVEBEYOND] = Shape_MoveBeyond;
    vtable_Shape[VT_SHAPE_DESTRUCTOR] = Shape_Destructor;

    vtable_Shape_Ellipse[VT_SHAPE_PRINTOUT] = Ellipse_PrintOut;
    vtable_Shape_Ellipse[VT_SHAPE_PRINTOUT1] = Ellipse_PrintOut1;
    vtable_Shape_Ellipse[VT_SHAPE_MOVE] = Shape_Move;
    vtable_Shape_Ellipse[VT_SHAPE_MOVEBEYOND] = Ellipse_MoveBeyond;
    vtable_Shape_Ellipse[VT_SHAPE_DESTRUCTOR] = Ellipse_Destructor;

    vtable_Shape_Rectangle[VT_SHAPE_PRINTOUT] = Rectangle_PrintOut;
    vtable_Shape_Rectangle[VT_SHAPE_PRINTOUT1] = Rectangle_PrintOut1;
    vtable_Shape_Rectangle[VT_SHAPE_MOVE] = Shape_Move;
    vtable_Shape_Rectangle[VT_SHAPE_MOVEBEYOND] = Shape_MoveBeyond;
    vtable_Shape_Rectangle[VT_SHAPE_DESTRUCTOR] = Rectangle_Destructor;

}

C++ implementation of the MoveBeyond method is again almost the same as its C counterpart. Below are the listings of shape header and implementation files:

// Partial listing of shape.hpp
#ifndef SHAPE_HPP
#define SHAPE_HPP

class Shape
{
public:
    ...
    virtual void MoveBeyond(float minX, float minY);
    ...
};

#endif
// Partial listing of shape.cpp
#include <iostream>
#include <stdlib.h>
#include <string.h>
#include "shape.hpp"
...
void Shape::MoveBeyond(float minX, float minY)
{
    if (minX > locationX)
        locationX = minX;
    if (minY > locationY)
        locationY = minY;
}
// Partial listing of ellipse.hpp
#include "shape.hpp"

#ifndef ELLIPSE_HPP
#define ELLIPSE_HPP

class Ellipse: public Shape
{
public:
    ...
    void MoveBeyond(float minX, float minY);
    ...
};

#endif
// Partial listing of ellipse.cpp
#include <iostream>
#include "ellipse.hpp"
...
void Ellipse::MoveBeyond(float minX, float minY)
{
    Shape::MoveBeyond(minX + radiusX, minY + radiusY);
}

We can also provide C# implementation of the MoveBeyond method, just to demonstrate how similar it is to all that we have seen this far.

// Partial listing of Shape.cs
using System;

namespace Geometry
{
    public class Shape
    {
        ...
        public virtual void MoveBeyond(float minX, float minY)
        {
            if (minX > locationX)
                locationX = minX;
            if (minY > locationY)
                locationY = minY;
        }
        ...
    }
}
// Partial listing of ellipse.cs
using System;

namespace Geometry
{
    public class Ellipse: Shape
    {
        ...
        public override void MoveBeyond(float minX, float minY)
        {
            base.MoveBeyond(minX + LocationX, minY + LocationY);
        }
        ...
    }
}

Evolution of Abstract Methods

By logically pushing virtual method principles further, we encounter cases when base type is utterly unable to provide default implementation of the method. It is easy to conceive a proper example of such method which is unimplementable in the given base type. In our shapes example, base type Shape can define a method named GetArea which is supposed to calculate the overall area of the shape. That method cannot be implemented in the base type because base type does not know which particular shape it represents and therefore cannot apply any formula to calculate it. Virtual method without definition is in C# terms called abstract (in C++ it is referred to as pure virtual function). Abstract method is a virtual method which does not have implementation in type that defines it. Method must be implemented by the derived type in order to be called on an object.

Let's see what it looks like in plain C on an example of method which calculates shape's area. To be consistent with other accessors, we will define abstract accessor method get_Area. This method would be implemented in derived types only. Here is the code:

/* Partial listing of shape.h */
#include "vtable.h"

#ifndef SHAPE_H
#define SHAPE_H

#define VT_SHAPE_PRINTOUT 0
#define VT_SHAPE_PRINTOUT1 1
#define VT_SHAPE_MOVE 2
#define VT_SHAPE_MOVEBEYOND 3
#define VT_SHAPE_GETAREA 4
#define VT_SHAPE_DESTRUCTOR 5
#define VT_SHAPE_END 6
...
typedef float (*vcall_Shape_get_Area)(const struct Shape*);
...
#endif
/* Partial listing of ellipse.h */
#include "shape.h"

#ifndef ELLIPSE_H
#define ELLIPSE_H
...
float Ellipse_get_Area(const struct Ellipse *_this);
...

#endif
/* Partial listing of ellipse.c */
#include "ellipse.h"
#include <stdio.h>
...
float Ellipse_get_Area(const struct Ellipse *_this)
{
    return 3.14159F * _this->radiusX * _this->radiusY;
}
...
/* Partial listing of rectangle.h */
#include "shape.h"

#ifndef RECTANGLE_H
#define RECTANGLE_H
...
float Rectangle_get_Area(const struct Rectangle *_this);
...

#endif
/* Partial listing of rectangle.c */
#include "rectangle.h"
#include <stdio.h>
...
float Rectangle_get_Area(const struct Rectangle *_this)
{
    return _this->width * _this->height;
}
...
/* Listing of vtable.c */
#include <stdlib.h>
#include "vtable.h"
#include "shape.h"
#include "ellipse.h"
#include "rectangle.h"

void vtable_Initialize()
{

    vtable_Shape[VT_SHAPE_PRINTOUT] = Shape_PrintOut;
    vtable_Shape[VT_SHAPE_PRINTOUT1] = Shape_PrintOut1;
    vtable_Shape[VT_SHAPE_MOVE] = Shape_Move;
    vtable_Shape[VT_SHAPE_MOVEBEYOND] = Shape_MoveBeyond;
    vtable_Shape[VT_SHAPE_GETAREA] = NULL;
    vtable_Shape[VT_SHAPE_DESTRUCTOR] = Shape_Destructor;

    vtable_Shape_Ellipse[VT_SHAPE_PRINTOUT] = Ellipse_PrintOut;
    vtable_Shape_Ellipse[VT_SHAPE_PRINTOUT1] = Ellipse_PrintOut1;
    vtable_Shape_Ellipse[VT_SHAPE_MOVE] = Shape_Move;
    vtable_Shape_Ellipse[VT_SHAPE_MOVEBEYOND] = Ellipse_MoveBeyond;
    vtable_Shape_Ellipse[VT_SHAPE_GETAREA] = Ellipse_get_Area;
    vtable_Shape_Ellipse[VT_SHAPE_DESTRUCTOR] = Ellipse_Destructor;

    vtable_Shape_Rectangle[VT_SHAPE_PRINTOUT] = Rectangle_PrintOut;
    vtable_Shape_Rectangle[VT_SHAPE_PRINTOUT1] = Rectangle_PrintOut1;
    vtable_Shape_Rectangle[VT_SHAPE_MOVE] = Shape_Move;
    vtable_Shape_Rectangle[VT_SHAPE_MOVEBEYOND] = Shape_MoveBeyond;
    vtable_Shape_Rectangle[VT_SHAPE_GETAREA] = Rectangle_get_Area;
    vtable_Shape_Rectangle[VT_SHAPE_DESTRUCTOR] = Rectangle_Destructor;

}

In this way we have specifically said that all instances of the Ellipse type are to use the ellipse area formula in calculation; all instances of the Rectangle type on the other hand will use the rectangle area formula when asked the same. Shape type instances are now becoming extinct. Nobody ever is going to instantiate the Shape type directly any more. Once that null entry has made its way to Shape type's virtual method table, no more direct calls to Shape's constructor are permitted. Only derived types can be instantiated and then that is safe because those types are overwriting the null entry in their virtual method tables with valid pointers to functions they provide.

We can demonstrate the use of get_Area abstract method by writing a function which accepts array of shapes and prints their areas. Testing function is called PrintAreas and here is its source code with accompanying main function:

/* Listing of main.c */
#include <stdlib.h>
#include "ellipse.h"
#include "rectangle.h"
#include "shape.h"
#include "vtable.h"
#include <stdio.h>

void PrintAreas(struct Shape *shapes[], int count)
{
    int i = 0;

    for (i = 0; i < count; i++)
    {
        vcall_Shape_get_Area f = (vcall_Shape_get_Area)shapes[i]->vtable[VT_SHAPE_GETAREA];
        printf("%s's area is %.2f\n", Shape_get_Name(shapes[i]), f(shapes[i]));
    }

}

int main(char args[])
{

    struct Ellipse *ellipse = NULL;
    struct Rectangle *rectangle = NULL;
    struct Shape *shapes[2];
    vcall_Shape_Destructor destructor = NULL;

    vtable_Initialize();

    ellipse = (struct Ellipse*)malloc(sizeof(struct Ellipse));
    Ellipse_Constructor1(ellipse, 1.0F, 2.0F, 3.0F, 4.0F);
    Shape_set_Name((struct Shape*)ellipse, "Ellipse");

    rectangle = (struct Rectangle*)malloc(sizeof(struct Rectangle));
    Rectangle_Constructor1(rectangle, 3.0F, 4.0F, 5.0F, 6.0F);
    Shape_set_Name((struct Shape*)rectangle, "Rectangle");

    shapes[0] = (struct Shape*)ellipse;
    shapes[1] = (struct Shape*)rectangle;

    PrintAreas(shapes, 2);

    /* Now call virtual destructors to release shapes */
    destructor = (vcall_Shape_Destructor)ellipse->_base.vtable[VT_SHAPE_DESTRUCTOR];
    destructor((struct Shape*)ellipse);
    free(ellipse);

    destructor = (vcall_Shape_Destructor)rectangle->_base.vtable[VT_SHAPE_DESTRUCTOR];
    destructor((struct Shape*)rectangle);
    free(rectangle);

}

This code produces correctly calculated areas of the two shapes:

            
Ellipse's area is 37.70
Rectangle's area is 30.00
                
    

Output written by the PrintAreas function proves that it has correctly decided which implementation to call for each of the shapes. Dynamic dispatch mechanism was again utilized to resolve function addresses when they were requested.

Abstract Methods in Object-Oriented Languages

C++ implementation of the pure virtual method is very simple. The only thing to do in base class is to set function's body to = 0, literally. Here is the shape.hpp header file which demonstrates this:

// Partial listing of shape.hpp
#ifndef SHAPE_HPP
#define SHAPE_HPP

class Shape
{
public:
    ...
    virtual float get_Area() const = 0;
    ...
};

#endif

Observe that get_Area method is declared to operate on constant instance of the class, which makes it a proper accessor. Here is the implementation for get_Area provided by Ellipse and Rectangle classes:

// Partial listing of ellipse.hpp
#include "shape.hpp"

#ifndef ELLIPSE_HPP
#define ELLIPSE_HPP

class Ellipse: public Shape
{
public:
    ...
    float get_Area() const;
    ...
};

#endif
// Partial listing of ellipse.cpp
#include <iostream>
#include "ellipse.hpp"
...
float Ellipse::get_Area() const
{
    return 3.14159F * radiusX * radiusY;
}
// Partial listing of rectangle.hpp
#include "shape.hpp"

#ifndef RECTANGLE_HPP
#define RECTANGLE_HPP

class Rectangle: public Shape
{
public:
    ...
    float get_Area() const;
    ...
};

#endif
// Partial listing of rectangle.cpp
#include <iostream>
#include "rectangle.hpp"
...
float Rectangle::get_Area() const
{
    return width * height;
}

C# implementation carries one novelty. Area would be implemented as a property - accessor method is then provided internally by the compiler - but this time property would be read-only. It has no mutator; we cannot set shape's area, only get it. In addition, this property would be abstract, which is allowed by C# syntax:

// Partial listing of shape.cs
using System;

namespace Geometry
{
    public abstract class Shape
    {
        ...
        public abstract float Area { get; }
        ...
    }
}

Note that class declaration now contains keyword abstract - this is mandatory in C# if class contains abstract members. With Area property declaration given in the Shape base class, Ellipse's and Rectangle's task is fairly easy:

// Partial listing of ellipse.cs
using System;

namespace Geometry
{
    public class Ellipse: Shape
    {
        ...
        public override float Area
        {
            get
            {
                return (float)Math.PI * radiusX * radiusY;
            }
        }
        ...
    }
}
// Partial listing of Rectangle.cs

namespace Geometry
{
    public class Rectangle: Shape
    {
        ...
        public override float Area
        {
            get
            {
                return width * height;
            }
        }
        ...
    }
}

We have used the static class Math and its property PI to calculate ellipse's radius. Apart from this helper there is almost nothing special in these properties. The fact that they are providing body for the base property is denoted by the override keyword. Properties are implementing the getter, which will be compiled into get_Area virtual method and then virtual method tables for Ellipse and Rectangle class will contain pointer to their implementations instead of null entry inherited from the Shape class. There is apparently nothing new under the sun.


If you wish to learn more, please watch my latest video courses

About

Zoran Horvat

Zoran Horvat is the Principal Consultant at Coding Helmet, speaker and author of 100+ articles, and independent trainer on .NET technology stack. He can often be found speaking at conferences and user groups, promoting object-oriented and functional development style and clean coding practices and techniques that improve longevity of complex business applications.

  1. Pluralsight
  2. Udemy
  3. Twitter
  4. YouTube
  5. LinkedIn
  6. GitHub