Table of Contents

  1. Overview
  2. Define base and derived classes
  3. Virtual functions
  4. Abstract base classes
  5. Access control and inheritance
  6. Class scope under inheritance
  7. Constructors and copy control

(1) Object-Oriented Programming

The key ideas of OOP are data abstraction, inheritance, and dynamic binding.

(i) Overview ↑top

Base class:

class Quote {
public:
    std::string isbn() const;
    virtual double net_price(std::size_t n) const;
};

Derived class:
a derived class must include in its own class body a decl or all the virtual funcs it intends to define for itself. override can be used to note that the member func is an override of a virtual it inherits.

//Bulk_quote inherits from Quote
class Bulk_quote : public Quote {
public:
    double net_price(std::size_t) const override;
}

Dynamic binding:
through dynamic binding, we can use the same code to process objs of either type Quote or Bulk_quote interchangeably.

Dynamic binding happens when a virtual function is called through a reference (or a pointer) to a base class.

//cal and print the price for the given #copies
double print_total (ostream &os,
                        const Quote &item, size_t n) {
    //depending on the type of obj bound to item para
    //calls either Quote::net_price or Bulk_quote::net_price
    double ret = item.net_price(n);
    return ret;
}
-----------
//basic has type Quote; bulk has type Bulk_quote
print_total(cout, basic, 20); //calls Quote::net_price
print_total(cout, bulk, 20); //calls Bulk_quote::net_price

(ii) Define base and derived classes ↑top

definition

Base classes ordinarily should define a virtual destructor, which is needed even if it does no work.

class Quote {
public:
    Quote() = default; //default cstr
    Quote(const std::string &book, double sales_price):
                    bookNo(book), price(sales_price) { }
    std::string isbn() const { return bookNo; }
    //return the total sales price for the #items
    //derived classes will override and apply different
    // discount algorithms
    virtual double net_price(std::size_t n) const
        { return n*price; }
    virtual ~Quote () = default; //dynamic binding for dstr
private:
    std::string bookNo;          //ISBN number of this item
protected:
    double price = 0.0;          //normal, undiscountd price
};

Derived classes inherit the members of their base class. However, a derived class needs to be able to provide its own definitions for operations, e.g., net_price, that are type dependent.

Any nonstatic member func, other than a cstr, may be virtual. Member funcs that are not declared as virtual are resolved at compile time, not run time. When we call a virtual function through a pointer or reference, the call will be dynamically bound.

A derived class must specify from which class(es) it inherits in class derivation list. Each base class name may be preceded by an optional access specifier, which is one of public, protected, private.

Derived class frequently, but not always, override the virtual funcs that they inherit. If no override, then the derived class inherits the version defined in its base class.

class Bulk_quote : public Quote {
public:
    Bulk_quote() = default;
    Bulk_quote(const std::string&, double, 
                std::size_t, double);
    //overrides the base version to impl bulk purchase discount policy
    double net_price(std::size_t) const override;
private:
    std::size_t min_qty = 0; //min purchase for the discount to apply
    double discount = 0.0;   //fractional discount to appy
};

derived-to-base conversion

A derived object contains multiple parts: a subobject containing the (nonstatic) members defined in the derived class itself, plus subobjects corresponding to each base class.

Since a derived obj contains subparts of its base class(es), we can use an obj of a derived type as if it were an obj of its base type(s). In particular, we can bind a base-class ref or ptr to the base-class part of a derived obj:

Quote item;         //obj of base type
Bulk_quote bulk;    //obj of derived type
Quote *p = &item;   //p points to a Quote obj
p = &bulk;          //p points to the Quote part of bulk
Quote &r = bulk;    //r bound to the Quote part of bulk

The conversion is implicitly applied by compiler, which means that we can use an obj of derived type or a ref to a derived type when a ref to the base type is required. Likewise, we can use a ptr to a derived type where a ptr to the base type is required.

derived-class constructors

Although a derived obj contains members of base class, it cannot directly init those members; instead, it must use a base-class cstr to init its base-class part:

Bulk_quote(const std::string& book, double p, 
                std::size_t qty, double disc) :
        Quote(book, p), min_qty(qty), discount (disc) { }

The base class is inited first, and then the members of the derived class are inited in the order in which they are declared in the class.

The scope of a derived class is nested inside the scope of its base class; thus, there is no distinction between a member of the derived class uses its own members and base-class members. A derived class may access the public and protected members of its base class.

If a base class defines a static member, there is only one such member defined for the entire hierarchy. And static members obey normal access control.

preventing inheritance

We can prevent a class from being used as a base by following the class name with final:

class NoDerived final { };      //NoDerived can't be a base class
class Base { };
//Last is final; we cannot inherit from Last
class Last final : Base { ];    //Last can't be a base class
class Bad : NoDerived { };      //ERROR: NoDerived is final
class Bad2 : Last { };          //ERROR: Last is final

