A Tour of C++ | Notes 2 Abstraction (Ch 5 - Ch 8)

5 Classes

A class is a user-defined type designed to represent an entity within a program. The C++ language provides fundamental support for three important kinds of classes:

  • Concrete classes
  • Abstract classes
  • Classes in class hierarchies

An astounding number of useful classes fall into one of these categories. Many others can be seen as variations of these or are constructed using combinations of their techniques.

Concrete Types

The basic idea of concrete classes is that they behave “just like built-in types”.

An Arithmetic Type

A classic example of a user-defined arithmetic type is the complex class:

class complex {
    double re, im; // representation: two doubles
public:
    complex(double r, double i) : re{r}, im{i} {}
    complex(double r) : re{r}, im{0} {}
    complex() : re{0}, im{0} {}

    double real() const { return re; }
    void real(double d) { re = d; }
    double imag() const { return im; }
    void imag(double d) { im = d; }

    complex& operator+=(complex z) {
        re += z.re;
        im += z.im;
        return *this;
    }

    // ...
};
  • A constructor that can be called without arguments is known as a default constructor.
  • The const qualifier on member functions like real() and imag() indicates that these functions do not modify the object.

A Container

A container is an object that holds a collection of elements. The class Vector is a container because objects of this type store such a collection:

class Vector {
public:
    Vector(int s) : elem{new double[s]}, sz{s} {
        for (int i = 0; i != s; ++i)
            elem[i] = 0;
    }

    ~Vector() { delete[] elem; }

    double& operator[](int i);
    int size() const;
private:
    double* elem;
    int sz;
};
  • A destructor ensures that resources allocated in the constructor are properly released. It is declared using the complement operator ~ followed by the class name.
  • delete frees a single object; delete[] frees an array.

This pattern of acquiring resources in the constructor and releasing them in the destructor is known as Resource Acquisition Is Initialization (RAII). It eliminates the need for “naked” new or delete operations, making code safer and more maintainable.

Objects of class Vector obey the same rules for naming, scoping, memory allocation, and lifetime as built-in types:

Vector gv(10); // global variable; gv is destroyed at the end of the program
Vector* gp = new Vector(100); // Vector on free store; never implicitly destroyed
void fct(int n)
{
    Vector v(n);
    {
        Vector v2(2 * n);
    } // v2 is destroyed here
} // v is destroyed here

Initializing Containers

class Vector {
public:
    Vector();
    Vector(std::initializer_list<double>);

    void push_back(double);
    // ...
};

The std::initializer_list used here is a standard library type recognized by the compiler. When we write {1, 2, 3}, the compiler creates an initializer_list object to pass to the constructor.

Vector::Vector(std::initializer_list<double> lst)
    : elem{new double[lst.size()]}, sz{static_cast<int>(lst.size())}
{
    copy(lst.begin(), lst.end(), elem);
}

Since the standard library typically uses unsigned types for sizes and indices, we use static_cast to convert the size_t to int. Other casts include reinterpret_cast, bit_cast (to treat memory as bytes), and const_cast (to remove const qualifiers).

Abstract Types

Types like complex and Vector are called concrete types because their implementation details (i.e., their representation) are part of the class definition. By contrast, an abstract type completely hides its implementation from the user.

class Container {
public:
    virtual double& operator[](int) = 0;
    virtual int size() const = 0;
    virtual ~Container() {}
};
  • The virtual keyword means the function may be redefined in a derived class. Such functions are called virtual functions.

  • The =0 syntax defines a pure virtual function, meaning any derived class must override it.

A class with a pure virtual function is called an abstract class.

The Container can be used like this:

void use(Container& c)
{
    const int sz = c.size();
    for (int i = 0; i != sz; ++i)
        cout << c[i] << '\n';
}
  • As is common, the abstract class Container does not have a constructor, since it doesn’t store data.
  • It does have a virtual destructor so that derived classes can be correctly cleaned up through base class pointers.

To use Container, we must define a concrete subclass that implements its interface:

class Vector_container : public Container {
public:
    Vector_container(int s) : v(s) {}
    ~Vector_container() {}

    double& operator[](int i) override { return v[i]; }
    int size() const override { return v.size(); }
private:
    Vector v;
};

Here, : public means that Vector_container is-a Container. The member destructor ~Vector() is implicitly invoked by the destructor of Vector_container.

void g()
{
    Vector_container vc(10);
    // ...
    use(vc);
}

Since use() only depends on the Container interface, it works equally well for any other class that implements that interface:

class List_container : public Container {
public:
    // ...
};

void h()
{
    List_container lc = {1, 2, 3};
    use(lc);
}

As a result, use(Container&) doesn’t need to be recompiled even if implementations of derived classes change.

Virtual Functions

When h() calls use(), List_container’s operator[]() must be called. When g() calls use(), Vector_container’s operator[]() must be called. To achive this resolution, a Container object must contain infomation to allow it to select the right function to call at run time, - the virtual function table, or simply the vtbl.

Class Hierarchies

6 Essential Operations

7 Templates

7.1 Introduction

A template is a class or a function that we parameterize with a set of types of values. We use templates to represent general ideas from which we can generate specific types or functions.

7.2 Parameterized Types

We can generalize our vector-of-doubles type to a vector-of-anyting type by making it a template and replacing the specific type double with a type parameter:

template<typename T>
class Vector {
private:
    T* elem;
    int sz;
public:
    explicit Vector(int s);
    ~Vector() {delete[] elem; }
    // ...
    T& operator[](int i);    
}

The template<typename T> prefix makes T a type parameter of the declaration it prefixes. In older code, we often see template<class T>, where class and typename are equivalent.