Skip to content

Chapter 4: Classes

4.1

This chapter informally presents ways of defining and using new types (user-defined types). In particular, it presents the basic properties, implementation techniques, and language facilities used for concrete classes, abstract classes, and class hierarchies.

The central language feature of C++ is the class. A class is a user-defined type provided to represent a concept in the code of a program. Whenever our design for a program has a useful concept, idea, entity, etc., we try to represent it as a class in the program so that the idea is there in the code, rather than just in our heads, in a design document, or in some comments.

4.2

The basic idea of concrete classes is that they behave "just like built-in types." For example, a complex number type and an infinite-precision integer are much like built-in int, except of course that they have their own semantics and sets of operations. Similarly, a vector and a string are much like built-in arrays, except that they are better behaved.

The defining characteristics of a concrete type is that its representation is part of its definition. In many important cases, such as a vector, that representation is only one or more pointers to data stored elsewhere, but that representation is present in each Objet of a concrete class That allows implementation to be optimally efficient in time and space.

  • place objects of concrete types on stack, in statically allocated memory, and in other objects
  • refer to objects directly
  • initliaze objects immmediately and completely
  • copy and move objects

The representation can be private and accessible only through the member functions, but it is present. Therefore, if the representation changes in any signifcant way, a user must recompile. This is the price to pay for having concrete types behave exactly like built-in types.

4.2.1

In addition to the logical demands, complex must be efficient or it will remain unused. This implies that simple operations must be inlined. That is, simple operations must be implemented without function calls in the generated machine code.

A constructor that can be invoked without an argument is called a default constructor.

many useful operations do not require diret access to the representation of complex, so they can defined separately from the class definition.

The compiler converts operators involving complex numbers into appropriate function calls. For example, c!=b means operator!=(c, b), and 1/a means operator/(complex{1}, a).

Also, it is not possible to change the meaning of an operator for built-in types. so you can't redefine + to subtract ints.

4.2.2

A container is an object holding a collection of elements.

The name of a destructor is the complement opeartor, ~, followed by the name of the class; it is the complement of a constructor. deleting element in a Vector is done without intervention by users of Vector. The user simply create and use Vectors much as they would variables of built-in types.

A Container uses a handle-to-data model, which is very commonly used to manage data that can v ary in size during the lifetime of an object. The technique of acquiring resources in a constructor and releasing them in a destructor, known as Resource Acquisition is Initialization or RAII, allows us to eliminate "naked new operatos," that is, to avoid allocations in genral code and keep them buried inside the implementation of well-behaved abstractions.

4.2.3

A container exists to hold elements, so obviously we need convenient ways of getting elements into a container. For Vector

  • initializer-list constructor: Initialize with a list of elements.
  • When we use a {}-list, such as {1,2,3,4}, the compiler will create an object of type std::initializer_list to give to the program.
  • push_back(): add a new element at the end of (at the back of) the sequence.

A static_cast does not check the value it is converting; the programer is trusted to use it correctly. This is not always a good assumption, so if in doubt, check the value.

Other casts are reinterpret_cast for treating an object as simply a sequence of bytes and const_cast for "casting away const." Judicious use of the type system and well-deisgned libraries allow us to eliminate unchecked casts in higher-level software.

4.3

Types such as complex and Vector are called concrete types because their representation is part of their definition. In contrast, an abstract type is a type that completely insulates a user from implementation details.

class Container{
public:
    virtual double& operator[](int) = 0;  // pure virtual function
    virtual int size() const = 0;         // const member function
    virtual ~Container(){}                // destructor.
    // may not need to define constructor, but destructor has to be virtual
};

The word virtual means "may be redefined later in a class derived from this one". Unsurprisingly, a function declared virtual is called a virtual function. A class derived from Container provides an implementation for the Container inteface. The curious =0 syntax says that the function is pure virtual; that is, some class derived from Container must define the function.

A class that provides the interface to a variety of other classes is often called a polymorphic type.

As is common for abstract classes, Container does not have a constructor. On the other hand, Container does have a destructor and that destructor is virtual, so that classes derived from Container can provide implementation. Again, that is common for abstract classes because they tend to be manipulated through references or pointers, and someone destroying a Container throught a pointer has no idea what resources are owned by its implementation.