conversions and inheritance

Ordinarily, the type of a reference or pointer and the obj to which the reference/pointer refers must match exactly, with two exceptions:

double dval = 3.14;
doule &dref = dval; //OK
int &iref = dval;   //ERROR: not match

double d2;
double *pi = &d2;
int *i_pi = &d2;    //ERROR: not match

1) we can init a ref to const from any expr that can be converted.

double dval = 3.14;
const int &ri = dval;
---------compiler transformation
const int temp = dval; //create a temp const int from double
const int &ri = temp;  //bind ri to that temp
//Note: if no 'const', then we could change 'temp' but cannot change 'dval'

2) we can bind a ptr or ref to a base-class type to an obj of a type derived from the base class. When we use a ref (or ptr) to a base-class type, we don't know the actual type of the obj to which the ptr or ref is bound.

Quote item;
Bulk_quote bulk;
Quote *p = &item;   //p points to Quote obj
p = &bulk;          //p points to the Quote part of bulk
Quote &r = bulk;    //r bound to the Quote part of bulk

The conversion from derived to base exists because every derived obj contains a base-class part to which a ptr or ref of the base-class type can be bound. There is no similar guarantee for base-class objs, and thus no automatic conversion from the base class to derived.

Quote base;
Bulk_quote* bulkP = &base;  //ERROR: cannot convert base to derived
Bulk_quote& bulkRef = base; //ERROR: ditto

The automatic derived-to-base conversion applies only for conversions to a reference or pointer type. There is no such conversion from a derived-class type to the base-class type. Note that when we init or assign an obj of a class type, we are actually calling a func, cstr for init, and asgn operator for assign. These members normally have a para that is a ref to the const version of the class type.

When we init or assign an obj of a base type from an obj of a derived type, only the base-class part of the derived obj is copied, moved, or assigned. The derived part of the obj is ignored.

Bulk_quote bulk;  //obj of derived type
Quote item(bulk); //uses the Queue::Quote(const Quote&) cstr
item = bulk;      //calls Quote::operator=(const Quote&)

(iii) Virtual functions ↑top

C++ dynamic binding happens only when a virtual member func is called through a ref or a ptr to a base-class type. When a virtual func is called through a ref or ptr, the compiler generates code to decide at runtime which func to call.

Once a function is declared as virtual, it remains virtual in all the derived classes, which are not required to repeat the virtual keyword. When a derived class overrides a virtual, the parameters in the base and derived classes must match exactly, except only when virtuals return ref/ptr related by inheritance.

final and override

The compiler rejects a program if a func marked override does not override an existing virtual func; any attempt to override a func that has been defined as final will be flagged as error.

stuct B {
    virtual void f1(int) const;
    virtual void f2();
    void f3();
};
struct D2 : B {
    //inherits f2() and f3() from B and overrides f1(int)
    void f1(int) const final; //subseq classes can't override f1(int)
    void f2(int) override;    //ERROR: B has no f2(int) func
};
struct D3 : D2 {
    void f2();                //OK: overrides f2 inherited from indirect base B
    void f1(int) const;       //ERROR: D2 declared f2 as final
};

Virtual function implementation is based on virtual table.

default arguments

Virtual functions that have default arguments should use the same argument values in the base and derived classes.

circumventing the virtual mechanism

In some cases, we want to force the call to use a particular version of the virtual, which can be achieved with scope operator:

//calls the version from the base class regardless of the dynamic type of baseP
double undiscounted = baseP->Quote::net_price(42);

(iv) Abstract base classes ↑top

We can provide a pure virtual function, which does not have to be defined. We specify that a virtual func is a pure virtual by writing =0 in place of a func body. However, we can provide a definition for a pure virtual, but the func body must be defined outside the class.

double net_price(std::size_t) const = 0;

A class containing (or inheriting without overriding) a pure virtual function is an abstract base class. An abstract base class defines an interface for subsequent classes to override. We cannot (directly) create objects of a type that is an abstract base class. Classes that inherit from abstract base must define the pure virtual func, or those classes will be abstract as well.

(v) Access control and inheritance ↑top

protected members

A class uses protected for those members that it is willing to share with its derived or friend classes but wants to protect from general access.

A derived class member or friend may access the protected members of the base class only through a derived object. The derived class has no special access to the protected members of base-class objects.

class Base {
protected:
    int prot_mem;                   //protected member
};
class Sneaky : public Base {
    friend void clobber(Sneaky&);   //can access Sneaky::prot_mem
    friend void clobber(Base&);     //can't access
    int j;                          //j is private by default
};
-----------
//OK: clobber can access the private and protected
//  members in Sneaky objects
void clobber(Sneaky &s) { s.j = s.prot_mem = 0; }
//ERROR: clobber can't access the protected members in Base
void clobber(Base &b) { b.prot_mem = 0; }

