The C++ Programming Language – Part 2

This post will be mostly for personal reference as I go through Bjarne Stroustrop’s “The C++ Programming Language” 4th edition textbook. Some of the notes will appear random.

Chapter 3 – Abstraction Mechanisms

Three types of classes will be discussed. Concrete classes, abstract classes, and classes in class hierarchies. A concrete class behaves like a built-in type. Concrete classes can be on the stack, on the heap, in static memory, in other objects, can be directly referenced, and more. They are essentially tangible. They are initialized on creation, and copy-able. Functions defined in concrete classes are inlined by default when possible. Their destructors are automatically called when the concrete object goes out of scope. An abstract class is quite different. The user sees zero implementation details, only knows how to use the interface. All abstract objects go on the heap because we can’t know the size ahead of time. Virtual and pure virtual functions are placeholders for implementation in subclasses. Abstract classes provide interfaces for all subclasses to override. This helps keep consistency across classes; enforces a model that subclasses must adhere to. In this way, users of the interface don’t need to know about subclass implementation, just the interface. The compiler converts the name of virtual functions into an index into a table of function pointers; the “virtual function table” or vtable. The vtable pointer is in memory immediately before the object instance. Classes in hierarchies are going to be explored in more detail in a different post. See SOLID design principles for more information.

A function object, aka Functor, is an object member that is callable. Yes, C++ can have callable objects. You need to define the () operator to make it callable.

/* A function object, aka Functor, is an object member that is callable */

template<typename T> class Less_than
{
    const T val;

    public:
        Less_than(const T& v) {
            val = v;
        }

        /* Define callable () */
        bool operator()(const T& x) const {
            return x < val;
        }
}

It is also a great idea to define copy constructor and copy assignment operator for an object. This way, you can explicitly define what happens during these operations. Otherwise, your code may not do exactly what you assume it does.

class Vector {

    private:

        double *elem;
        int size;

    public:

        Vector(int s);
        ~Vector() {
            delete[] elem;
        }

        /* copy constructor */
        Vector(const Vector& a)
        {
            /* create a new area of memory for the new object's elem array */
            elem = new double[a.size];
            size = a.size;

            /* copy elements */
            for (int i = 0; i != size; i++) {
                elem[i] = a.elem[i];
            }
        }

        /* copy assignment */
        Vector& operator=(const Vector& a);

        double& operator[](int i);
        const double& operator[](int i) const;

        int size() const;

};

Abstract class example with pure virtual functions. Utilizing “= 0;” makes a function pure virtual. If a function in a class is pure virtual, the class is now defined as abstract. Pure virtual functions must be implemented by subclasses.

/* Example of a virtual / abstract class with pure virtual functions */

class Container
{
    public:
        virtual double& operator[](int) = 0;
        virtual int size() const = 0;
        virtual ~Container() {}
}

Finally, a typical user-defined class that behaves like a built-in type. This is a concrete class.

/* A classical user-defined type example */

class Complex
{
    double re;
    double im;

    public:

        /* Constructors */
        Complex(double r, double i) {
            re = r;
            im = i;
        }

        Complex(double r) {
            re = r;
            im = 0;
        }

        Complex() {
            re = 0;
            im = 0;
        }

        /* Accessors and mutators
         * const indicates that we will not modify the object for which they are called on
         */
        double real() const {
            return re;
        }

        void real(double d) {
            re = f;
        }

        double imag() const {
            return im;
        }

        void imag(double d) {
            im = d;
        }

        
        /* Operator definitions */
        Complex& operator+=(Complex z) {
            re += z.re;
            im += z.im;
            return *this;
        }

        Complex& operator-=(Complex z) {
            re -= z.re;
            im -= z.im;
            return *this;
        }


        /* Destructor */
        ~Complex() {
            // cleanup if stuff is on the heap
        }
}

Classes can also be parameterized; that is, the type can be determined at compile time. In this example, a Vector can be a container for any type, represented by T in the implementation. Templates are resolved at compile time, so there is no performance overhead at runtime.

/* Allow for parameterization of class */

template<typename T> class Vector {

    private:
        T* elem;
        int size;

    public:
        Vector(int s);
        T& operator[](int i);
        const T& operator[](int i) const;
}