Inherit from a class:

class Vector_container: public Container {
public:
    //...
    ~Vector_container(){}
    double& operator[](int) override {return v[i];}
private:
    Vector v;  // destructor is deleted to ~Vector::Vector
};

The :public can be read as "is derived from" or "is a subtype of". The member function operator[]() are said to override the corresponding members in the base class Container. Here the explicit override is used to make clear what's intended. The explicit use of override is particualrly useful in larger class hierarchies where it can otherwise be hard to know what is supposed to override that.

The destructor overrides the base class destructor. Note that the member destructor is implicity invoked by its class's destructor.

4.4

Let's talk about virtual functions. How is a virtual function resolved to a particular class in the class hierarchy?

To achieve this resolution, a Container object must contain information to allow it to select the right functino to call at run time. The usual implementation techique is for the compiler to conver the name of a virtual function into an index into a table of pointers to functions. That table is usually called the virtual function table or simply the vtbl. Each class with virtual functions has its own vtbl identifying its virtual functions. (here, Vector_container, or another list_continer, will have their own class-level vtbl)

The implementation of the caller needs only to know the location of the pointer to the vtbl in a Container and the index used for each virtual function. This virtual call mechanism can be made almost as efficient as the normal function call mechanism (within 25%). Its space overhead is one pointer in each object of a class with virtual functions plus one vtbl for each such class.

  • class holds the vtbl, which holds points to the base address of each virtual function.
    • this vtbl is per class, only one table for one class, share across all objects of that class.
  • object of a class holds a pointer to the vtbl, which points the base address of the vtbl.
    • Because this vtbl-ptr is added to each object of a class with virtual class, the size of the object += size_of(pointer)

4.5

A class hierarchy is a set of classes ordered in a lattice created by derivation. We use class hierarchies to represent concepts that have hierarchical relationship.

As far as representation is concerned, nothing (except the location of the pointer to the vtble) is common for every Shape.

void Smiley::draw() const {
    Circle::draw(); // call base class method
}

A virtual destructor is essential for an abstract class becuase an object of a derived class is usually manipulated through the interface provided by its abstrat base class. In particualr, it may be dleeted through a pointer to a base class. Then, the virtual function call mechanism ensures that the proper destructor is called. That destructor then implicitly invokes the destructors of its bases and members.

4.5.1

Concrete classes -- especialy classes with small representations -- are much like built-in types: we define them as local variables, access them using their names, copy them around, etc. Classes in class hierachies are different: we tend to allocate them on the free store using new, and we access them through pointers or references.

4.5.2

We can ask "is this Shape a kind of Smiley?" using the dynamic_cast operator:

Shape* ps{read_shape(cin)};
if (Smiley* p = dynamic_cast<Smiley*>(ps)) {  // does ps point to a Smiley?
    // ... a Smiley; use it
}
else {
   // ... not a Smiley, try something else
}

If at run time the object pointed to by the argument of dynamic_cast is not the expected type or class derived from the expected type, dynamic_cast returns nullptr. This test can often conveniently be placed in the initialization of a variable in a condition.

When a different type is unacceptable, we can simply dynamic_cast to a reference type. If the object is not the expected type, dynamic_cast throws a bad_cast exception:

Shapp* ps {read_shape(cin)};
Smiley& r{dynamic_cast<Smiley&>(*ps)};  // prepare to catch the bad_cast

Code is cleaner when dynamic_cast is used with restraint. If we can avoid using type information, we can write simpler and more efficient code, but occasionally type information is lost and must be recovered. Operations similar to dynamic_cast are known as "is kind of" and "is instance of" operations.

4.5.3

One simple solution to avoid naked pointer is to use a standard-library unique-ptr rather than a "naked pointer". As a pleasant side effect of this change, we no longer need to define a destructor for Smiley. The compiler will implicitly generate one that does the required destruction of the unique_ptrs.

4.6

  • Use concrete classes to represent simple concepts
  • Make a function a member only if it needs direct access to the representation of a class
  • If a class is a container, give it an initializer-list constructor
  • A class with a virtual function should have a virtual destructor.

Reference