public, private and protected inheritance

Access to a member that a class inherits is controlled by a combination of the access specifier for that member in the base class, and the access specifier in the derivation list of the derived class.

class Base {
public:
    void pub_mem(); //public member
protected:
    int prot_mem;   //protected member
private:
    char priv_mem;  //private member
};
struct Pub_Derv : public Base {
    //OK: derived classes can access protected members
    int f() { return prot_mem; }
    //ERROR: private members are inaccessible to derived classes
    char g() { return priv_mem; }
};
struct Priv_Derv : private Base {
    //private derivation doesn't affect access in derived class
    int f1() const { return prot_mem; }
};

accessibility of derived-to-base conversion

Whether the derived-to-base conversion is accessible depends on which code is trying to use the conversion and may depend on the access specifier used in the derived class' derivation. Assuming D inhertis from B:

For any given point in your code, if a public member of the base class would be accessible, then the derived-to-base conversion is also accessible, and not otherwise.

friendship and inheritance

Friendship is not transitive or inherited. Friends of the base have no special access to members of its derived classes, and friends of a derived class have no special access to the base class:

class Base {
    //added friend decl; other members as before
    friend class Pa1; //Pa1 has no access to classes derived from Base
};

class Pa1 {
public:
    int f(Base b) { return b.prot_mem; } //OK: Pa1 is friend of Base
    int f2(Sneaky s) { return s.j; }     //ERROR: Pa1 is not friend of Sneaky
    //access to a base class is controlled by the base class
    //  even inside a derived obj
    int f3(Sneaky s) { return s.prot_mem; } //OK: pa1is a friend
};

f3 is correct as it follows the notion that each class controls access to its own members. Pa1 is a friend of Base, so Pa1 can access the members of Base objects. That access includes access to Base objects that are embedded in an obj of a type derived from Base.

exempting individual members

A derived class may provide a using decl only for names it is permitted to access:

class Base {
public:
    std::size_t size() const { return n; }
protected:
    std::size_t n;
};

class Derived : private Base {
public:
    //maintain access levels for members related to the size of the obj
    using Base::size;
protected:
    using Base::n;
};

Because Derived uses private inheritance, the inherited members, size and n, are (by default) private members of Derived. The using decls adjust the accessibility of these members.

(vi) Class scope under inheritance ↑top

static type and dynamic type

The static type of an expr is always known at compile time - it is the type with which a var is declared or that an expr yields. The dynamic type is the type of the obj in memory that a var or expr represents. The dynamic type may not be known until run time.

double print_total(ostream &os, const Quote &item, size_t n) {
    double ret = item.net_price(n);
    return ret;
}

The static type of item is Quote&. The dynamic type depends on the type of the argument to which item is bound. That type cannot be known until a call is executed at run time. If we pass a Bulk_quote obj to print_total, then the static type (Quote&) of item will differ from its dynamic type (Bulk_quote).

The dynamic type of an expr that is neither a ref nor a ptr is always the same as that expr's static type. E.g., a var of type Quote is always a Quote obj. The static type of a ptr or ref to a base class may differ from its dynamic type.

name lookup happens at compile time

The static type of an obj, ref or ptr determines which members of that obj are visible.

class Disc_quote : public Quote {
public:
    std::pair<size_t, double> discount_policy() const {
        return {quantity, discount};
        //other members as before
    }
};

We can use discount_policy only through an obj, ptr, or ref of type Disc_quote or of a class derived from Disc_quote:

//Bulk_quote inherits from Disc_quote
Bulk_quote bulk;
Bulk_quote *bulkP = &bulk;  //static and dyn types are same
Quote *itemP = &bulk;       //static and dyn types differ
bulkP->discount_policy();   //OK: bulkP has type Bulk_quote*
itemP->discount_policy();   //ERROR: itemP has type Quote*

Even though bulk has a member named discount_policy, that member is not visible through itemP, whose type is a ptr to Quote, meaning that the search for discount_policy starts in class Quote. The Quote class has no member named discount_policy, so we cannot call that member on an obj, ref or ptr of type Quote.

Given the call p->mem() (or obj.mem()), the following steps happen:

class scope

Each class defines its own scope within which its members are defined. Under inheritance, the scope of a derived class is nested inside the scope of its base classes. If a name is unresolved within the scope of the derived class, the enclosing base-class scopes are searched for a def of that name.

A derived-class member with the same name as a member of the base class hides direct use of the base-class member.

We can use a hidden base-class member by using the scope operator.

Funcs defined in an inner scope do not overload funcs declared in an outer scope. As a result, funcs defined in a derived class do NOT overload members defined in its base class(es), but hide the funcs in base.

struct Base {
    int memfcn();
};
struct Derived : Base {
    int memfcn(int);    //hides memfcn in the base
};
Derived d; Base b;

b.memfcn();             //calls Base::memfcn
d.memfcn(10);           //calls Derived::memfcn
d.memfcn();             //ERROR: memfcn with no argu is hidden
d.Base::memfcn();       //OK: calls Base::memfcn

virtual functions and scope

Why virtual funcs must have the same para list in the base and derived classes?
If the base and derived members took arguments that differed from one another, there would be no way to call the derived version through a ref or ptr to the base class

(vii) Constructors and copy control ↑top

virtual destructors

A base class almost always needs a destructor, so that it can make the destructor virtual. Executing delete on a pointer to base that points to a derived obj has undefined behavior if the base's destructor is not virtual.

class Quote {
public:
    //virtual dstr needed if a base ptr pointing
    //  to a derived obj is deleted
    virtual ~Quote() = default; //dynamic binding for dstr
};

Quote *itemP = new Quote;       //same static and dynamic type
delete itemP;                   //dstr for Quote called
itemP = new Bulk_quote;         //static and dynamic types differ
delete itemP;                   //dstr for Bulk_quote called

virtual destructors turn off synthesized move: If a class defines a destructor - even if it uses =default to use the synthesized version - the compiler will not synthesize a move operation for that class.

synthesized copy control and inheritance

The synthesized copy-control members in a base or derived class execute like any other synthesized cstr, asgnment operator, or dstr. In addition, these synthesized members init, assign, or destry the direct base part of an obj by using the crspding operation from the base class.

The syned default cstr, or any of the copy-control members of either a base or a derived class, may be defined as deleted. The way in which a base class is defined can cause a derived-class member to be defined as deleted:

class B{
public:
    B();
    B(const B&) = delete;
    //other members, not including a move cstr
};
class D : public B {
    //no cstrs
};

D d;                //OK: D's syned default cstr uses B's default cstr
D d2(d);            //ERROR: d's syned copy cstr is deleted
D d3(std::move(d)); //ERROR: implicitly uses D's deleted copy cstr

derived-class copy-control members

The initialization phase of a derived-class cstr inits the base-class part(s) of a derived obj as well as init its own members. Likewise, the copy and move cstrs for a derived class must copy/move the members of its part as well as the members in the derived, and a derived-class asgnment op must assign the members in the base part of the derived obj.

Differently, the dstr is responsible only for destroying the resources allocated by the derived class. The base-class part of a derived obj is destroyed automatically.

defining a derived copy or move cstr
When a derived class defines a copy or move operation, that operation is rspl for copying or moving the entire obj, including base-class members.

class Base { /* ... */ };
class D : public Base {
public:
    //by default, the base class default cstr inits the base part of an obj
    //to use the copy or move cstr, we must explicitly call
    //that cstr in the cstr init list
    D(const D& d) : Base(d)         //copy the base members
        /* initializers for members of D */ { ... }
    D(D&& d) : Base(std::move(d))   //move the base members
        /* initializers for members of D */ { ... }
};

derived-class assignment operator
Like copy and move cstrs, a derived-class asgnment operator must assign its base part explicitly:

//Base::operator=(const Base&) is not invoked auto
D &D::operator=(const D &rhs) {
    Base::operator=(rhs); //assigns the base part
    //assigns the members in derived class, as usual
    return *this;
};

derived-class dstr
Unlike the cstrs and asgnment ops, a derived dstr is rspl only for destroying the rscs allocated by the derived class:

class D : public Base {
public:
    //Base::~Base invoked auto
    ~D() { /*do what it takes to clean up derived members*/ }
};

Objs are destroyed in the opposite order from which they are constructed: the derived dstr is run first, and then the base-class dstrs are invoked, back up through the inheritance hierarchy.

inherited constructors

A derived class can reuse the cstrs defined by its direct base class. A class may inherit constructors only from its direct base, and a class cannot inherit the default, copy and move cstrs. If the derived class does not directly define these cstrs, the compiler synthesizes them as usual.

class Bulk_quote : public Disc_quote {
public:
    using Disc_quote::Disc_quote; //inherit Disc_quote's cstr
    double net_price(std::size_t) const;
};

When applied to a cstr, a using decl causes the compiler to generate a derived cstr crspding to each cstr in the base. That is, for each cstr in the base class, the compiler generates a cstr in the derived class that has the same para list.

The compiler-generated cstrs have the form:

derived (parms) : base(args) { }

where derived is the name of the derived class, base is the name of the base class; parms is the para list of the cstr, and args pass the para from the derived cstr to the base cstr.

Bulk_quote(const std::string& book, double price,
                std::size_t qty, double disc) :
            Disc_quote(book, price, qty, disc) { }