CS304 — Final Term Summary (Lectures 23–45)
📘 Lecture 23 — Protected Access Specifier, IS A Relationship
📖 Overview: This lecture addresses the problem of accessing helper functions in derived classes when they are declared private in the base class. It introduces the protected access specifier as a solution that balances encapsulation with inheritance needs, and explains the IS A relationship and how derived class objects can be used where base class objects are required.
🗂️ Topics Covered
The lecture covers accessing base class member functions in derived classes and the limitation of private members for helper functions. It introduces the protected access specifier as a solution, explaining its scope and drawbacks. The concept of the IS A relationship in public inheritance is explained with examples, along with the rules for using derived class objects in place of base class objects using pointers and references. Static typing and its role in member access are also discussed.
📝 Lecture Summary
Accessing base class member functions in derived class
Public methods of a base class can be directly accessed in its derived class. However, some class member functions serve as helper functions for other class members and should not be called directly using a class object. For example, a function that checks if a string contains only integers, functions for encryption/decryption, or the IsLeapYear(int) function in a Date class.
These helper functions are typically made private because there is no need to access them using a class object directly. Making them private works fine until we derive a child class that needs those functions. For instance, a SpecialDate class derived from Date to handle only working days would need the IsLeapYear function, but it cannot access it because private members are only accessible within the class they belong to.
Solution: Modify Access Specifier — One solution is to make the helper function public, but this exposes it to everyone, which defeats the purpose of encapsulation.
🔑 Definition — Helper functions: Member functions written to assist other member functions of the class, not intended to be called directly by external code.
“protected” access specifier
C++ provides the protected access specifier for situations where a function in a base class should be accessible in its derived classes but not outside of the class. The scope of protected access is between private and public — it prevents external access (like private) while allowing access in derived classes (like public).
Protected members of a class cannot be accessed outside the class but are accessible in derived classes of that class. Protected members of a base class become protected members of the derived class (in public and protected inheritance; in private inheritance they become private).
class Date {
// ...
protected:
bool IsLeapYear(int );
};
int main(){
Date aDate;
aDate.IsLeapYear(); // Error — not accessible outside the class
return 0;
}
// In derived class — accessible
void SpecialDate::AddSpecialYear(int i) {
if(day == 29 && month == 2 && !IsLeapyear(year+i)) { /* OK */ }
}
🔑 Definition — Protected: An access specifier that makes members accessible within the class itself and in derived classes, but not outside the class hierarchy.
💡 Why this matters: Protected members allow derived classes to reuse base class implementation details without exposing them to the entire program.
Disadvantages of protected Members: Protected members break encapsulation because they become part of both the base class's implementation and the derived class's implementation. This violates the principle that "a class data members and functions should be encapsulated in the class itself."
📐 Principle: Protected members compromise encapsulation by making base class implementation details accessible to derived classes.
“IS A” Relationship
Public inheritance models the "IS A" relationship. For example:
- Line IS A Shape
- Circle IS A Shape
- Triangle IS A Shape
The general principle is: "Derived Object IS A kind of Base Object" — meaning a derived class object is a special kind of base object with extra properties (attributes and behavior).
Derived class objects can be used where base class objects are required because the derived class object contains an implicit base class object. However, the reverse is not true — a base class object cannot be used where a derived class object is required because a base class only has the base part, not the derived class part.
📌 Example:
class Person {
char * name;
public:
const char * GetName();
};
class Student: public Person {
int rollNo;
public:
int GetRollNo();
};
int main() {
Student sobj;
cout << sobj.GetName(); // OK — accessible from base
cout << sobj.GetRollNo(); // OK — accessible from derived
return 0;
}
Static Type
Static type is the type used to declare a reference or pointer. For example:
- In
Person * pPtr = 0;— the static type ofpPtrisPerson* - In
Student s;— the static type ofsisStudent
Member Access is determined by the static type of the pointer or reference. A base class pointer can hold the address of a derived class object, but it can only access the interface of the base class.
int main() {
Person * pPtr = 0;
Student s;
pPtr = &s; // OK — derived to base conversion
cout << pPtr->GetName(); // OK — GetName is in Person
cout << pPtr->GetRollNo(); // Error — GetRollNo not in Person
return 0;
}
This also applies to references:
int main() {
Person p;
Student s;
Person & refp = s; // Reference to derived object
cout << refp.GetName(); // OK
cout << refp.GetRollNo(); // Error
return 0;
}
Explicit use of IS A relationship: Using a base class reference or pointer to refer to a derived class object.
Implicit use of IS A relationship: Passing a derived class object to a function that expects a base class reference or pointer.
void Play(const Person& p) {
cout << p.GetName() << " is playing";
}
void Study(const Student& s) {
cout << s.GetRollNo() << " is studying";
}
int main() {
Person p;
Student s;
Play(p); // OK — Person reference initialized with Person
Play(s); // OK — Student IS A Person, implicit conversion
return 0;
}
🔑 Definition — Static Type: The type used to declare a pointer or reference variable, which determines at compile time which member functions can be accessed through that variable.
📐 Rule: A derived class object can always be used where a base class object is expected, but not vice versa.
⭐ Key Takeaways
The protected access specifier provides a middle ground between private and public, allowing derived classes to access base class members while preventing external access, though it partially breaks encapsulation. The IS A relationship means that a derived class object is a specialized version of the base class object and can be used wherever a base class object is expected. However, static typing determines member access — a base class pointer or reference can only access base class members, even when pointing to a derived class object. The implicit and explicit use of this relationship is crucial for polymorphic function calls and interface design in object-oriented programming.
🧠 Quick Revision Questions
- What is the problem with making helper functions private when you later need to create a derived class?
- How does the protected access specifier differ from both private and public?
- What is the main disadvantage of using protected members in terms of object-oriented principles?
- Can a base class pointer be used to call a derived class member function? Why or why not?
- What does static type mean, and how does it affect member access when using inheritance?
📘 Lecture 24 — Protected Access Specifier, IS-A Relationship, Copy Constructor and Inheritance
📖 Overview: This lecture explores key inheritance concepts in C++, including the protected access specifier and its role in derived classes, the IS-A relationship that governs public inheritance, and the detailed mechanics of copy constructors and assignment operators when working with base and derived classes. Understanding these topics is essential for building robust, memory-safe object-oriented programs.
🗂️ Topics Covered
The lecture covers protected member access specifiers and their significance in inheritance, the IS-A relationship that defines public inheritance, static type of identifiers and its role in member access, copy constructors in inheritance scenarios including compiler-generated default behavior, shallow vs deep copying, and assignment operators in derived classes. It also examines explicit vs implicit calls to base class member functions and assignment operators, and concludes with an appendix on type casting operations including upcasting, downcasting, static_cast, dynamic_cast, and reinterpret_cast.
📝 Lecture Summary
Protected Members
Protected members are between public and private - they are used in inheritance. From outside the class, no one can access protected members. However, any publicly derived class can access protected members, and they behave as protected members of the derived class. In a single standalone class, the protected access specifier has no significance, but when deriving a class, it becomes significant.
🔑 Definition — Protected access specifier: A member access level where members are accessible to derived classes but not to outside code.
IS-A Relationship
Inheritance is used when two classes have an IS-A kind of relationship. In C++, public inheritance is used for IS-A relationships. A publicly derived class pointer can be used for its base class pointer because the derived class is a kind of base class. For example, in "Student IS-A Person" relationship, a student has all properties of a person (walks, eats, drinks) but also has extra properties like studying in a program. The compiler treats assigning a derived class pointer to a base class pointer as an explicit manifestation of the IS-A relationship.
Static Type of an Identifier
The static type of an identifier is the type used to declare it. For example, Student * student; has a static type of Student, and Person * person; has a static type of Person. Access to members of an identifier is governed by its static type - the compiler uses static type to determine which members (functions and variables) the identifier can use.
🔑 Definition — Static type: The type used to declare an identifier, which governs member access at compile time.
Copy Constructor
A copy constructor is a member function of a class used to create an object by copying values from an already existing object.
Copy Constructor in Case of Inheritance
Consider two classes, Person (base) and Student (derived). Person has an attribute name, and Student has an attribute major. The code (Listing 24.1) demonstrates copy constructor behavior in inheritance. In main(), creating Student sobj1("Ali","Computer Science") then Student sobj2 = sobj1; invokes the compiler-generated default copy constructor for Student, which in turn calls the copy constructor of the base class Person. The base class anonymous object is created first, then the derived part is created.
💡 Why this matters: The order of construction (base first, then derived) is critical when using copy constructors, as the derived class copy constructor must properly initialize the base class part.
Shallow Copy
The compiler by default uses shallow copy. In shallow copy, both char * name and char * major pointers of both objects (sobj1 and sobj2) point to the same memory locations. The compiler generates a copy constructor for the derived class, calls the base class copy constructor, and then performs shallow copy of the derived class's data members. The problem with shallow copy is that when one object is destroyed (freeing its dynamically allocated memory), the other object's pointers become invalid (dangling pointers).
🔑 Definition — Shallow copy: A copy operation where only pointer values are copied, not the memory they point to, causing both objects to share the same dynamically allocated memory.
Deep Copy
Deep copy solves the shallow copy problem by writing custom copy constructor code. The solution is to allocate new dynamic memory for the copied object so it doesn't rely on the original object's memory. First, write a custom copy constructor for the base class Person:
Person::Person(const Person & rhs): name(NULL){
if (rhs.name != NULL) {
name = new char[strlen(rhs.name)+1];
strcpy(name,rhs.name);
}
}
When the derived class copy constructor is written, it must explicitly call the base class copy constructor from its initialization list. Without this explicit call, the compiler calls the base class default constructor (not the copy constructor), resulting in missing data (e.g., name being NULL). The correct derived class copy constructor is:
Student::Student(const Student & rhs) : Person(rhs), major(NULL) {
if (rhs.major != NULL) {
major = new char [strlen(rhs.major)+1];
strcpy(major,rhs.major);
}
}
🔑 Definition — Deep copy: A copy operation that allocates new memory for dynamically allocated members and copies the actual data, not just pointers.
📐 Copy constructor execution order: When Student sobj2 = sobj1; is executed with user-defined deep copy constructors:
- Student copy constructor is invoked
- Person copy constructor (base class) is called from initialization list
- Deep copy of base class data members (name)
- Deep copy of derived class data members (major)
Assignment Operator
The compiler also generates an assignment operator for a class if needed. In inheritance, when assigning one derived class object to another, the derived class copy assignment operator is invoked, which in turn calls the base class assignment operator. Dynamic memory allocation requires writing a user-defined assignment operator, similar to the copy constructor. The derived class assignment operator must explicitly call the base class assignment operator.
Calling Base Class Member Functions
There are two ways to call base class functions from derived class:
Explicit Way: Mention the base class name using Person::GetName(). Even without the base class name, the call works because the derived class can access its base class public methods.
Implicit Way: Use static_cast<const Person &>(*this).GetName() - casting the derived object to a base class reference and calling the method.
Assignment Operator in Derived Classes
The assignment operator of the base class is not automatically called when a derived class has a user-defined assignment operator. To call it explicitly:
Person::operator = (rhs); // Explicit call
To call it implicitly:
static_cast<Person &>(*this) = rhs; // C++ way of type casting
Before performing the deep copy of derived class data members, the code must delete previously allocated memory for major to avoid memory leaks: if (major != NULL) delete [] major;
⭐ Key Takeaways
Students must remember that the protected access specifier allows derived classes to access members while hiding them from external code, and public inheritance models the IS-A relationship. Copy constructors in inheritance require careful handling: if a derived class defines a custom copy constructor, it must explicitly call the base class copy constructor from its initialization list, otherwise the base class default constructor runs and data is lost. Shallow copy by the compiler creates dangling pointers when dynamic memory is involved, so deep copy must be implemented manually. Similarly, derived class assignment operators must call base class assignment operators and delete old dynamically allocated memory before performing deep copy. Casting operations like static_cast, dynamic_cast, and reinterpret_cast enable type conversions in inheritance, with upcasting (derived to base) being safer than downcasting (base to derived).
🧠 Quick Revision Questions
- What happens to the name attribute of a Student object when a derived class copy constructor is defined but does not explicitly call the base class copy constructor?
- Why does the compiler generate a copy constructor for the derived class even when the base class has a user-defined copy constructor?
- In shallow copy, what problem occurs when one object's destructor is called to free dynamically allocated memory?
- What is the correct syntax to implicitly call the base class assignment operator from a derived class assignment operator?
- Explain the difference between upcasting and downcasting, and name one casting operator suitable for each.
📘 Lecture 25 — Overriding Member Functions of Base Class in Derived Class (Function Overriding)
📖 Overview: This lecture explains how a derived class can override (redefine) member functions of its base class by providing a function with the same signature. It distinguishes function overriding from overloading, demonstrates practical examples including calling base class versions from derived class methods, and introduces the hierarchy of inheritance with direct and indirect base classes.
🗂️ Topics Covered
The lecture covers function overriding vs. function overloading, overriding member functions of base class (with examples of totally changing or extending function behavior), how to properly call a base class overridden method using scope resolution, the problem of recursive calls, and pointer behavior with overridden functions. It also introduces hierarchy of inheritance, direct base classes, and indirect base classes.
📝 Lecture Summary
Overloading vs. Overriding
Function Overloading occurs within the scope of one class — two functions with the same name but different parameters and return type. Function Overriding occurs across parent and child classes in an inheritance hierarchy — the derived class provides a function with the same signature (same name, parameters, and return type) as the base class function. Overriding within the scope of a single class causes a compilation error due to duplicate declaration.
🔑 Definition — Function Overriding: The derived class redefines a base class member function with the same signature to change or extend the behavior.
📌 Example: class Parent { public: void Func1(); void Func1(int); }; shows overloading; class Child : public Parent { public: void Func1(); }; shows overriding.
Overriding Member Functions of Base Class
A derive class can override a base class member function so that the working of the function is totally changed. Example: class ParalyzedPerson : public Person { public: void Walk(); };
A derive class can also override a base class member function so that the working is similar to the former implementation but extended. Example: class Student : public Person overrides Print() to display both name (from Person) and major.
class Student : public Person {
char * major;
public:
Student(char * aName, char* aMajor);
void Print(){
cout << "Name: " << GetName() << endl
<< "Major: " << major << endl;
}
};
Output: Name: Ahmed then Major: Computer Science
Calling Base Class Overridden Function from Derived Class
A derived class can call the base class member function from its overridden function to perform the base class part first, then add its own tasks. This aligns with OOP principles where each class handles its own responsibilities.
The wrong way causes infinite recursion:
void Print(){
Print(); // Calls itself recursively — infinite loop
cout << "Major:" << major << endl;
}
To fix this, use the scope resolution operator :::
void Print(){
Person::Print(); // Correctly calls base class Print
cout << "Major:" << major << endl;
}
Output: Name: Ahmed then Major: Computer Science
💡 Why this matters: Without scope resolution, the compiler calls the derived class version, causing infinite recursion. Scope resolution explicitly tells the compiler to call the base class version.
Pointer Behavior with Overridden Methods
When using pointers to call overridden methods, they are called according to the static type of the pointer, not the actual object type.
int main(){
Student a("Ahmad", "Computer Science");
Student *sPtr = &a;
sPtr->Print(); // Calls Student::Print() because sPtr is Student*
Person *pPtr = sPtr; // Static type is Person*
pPtr->Print(); // Calls Person::Print() because pPtr is Person*
return 0;
}
Output:
Name: Ahmed
Major: Computer Science
Name: Ahmed
This behavior is often undesirable — calling the base class version when you want the derived version. The lecture notes that this problem will be solved using virtual functions in a later lecture.
Hierarchy of Inheritance
Inheritance relationships are represented in a tree-like hierarchy. The root is the most base class, and branches represent derived classes.
🔑 Definition — Direct Base Class: A base class explicitly listed in a derived class's header with a colon (:). Example: class Child1 : public Parent1 { ... }; Here, Parent1 is a direct base class of Child1.
🔑 Definition — Indirect Base Class: A base class not explicitly listed in a derived class's header with a colon, but inherited from two or more levels up the hierarchy. Example:
class GrandParent { ... };
class Parent1 : public GrandParent { ... };
class Child1 : public Parent1 { ... }; // GrandParent is an indirect base class of Child1
⭐ Key Takeaways
Function overriding allows a derived class to redefine a base class member function with an identical signature (name, parameters, return type), whereas function overloading occurs within the same class with different parameters. When overriding, always use scope resolution (Base::Method()) to call the base class version from the derived version to avoid infinite recursion. Pointers call overridden functions based on their static type, not the actual object type — this limitation is later resolved by virtual functions. Finally, inheritance is organized as a hierarchy with direct base classes (explicitly listed) and indirect base classes (inherited from higher levels).
🧠 Quick Revision Questions
- What is the key difference between function overloading and function overriding in C++?
- Why does calling
Print()(without scope resolution) inside the Student'sPrint()method cause infinite recursion? - How do you correctly call the base class version of an overridden function from the derived class?
- Given a
Person*pointer pointing to aStudentobject, which version ofPrint()is called? Why? - What is the difference between a direct base class and an indirect base class? Give an example of each.
📘 Lecture 26 — Base Initialization
📖 Overview: This lecture explores the concept of base initialization in class hierarchies, focusing on which constructors a derived class can call during initialization. It also introduces the three types of inheritance in C++, with detailed coverage of private and protected inheritance, including practical examples of when each type is appropriate.
🗂️ Topics Covered
The lecture covers base initialization rules in class hierarchies, including the restriction that a child can only call its direct base class constructor. It then explains function overriding in hierarchy and presents the three types of inheritance: public, protected, and private. Detailed examples are provided for private inheritance, demonstrating how to model "implemented in terms of" relationships using the Set and Collection classes.
📝 Lecture Summary
26.1. Base Initialization
In class hierarchies, the child class can only call the constructor of its direct base class to perform initialization using its constructor initialization list. The child cannot call the constructor of any of its indirect base classes for initialization.
🔑 Definition — Direct Base Class: The immediate parent class from which a derived class directly inherits. 🔑 Definition — Indirect Base Class: A class that is higher in the hierarchy but not the immediate parent (e.g., grandparent).
📌 Example:
class GrandParent {
int gpData;
public:
GrandParent() : gpData(0) {...}
GrandParent(int i) : gpData(i) {...}
void Print() const;
};
class Parent1: public GrandParent {
int pData;
public:
Parent1() : GrandParent(), pData(0) {...}
};
class Child1 : public Parent1 {
public:
Child1() : Parent1() {...}
Child1(int i) : GrandParent(i) //Error: indirect base class
{...}
void Print() const;
};
In the above, Child1 constructor cannot call GrandParent's constructor from its initialization list because GrandParent is an indirect base class.
Overriding in class hierarchy means that a child class can override (redefine) the function of any of its parent classes, whether direct or indirect.
📌 Example of overriding:
void GrandParent::Print() {
cout << "GrandParent::Print" << endl;
}
void Child1::Print() {
cout << "Child1::Print" << endl;
}
int main(){
Child1 obj;
obj.Print(); // Calls Child1::Print
obj.Parent1::Print(); // Calls GrandParent::Print (no override in Parent1)
obj.GrandParent::Print(); // Calls GrandParent::Print directly
return 0;
}
Output:
Child1::Print
GrandParent::Print
GrandParent::Print
💡 Why this matters: Understanding which constructors can be called is essential for proper object initialization in deep hierarchies. The function overriding mechanism allows polymorphic behavior while preserving access to parent versions through scope resolution.
26.2. Types of Inheritance
There are three types of inheritance specified using keywords: public, protected, and private.
a. Public Inheritance
class Child: public Parent {...};
| Member access in Base Class | Derived Class |
|---|---|
| Public | Public |
| Protected | Protected |
| Private | Hidden |
b. Protected Inheritance
class Child: protected Parent {...};
| Member access in Base Class | Derived Class |
|---|---|
| Public | Protected |
| Protected | Protected |
| Private | Hidden |
c. Private Inheritance
class Child: private Parent {...};
| Member access in Base Class | Derived Class |
|---|---|
| Public | Private |
| Protected | Private |
| Private | Hidden |
If the user does not specify the type of inheritance, the default type is private inheritance:
class Child: Parent {...}; // Equivalent to: class Child: private Parent {...};
26.3. Private Inheritance
Private inheritance is used when we want to reuse the code of some class. It models an "Implemented in terms of" relationship.
🔑 Definition — "Implemented in terms of": A design relationship where a class uses the implementation of another class but does not expose that class's interface to the outside world.
📌 Example: Collection and Set classes
Consider a class Collection that stores elements:
class Collection {
...
public:
void AddElement(int);
bool SearchElement(int);
bool SearchElementAgain(int);
bool DeleteElement(int);
};
This Collection class supports:
- AddElement: to add elements to the collection
- SearchElement: searches for any element; returns true as soon as found
- SearchElementAgain: finds the second instance of an element; returns true on duplicate
- DeleteElement: deletes any entry from the collection
The Collection class allows duplicate elements. Now suppose we want to implement a Set class, which has similar functionality but cannot allow duplicate elements. We can use inheritance but not public inheritance because that would expose all Collection functions through the Set interface. We only want to use some Collection functions internally to implement Set functionality.
Using private inheritance achieves two main advantages:
- Specialization of the class according to Set requirements (removing duplicate elements)
- Making the Collection class interface inaccessible from outside world using a Set class reference
📌 Class Set implementation:
class Set: private Collection {
private:
...
public:
void AddMember(int);
bool IsMember(int);
bool DeleteMember(int);
};
void Set::AddMember(int i){
if (!IsMember(i))
AddElement(i); // Only adds if not already a member
}
bool Set::IsMember(int i){
return SearchElement(i);
}
💡 Why this matters: Private inheritance allows you to leverage existing code while completely hiding the base class interface. This is crucial when you want to inherit implementation without inheriting interface — a common scenario in code reuse.
⭐ Key Takeaways
A derived class can only call its direct base class constructor from its initialization list, not any indirect base class constructors. The three types of inheritance — public, protected, and private — control how base class members are accessible in the derived class and beyond. Public inheritance models an "is-a" relationship, while private inheritance models an "implemented in terms of" relationship suitable for code reuse without interface exposure. If no inheritance type is specified, C++ defaults to private inheritance. The default type is private inheritance, which is essential to remember for exam questions involving unspecified inheritance.
🧠 Quick Revision Questions
- Why is calling
GrandParent(i)inChild1's initialization list an error in C++? - What are the three types of inheritance in C++ and what keywords specify them?
- What is the default type of inheritance if none is specified?
- In private inheritance, what access level do public members of the base class get in the derived class?
- Why would you use private inheritance instead of public inheritance for implementing a Set class from a Collection class?
📘 Lecture 27 — Private Inheritance and Protected Inheritance
📖 Overview: This lecture continues the discussion of private inheritance, emphasizing its role in implementing specialization (restriction) through the “implemented in terms of” relationship. It then introduces protected inheritance, comparing its properties to private and public inheritance, and explains how each type controls the accessibility of base class members in derived classes and the outside world.
🗂️ Topics Covered
The lecture begins with a detailed review of private inheritance using the Collection and Set example. It then formally defines specialization (restriction) and explains how private inheritance explicitly implements it. The essential properties of private inheritance are listed, including conversion rules, constructor calling, and restrictions for classes more than one level down the hierarchy. Finally, protected inheritance is introduced and its properties are compared to private and public inheritance through a table and a code example.
📝 Lecture Summary
27.1. Specialization (Restriction)
In specialization, the derived class is behaviourally incompatible with the base class. Behaviourally incompatible means that the base class cannot always be replaced by the derived class. Specialization is represented by the “Implemented in terms of” relationship and can be implemented using both private and protected inheritance.
🔑 Definition — Specialization (Restriction): A relationship where the derived class restricts or modifies the behavior of the base class, making it behaviourally incompatible. The base class cannot always be substituted by the derived class.
📌 Example: The Person class has an age attribute with a range of [0..125] and a setAge() function that accepts any age in that range. The Adult class specializes Person by restricting the age range to [18..125]. If someone tries to set an age below 18 in Adult, it generates an error. This shows restriction because the Adult class cannot be used everywhere the Person class is expected (it is behaviourally incompatible).
Essential Properties of Private Inheritance
- In Private Inheritance, only member functions and friend classes or functions of a derived class can convert a pointer or reference of the derived object to that of the parent object.
📌 Example:
void DoSomething(const Parent &);
Child::Child(){
Parent & pPtr = static_cast<Parent &>(*this); // fine
DoSomething(pPtr);
// DoSomething(*this); // this single line is equal to two lines above.
}
- As in public inheritance, the child class object has an anonymous object of the parent class.
- As in public inheritance, the default constructor and copy constructor of the parent class are called when constructing an object of the derived class.
📌 Example:
class Parent{
public:
Parent(){ cout << “Parent Constructor”; }
Parent(const Parent & prhs){ cout << “Parent Copy Constructor”; }
};
class Child: private Parent{
public:
Child(){ cout << “Child Constructor”; }
Child(const Child & crhs) : Parent(crhs){ cout << “Child Copy Constructor”; }
};
int main() {
Child cobj1; // default constructor will be invoked
Child cobj2 = cobj1; // copy constructor will be invoked
return 0;
}
Output:
Parent Constructor
Child Constructor
Parent Copy Constructor
Child Copy Constructor
- In private inheritance, the derived class that is more than one level down the hierarchy cannot access the member functions of the grandparent class. This is because
publicandprotectedmembers of the derived class become private members of the privately derived class for all practical purposes.
📌 Example:
class GrandParent{
public:
void DoSomething();
};
class Parent: private GrandParent{
void SomeFunction(){ DoSomething(); } // OK
};
class Child: private Parent{
public:
Child() { DoSomething(); } // Error
};
- In private inheritance, the derived class that is more than one level down the hierarchy cannot convert its pointer or reference to that of the GrandParent. The reason is that private inheritance implements specialization, and all features of the base class are restricted only to the privately derived class and its friends.
📌 Example:
void DoSomething(GrandParent&);
class GrandParent{};
class Parent: private GrandParent{
public:
Parent() {DoSomething(*this);} // fine
};
class Child: private Parent {
public:
Child() { DoSomething(*this); } // Error
};
💡 Why this matters: These restrictions ensure that the specialized behavior of a privately derived class is not accidentally used in contexts where the base class is expected, maintaining the integrity of the specialization.
27.2. Protected Inheritance
If a class D has been derived using protected inheritance from class B (if B is a protected base and D is derived class), then public and protected members of B can be accessed by member functions and friends of class D and classes derived from D. Protected inheritance is used to build class hierarchy using the “Implemented in terms of” relationship.
class GrandParent{
public:
void DoSomething();
};
class Parent: protected GrandParent{
void SomeFunction(){ DoSomething(); } // OK
};
class Child: protected Parent{
public:
Child() { DoSomething(); } // OK (unlike private inheritance)
};
💡 Why this matters: Protected inheritance lies between public and private inheritance. It allows the derived class and its children to access the base class features, but the outside world cannot. This is useful when you want to extend a hierarchy without exposing the base class interface to external users.
27.3. Properties of Protected Inheritance
If B is a protected base and D is derived class, then only friends and members of D and friends and members of classes derived from D can convert D* to B* or D& to B&. (In private inheritance, only the derived class or its friends can convert pointer to base class.)
📌 Example:
void DoSomething(GrandParent&);
class GrandParent{};
class Parent: protected GrandParent{};
class Child: protected Parent {
public:
Child() { DoSomething(*this); } // fine
};
Comparison of Public, Protected, and Private Inheritance
| Accessibility of Base Class Public Members | Public Inheritance | Protected Inheritance | Private Inheritance |
|---|---|---|---|
| In Derived 1 | Yes | Yes | Yes |
| In Derived 2, Derived 3, ... (further down hierarchy) | Yes | Yes | No |
| In Main (outside world) | Yes | No | No |
- Private data members will NOT be accessible in any derived class or in the main function.
- Protected data members will become private data members in the case of private inheritance and protected data members of the derived class in the case of protected inheritance.
A Good Programming Exercise
A good programming exercise would be to write a program that shows the accessibility of all types of member functions for all types of inheritance in derived classes.
⭐ Key Takeaways
- Specialization (Restriction) is “implemented in terms of” and is used when the derived class is behaviourally incompatible with the base class. It can be implemented using both private and protected inheritance.
- In private inheritance, only the derived class (and its friends) can access base class members and convert pointers/references to the base class. Classes further down the hierarchy cannot access grandparent members or convert to grandparent references.
- Protected inheritance allows the derived class and its descendants to access the base class’s public and protected members, but the outside world cannot. It sits between public and private inheritance.
- The choice of inheritance type controls how much of the base class interface is exposed: public allows outside access, protected allows descendants access, private restricts access to only the immediate derived class.
- In all inheritance types, the base class default and copy constructors are called when constructing derived objects, and an anonymous base class sub-object exists within the derived object.
🧠 Quick Revision Questions
- What is the difference between public inheritance and private inheritance in terms of accessibility of base class public members in the main function?
- In the specialization (restriction) example of
PersonandAdult, why can’tAdultreplacePersoneverywhere? - In private inheritance, what happens when a class tries to access a grandparent’s member function from a class two levels down?
- In protected inheritance, who can convert a derived class pointer to a base class pointer?
- What are the three types of inheritance, and which one(s) implement the “implemented in terms of” relationship?
📘 Lecture 28 — Virtual Functions
📖 Overview: This lecture introduces virtual functions in C++ to solve a classic OOP problem: how to call the correct derived class method when using a base class pointer array. It demonstrates why a simple inheritance-based approach fails and how virtual functions enable automatic runtime method dispatch, eliminating the need for complex type-checking code.
🗂️ Topics Covered
The lecture begins with a problem statement about drawing different geometric shapes from an array of Shape pointers. It presents a Shape hierarchy with Line, Circle, and Triangle classes, then shows the failure of a naive inheritance approach where the base class draw() method is always called. The lecture explores solution 1 involving switch/if-else logic with type checking and explains why this leads to delocalized, unmaintainable code. Finally, it introduces virtual functions as the proper OOP solution, demonstrates their implementation in the Shape hierarchy, and contrasts static versus dynamic binding.
📝 Lecture Summary
Virtual Functions
Problem Statement: Develop a function that can draw different types of geometric shapes from an array
The lecture presents a Shape Hierarchy with a base Shape class and derived classes Line, Circle, and Triangle. Each derived class has its own draw() and calcArea() methods. The goal is to implement a function that takes a Shape pointer array (Shape * []) and its size as parameters, then draws the appropriate shape for each element.
The key idea is that a base class pointer can store pointers to any of its publicly derived classes (IS-A relationship). This allows us to avoid complex code that checks class types individually. Instead, we can use a single function call in a loop:
void drawShapes(Shape *array[], int size) {
for (int i = 0; i < size; i++)
array[i]->draw(); // Should work for Line, Circle, Triangle
}
Implementation
A general Shape class is created with a draw() method that prints "Shape\n" and a calcArea() method returning 0. Derived classes Line, Circle, and Triangle override draw() with their own implementations.
In main(), objects of Line, Circle, and Triangle are created dynamically and stored in a Shape pointer array:
Shape* _shape[10];
Point p1(0, 0), p2(10, 10);
shape[1] = new Line(p1, p2);
shape[2] = new Circle(p1, 15);
When drawShapes() is called with this array, the output shows "Shape" repeatedly instead of the correct class names (Line, Circle, Triangle).
Why does this happen? The problem is that the static type of the array is Shape*, so the draw() method of the Shape class is always called, regardless of whether the actual pointer stored is a Line, Circle, Triangle, or Shape pointer.
Solution 1: Type Checking with Switch/If-Else
One solution modifies drawShapes() to determine the object type and call the appropriate method:
void drawShapes(Shape* _shape[], int size) {
for (int i = 0; i < size; i++) {
switch (_shape[i]->getType()) {
case 'L':
static_cast<Line*>(_shape[i])->draw();
break;
case 'C':
static_cast<Circle*>(_shape[i])->draw();
break;
// ...
}
}
}
Equivalent if-else logic:
if (_shape[i]->getType() == 'L')
static_cast<Line*>(_shape[i])->draw();
else if (_shape[i]->getType() == 'C')
static_cast<Circle*>(_shape[i])->draw();
Sample Output: Line, Circle, Triangle, Circle...
While this approach works, it has serious Problems:
🔑 Delocalized Code: If we need another function like printArea(), we must write the same switch/if-else logic again:
void printArea(Shape* _shape[], int size) {
for (int i = 0; i < size; i++) {
switch (_shape[i]->getType()) {
case 'L':
static_cast<Line*>(_shape[i])->calcArea();
break;
case 'C':
static_cast<Circle*>(_shape[i])->calcArea();
break;
// ...
}
}
}
This leads to two main consequences:
- Writing the same code repeatedly may produce errors if the programmer forgets to add switch cases.
- Adding a new Shape class requires adding a new case in all functions using this logic—if even one function is missed, the program shows incorrect output.
Such code is very hard to maintain.
Solution? We need a mechanism that can select the message target (class) automatically—without switch statements.
Polymorphism Revisited
In the OO model, polymorphism means different objects can behave in different ways for the same message (stimulus). Consequently, the sender of a message does not need to know the exact class of the receiver.
With correct inheritance structure and polymorphism, adding more shapes should work automatically—the appropriate draw() method should be called without modifying existing code. This is the benefit of OOP:
void drawShapes(Shape *array[], int size) {
for (int i = 0; i < size; i++)
array[i]->draw(); // Automatically calls correct class's draw()
}
To achieve this functionality, we need the concept of virtual functions—making those functions virtual in the base class that will be implemented by derived classes according to their requirements.
Virtual Functions: Definition and Implementation
🔑 Virtual Functions — Functions that achieve automatic runtime target class selection, eliminating the need for complex switch/if-else type-checking code.
Key characteristics:
- The target class of a virtual function call is determined at run-time automatically
- In C++, we declare a function virtual by preceding the function header with the keyword
virtual
Virtual Functions in the Shape Hierarchy
Functions that need to be overridden by derived classes are made virtual in the base class:
class Shape {
// ...
virtual void draw();
virtual int calcArea();
};
Derived classes then override these virtual functions:
class Line : public Shape {
// ...
virtual void draw() { cout << "Line...\n"; }
};
class Circle : public Shape {
// ...
virtual void draw() { cout << "Circle...\n"; }
virtual int calcArea();
};
class Triangle : public Shape {
// ...
virtual void draw() { cout << "Triangle...\n"; }
virtual int calcArea();
};
With virtual functions, the drawShapes() implementation becomes simple and works correctly:
void drawShapes(Shape* _shape[], int size) {
for (int i = 0; i < size; i++) {
_shape[i]->draw(); // Automatically calls correct class
}
}
Sample Output: Line, Circle, Triangle, Circle...
Similarly, printArea() works without switch logic:
void printArea(Shape* _shape[], int size) {
for (int i = 0; i < size; i++) {
cout << _shape[i]->calcArea();
cout << endl;
}
}
💡 Why this matters: Virtual functions automatically determine which class's method to call at runtime, making code extensible and maintainable. Adding a new shape class works without modifying existing functions.
Static vs Dynamic Binding
🔑 Static Binding — The target function for a call is selected at compile time.
Example:
Line _line;
_line.draw(); // Always Line::draw will be called (determined at compile time)
🔑 Dynamic Binding — The target function for a call is selected at run time.
Example:
Shape* _shape = new Line();
// Without virtual: Shape::draw() is called (static binding on static type Shape*)
// With virtual: Line::draw() is called (dynamic binding at runtime)
When draw() is not virtual, the call _shape->draw() invokes Shape::draw() because the compiler uses the static type Shape*.
When draw() is virtual in the base Shape class, the call _shape->draw() invokes Line::draw() because the runtime determines the actual object type and calls the appropriate overridden method.
⭐ Key Takeaways
This lecture teaches that virtual functions are the correct OOP mechanism for achieving runtime polymorphism, allowing base class pointers to correctly call derived class methods without manual type checking. Students must understand that static binding selects methods at compile time based on pointer/reference type, while dynamic binding (via virtual functions) selects methods at runtime based on the actual object type. The delocalized code problem with switch/if-else approaches makes code unmaintainable and error-prone when adding new derived classes. Virtual functions eliminate this problem by automatically routing method calls to the appropriate class implementation. Remember that declaring a function virtual in the base class with the virtual keyword enables dynamic binding for that function throughout the inheritance hierarchy.
🧠 Quick Revision Questions
-
Why did the naive inheritance approach (without virtual functions) print "Shape" instead of "Line", "Circle", or "Triangle" when calling draw() through a Shape pointer array?
-
What are the two main problems with using switch/if-else type-checking to call the correct derived class method?
-
How does declaring a function as
virtualin the base class change how the compiler handles function calls through base class pointers? -
What is the difference between static binding and dynamic binding? Give one example of each from the shape hierarchy.
-
In the Shape hierarchy, if a new class
Rectangleis added with its owndraw()andcalcArea()methods, would the existingdrawShapes()andprintArea()functions work without modification? Explain why or why not.
📘 Lecture 29 — Abstract Classes, Virtual Destructors, and Dynamic Dispatch
📖 Overview: This lecture completes the discussion on polymorphism by introducing abstract classes and pure virtual functions as essential tools for designing class hierarchies. It also addresses the critical problem of memory leaks when deleting derived objects through base pointers and explains how virtual destructors solve this. Finally, it demystifies how the compiler actually implements polymorphic behavior using virtual tables (vTables) and dynamic dispatch.
🗂️ Topics Covered
The lecture covers abstract classes versus concrete classes, pure virtual functions and how to declare them in C++, the Shape class hierarchy with abstract intermediate classes like Quadrilateral, the problem of memory leaks when destructors are non-virtual, virtual destructors and their proper destruction order, two usage patterns for virtual functions (inheriting interface alone vs. inheriting interface and implementation), and finally the internal mechanism of vTables and dynamic dispatch.
📝 Lecture Summary
Previous Lecture Review
The main concept of Polymorphism is that the same method can behave differently according to the object with respect to which it has been called. Related classes with similar functionality are combined in a class hierarchy (Shape, Line, Circle, Triangle) where draw and calcArea methods show polymorphic behavior. This type of polymorphism is achieved using virtual functions. The advantage is that the sender simply passes a method call and the appropriate method is called automatically, as demonstrated by using a for loop to draw all shapes using a shape pointer array. Without virtual functions, achieving the same functionality requires complex, delocalized switch statements.
💡 Why this matters: Virtual functions enable object-oriented code that is extensible and maintainable — new shape classes can be added without modifying existing drawing code.
29.1. Abstract Classes
An Abstract Class represents an abstract concept with no direct real-world existence. In the Shape class hierarchy, there is no real object named "Shape" — only specific shapes like Line, Circle, or Triangle exist. Abstract classes cannot be instantiated; they are used only for inheriting interface and/or implementation. The actual behavior (like draw and calcArea) is realized in derived classes.
29.2. Concrete Classes
Concrete Classes implement a concrete concept. They can be instantiated and may inherit from an abstract class or another concrete class. All classes studied so far in the course have been concrete classes.
29.3. Abstract Classes in C++
In C++, a class can be made abstract by making one or more of its functions pure virtual. Conversely, a class with no pure virtual functions is a concrete class (its objects can be instantiated).
29.4. Pure Virtual Functions
A Pure Virtual Function represents an abstract behavior and may have no implementation. For example, the draw method in Shape class represents abstract behavior since Shape itself has no real-world existence. A function is declared pure virtual by following its header with = 0:
virtual void draw() = 0;
A class having at least one pure virtual function becomes abstract and cannot be instantiated:
class Shape {
public:
virtual void draw() = 0;
};
Shape s; // Error! Cannot instantiate abstract class
🔑 Definition — Pure Virtual Function: A virtual function declared with = 0 that has no implementation in the base class, forcing derived classes to override it.
📐 Formula: virtual returnType functionName(parameters) = 0; → The function is purely abstract; only its interface is inherited.
📌 Example: virtual void draw() = 0; in Shape. Line, Circle, and Triangle must each provide their own implementation of draw().
29.5. Shape Hierarchy
A derived class of an abstract class remains abstract until it provides implementation for all pure virtual functions. Abstract classes are present at the root or near the root of the class hierarchy, while concrete classes are near the leaves.
In the hierarchy: Shape (abstract) → Quadrilateral (abstract, inherits from Shape but does not override draw) → Rectangle (concrete, overrides draw).
class Quadrilateral : public Shape {
// No overriding draw() method — Quadrilateral remains abstract
};
Quadrilateral q; // Error! Cannot instantiate abstract class
class Rectangle : public Quadrilateral {
public:
virtual void draw() { // draw is still virtual throughout hierarchy
// function body
}
};
Rectangle r; // OK — Rectangle is concrete
💡 Why this matters: At least one concrete class must exist at a leaf of the hierarchy; otherwise, the entire hierarchy would be uninstantiable and useless.
29.6. Virtual Destructors
When a derived class object is deleted through a base class pointer, the destructor is called according to the static type of the pointer (the pointer's declared type, not the object's actual type). If the base class destructor is not virtual, only the base class destructor runs, leaving the derived parts of the object undeleted — causing a memory leak.
Example of the problem:
class Shape {
public:
~Shape() { cout << "Shape destructor called\n"; }
};
class Rectangle : public Quadrilateral {
public:
~Rectangle() { cout << "Rectangle destructor called\n"; }
};
int main() {
Shape* pShape = new Rectangle();
delete pShape; // Only "Shape destructor called" — memory leak!
return 0;
}
Output: Shape destructor called — only the base part is destroyed; the Rectangle part remains allocated.
The Solution — Virtual Destructors:
Make the base class destructor virtual, just as we made draw virtual:
class Shape {
public:
virtual ~Shape() { cout << "Shape destructor called\n"; }
};
class Rectangle : public Quadrilateral {
public:
virtual ~Rectangle() { cout << "Rectangle destructor called\n"; }
};
int main() {
Shape* pShape = new Rectangle();
delete pShape; // All destructors called in correct order
return 0;
}
Output:
Rectangle destructor called
Quadrilateral destructor called
Shape destructor called
Now, the derived class destructor runs first, then the base class destructor — ensuring the complete object is properly destroyed with no memory leak.
🔑 Definition — Virtual Destructor: A destructor declared with the virtual keyword in a base class, ensuring that when an object is deleted through a base class pointer, the correct derived class destructor is called first, followed by all base class destructors in reverse order of inheritance.
📌 Example: virtual ~Shape() {} ensures that deleting through Shape* correctly calls ~Rectangle(), ~Quadrilateral(), and then ~Shape().
29.7. Virtual Functions – Usage
Virtual functions are used in two ways:
-
Inherit interface AND implementation (Simple Virtual Functions): Both base and derived classes have implementations. The base provides a default that derived classes may override.
-
Inherit interface ONLY (Pure Virtual Functions): Only derived classes provide implementation; the base class has no implementation (or a minimal one).
Example combining both:
class Shape {
public:
virtual void draw() = 0; // pure virtual — inherit interface only
virtual float calcArea() { // simple virtual — inherit interface and default implementation
return 0;
}
};
draw()is pure virtual because every shape must draw itself — each concrete derived class must override it.calcArea()is simple virtual because some shapes (like Line and Point) have no area — they inherit the default implementation that returns 0, while shapes like Circle and Triangle override it.
29.8. V Table (Virtual Table)
The compiler keeps track of virtual functions using a Virtual Function Table (vTable) for each class that has virtual functions. A vTable contains a pointer for each virtual function in the class.
How vTables work with the example:
int main() {
Point p1(10,10), p2(30,30);
Shape* pShape;
pShape = new Line(p1, p2);
pShape->draw();
pShape->calcArea();
return 0;
}
- When the code is compiled, vTables and implementation code for virtual and non-virtual functions are generated for all classes.
- Each class gets its own vTable:
Shape vTable(with pointers to Shape'sdrawandcalcArea) andLine vTable(with pointers to Line'sdrawandcalcArea). - When a
Lineobject is created, it contains a hidden pointer toLine vTable. - The
Shape* pShapestores the address of the Line object.
🔑 Definition — vTable: A per-class table created by the compiler containing function pointers for every virtual function in the class. Each object of a class with virtual functions carries a hidden pointer to its class's vTable.
29.9. Dynamic Dispatch (Dynamic Binding)
Dynamic Dispatch occurs in the case of virtual functions. For non-virtual functions, the compiler simply generates code to call the function directly. For virtual functions, the compiler generates code to:
- Access the object
- Access the associated vTable (through the object's hidden vTable pointer)
- Call the appropriate function (the one pointed to by the vTable entry)
This is why pShape->draw() calls Line::draw() even though pShape is declared as Shape* — because at runtime, the vTable of the actual object (Line) is used.
⭐ Key Takeaways
Abstract classes with pure virtual functions are essential for designing polymorphic class hierarchies where the base class represents a general concept with no physical instantiation. Every class with virtual functions should have a virtual destructor to prevent memory leaks when deleting derived objects through base class pointers — this ensures destructors run in the correct order (most derived first, base last). Virtual functions come in two flavors: simple virtual (inherit interface AND default implementation) and pure virtual (inherit interface only, forcing every concrete derived class to implement the method). The compiler implements polymorphism through vTables and dynamic dispatch, which adds both memory and processing overhead — so virtual functions should be used with care, balancing the need for extensibility against performance requirements.
🧠 Quick Revision Questions
-
What is the difference between an abstract class and a concrete class? Can you instantiate an object of an abstract class?
-
How do you declare a pure virtual function in C++? Write the syntax for a pure virtual function
draw()inside aShapeclass. -
What problem occurs if you delete a derived class object through a base class pointer and the base class destructor is NOT virtual? What is the resulting output?
-
What is a vTable and how does it enable dynamic dispatch? What three steps does the compiler generate code for when calling a virtual function?
-
Explain the difference between "inheriting interface only" and "inheriting interface and implementation" using virtual functions. Give a concrete example from the Shape hierarchy.
📘 Lecture 30 — Polymorphism – Case Study: A Simple Payroll Application
📖 Overview: This lecture demonstrates a practical implementation of polymorphism using a payroll system for different employee types. It also warns against using arrays polymorphically, explaining the critical distinction between pointer arrays and object arrays when working with polymorphic behavior.
🗂️ Topics Covered
The lecture covers a complete case study of a payroll application using polymorphism with an Employee base class and three derived classes (SalariedEmp, HourlyEmp, CommEmp). It then revisits the Shape hierarchy to demonstrate the dangerous consequences of treating object arrays polymorphically, explaining why pointer arrays must be used instead.
📝 Lecture Summary
30.1. Polymorphism – Case Study: A Simple Payroll Application
The lecture presents a payroll application for a company with three employee types: salaried employees, hourly employees, and commissioned employees. The system uses an array of Employee pointers to calculate salaries polymorphically and generate reports.
The Employee class is an abstract base class with a pure virtual function calcSalary(). It contains private members name and taxRate, with a constructor that initializes these values.
🔑 Definition — Abstract Base Class: A base class that cannot be instantiated directly, containing at least one pure virtual function.
class Employee {
private:
String name;
double taxRate;
public:
Employee(String&, double);
String getName();
virtual double calcSalary() = 0; // pure virtual function
};
📐 Formula: calcSalary() → Returns net salary after tax deduction.
The SalariedEmp class inherits from Employee and calculates salary as salary minus tax.
📌 Example: SalariedEmp("Aamir", 0.05, 15000) → tax = 15000 × 0.05 = 750 → net salary = 15000 – 750 = 14250
double SalariedEmp::calcSalary() {
double tax = salary * taxRate;
return salary – tax;
}
The HourlyEmp class calculates salary based on hours worked and hourly rate.
📌 Example: HourlyEmp("Faakhir", 0.06, 160, 50) → grossPay = 160 × 50 = 8000 → tax = 8000 × 0.06 = 480 → net salary = 7520
double HourlyEmp::calcSalary() {
double grossPay = hours * hourlyRate;
double tax = grossPay * taxRate;
return grossPay – tax;
}
The CommEmp class calculates salary based on sales and commission rate.
📌 Example: CommEmp("Fuaad", 0.04, 150000, 10) → grossPay = 150000 × 0.10 = 15000 → tax = 15000 × 0.04 = 600 → net salary = 14400
double CommEmp::calcSalary() {
double grossPay = sales * commRate;
double tax = grossPay * taxRate;
return grossPay – tax;
}
The generatePayroll function takes an array of Employee pointers and iterates through them, calling getName() and calcSalary() polymorphically:
void generatePayroll(Employee* emp[], int size) {
for (int i = 0; i < size; i++) {
cout << emp[i]->getName() << '\t' << emp[i]->calcSalary() << '\n';
}
}
💡 Why this matters: The same calcSalary() call on a base class pointer invokes different implementations depending on the actual derived object type, demonstrating the power of polymorphism.
Important point: Polymorphism always works with pointers of class objects, not with actual objects.
30.2. Shape Hierarchy Revisited
This section demonstrates why polymorphism fails with object arrays. The Shape class hierarchy is revisited with a non-abstract Shape class:
class Shape {
public:
Shape();
virtual void draw() { cout << "Shape\n"; }
virtual int calcArea() { return 0; }
};
class Line : public Shape {
public:
Line(Point p1, Point p2);
void draw() { cout << "Line\n"; }
};
When an array of Shape objects is passed to drawShapes(), it works correctly:
void drawShapes(Shape _shape[], int size) {
for (int i = 0; i < size; i++) {
_shape[i].draw();
}
}
However, when an array of Line objects is passed to the same function, runtime error occurs:
int main() {
Line _line[10];
_line[0] = Line(p1, p2);
drawShapes(_line, 10); // PROBLEM!
return 0;
}
🔑 Definition — Array Slicing: When a derived class object is assigned to a base class array element, only the base class portion is copied, losing derived class data and causing incorrect memory calculations.
The compiler calculates the address of the next element using sizeof(Shape), but the actual array contains Line objects of a different size. This leads to incorrect address calculation and abnormal program termination.
📌 Example: If Shape object size = 10 bytes and Line object size = 15 bytes, and Line array starts at address 0000, the compiler expects next object at address 0010, but the actual next Line object is at address 0015, causing a runtime error.
The correct approach uses a pointer array:
void drawShapes(Shape* _shape[], int size) {
for (int i = 0; i < size; i++) {
_shape[i]->draw(); // Correct polymorphic behavior
}
}
💡 Why this matters: Since all pointers are 4 bytes on 32-bit systems, the array element size is constant regardless of what the pointers point to, ensuring correct address calculation.
⭐ Key Takeaways
The lecture demonstrates that polymorphism effectively solves the payroll problem by allowing a single function to handle different employee types through virtual functions. However, the critical warning is that arrays should never be used polymorphically because element address calculation depends on array type size, and derived class objects have different sizes than base class objects. Always use arrays of base class pointers (not objects) to achieve correct polymorphic behavior. The pointer array ensures uniform element size (4 bytes per pointer) regardless of the actual object type, while object arrays cause memory errors due to incorrect size calculations.
🧠 Quick Revision Questions
- Why must the Employee class have
calcSalary()as a pure virtual function? - What would happen if we passed an array of
HourlyEmpobjects instead of an array ofEmployee*pointers togeneratePayroll()? - In the Shape hierarchy example, why does passing
Line[10]todrawShapes(Shape[], int)cause a runtime error? - What is the formula for calculating the address of the i-th element in an array, and why does this cause problems with polymorphic object arrays?
- How many bytes does each pointer occupy in a pointer array, and why does this ensure correct polymorphic behavior?
📘 Lecture 31 — Multiple Inheritance
📖 Overview: This lecture focuses on multiple inheritance in C++, where a class inherits from more than one base class. It covers the syntax, behavior, and common problems like ambiguity from duplicate function names, and introduces virtual inheritance as a solution to the diamond problem.
🗂️ Topics Covered
The lecture begins with the syntax of multiple inheritance and how derived class objects can use functions from all base classes. It then discusses pointer substitution with multiple base classes and the restrictions involved. The primary problems covered are ambiguous function calls when two base classes share the same function signature, and the "diamond" problem from multi-level multiple inheritance leading to duplicate base class subobjects. Finally, virtual inheritance is introduced as the solution to ensure a single shared base class instance.
📝 Lecture Summary
31.1. Multiple Inheritance
A class in C++ can inherit from more than one base class. The syntax lists all base classes separated by commas, each with its own access specifier (e.g., public, private, protected). The derived class inherits all the data members and member functions from each base class. An object of the derived class can perform tasks that objects of any of the base classes can perform. When using public multiple inheritance, a pointer to any of the base classes can point to a derived class object, but a pointer of one base class cannot be used to call a function belonging to a different base class.
🔑 Definition — Multiple Inheritance: A feature in C++ where a class can inherit from more than one base class simultaneously.
class Phone: public Transmitter, public Receiver
// Phone inherits from both Transmitter and Receiver
{ ... };
class Mermaid: private Woman, private Fish
// Private inheritance means derived classes of Mermaid cannot access Woman or Fish members
{ ... };
📌 Example: A Phone object can call Transmit() (from Transmitter) and Receive() (from Receiver).
int main(){
Phone obj;
obj.Transmit();
obj.Receive();
return 0;
}
🔑 Definition — Pointer Substitution with Multiple Inheritance: In public multiple inheritance, a derived class object can be pointed to by pointers of its base class types, but each base pointer can only access members of that specific base class.
int main(){
Phone obj;
Transmitter * tPtr = &obj; // Valid
Receiver * rPtr = &obj; // Valid
// tPtr->Receive(); // Error: tPtr is of type Transmitter*
return 0;
}
31.2. Problems in Multiple Inheritance
If two or more base classes have a member function with the same signature (name and parameters), the derived class will inherit multiple copies of that function. Calling this function on an object of the derived class results in a compile-time ambiguity error because the compiler does not know which base class's version to invoke. This ambiguity can also arise in multi-level multiple inheritance, known as the diamond problem, where a class inherits from two intermediate classes that both inherit from the same base class. This results in the final derived class having two implicit subobjects of the topmost base class.
🔑 Definition — Ambiguity in Multiple Inheritance: A compile-time error that occurs when the compiler cannot determine which base class's member function to call because two or more base classes have a function with the same signature.
📌 Example: LandVehicle and WaterVehicle both have a GetMaxLoad() function.
class LandVehicle{
public:
int GetMaxLoad();
};
class WaterVehicle{
public:
int GetMaxLoad();
};
class AmphibiousVehicle: public LandVehicle, public WaterVehicle { };
int main(){
AmphibiousVehicle obj;
// obj.GetMaxLoad(); // Error: ambiguous call
return 0;
}
📌 Example (Solution): The programmer must explicitly specify the class name using the scope resolution operator.
int main(){
AmphibiousVehicle obj;
obj.LandVehicle::GetMaxLoad();
obj.WaterVehicle::GetMaxLoad();
return 0;
}
🔑 Definition — The Diamond Problem: A specific ambiguity issue in multiple inheritance where a class inherits from two classes that both inherit from a single common base class, leading to two copies of the common base class's members in the final derived class.
📌 Example: AmphibiousVehicle inherits from LandVehicle and WaterVehicle, which both inherit from Vehicle.
class Vehicle{
public:
int GetMaxLoad();
};
class LandVehicle : public Vehicle{ };
class WaterVehicle : public Vehicle{ };
class AmphibiousVehicle: public LandVehicle, public WaterVehicle { };
int main(){
AmphibiousVehicle obj;
// obj.GetMaxLoad(); // Error: ambiguous
// obj.Vehicle::GetMaxLoad(); // Error: Vehicle is still ambiguous because there are two copies of it
obj.LandVehicle::GetMaxLoad(); // Valid: explicit path to one Vehicle copy
return 0;
}
💡 Why this matters: The diamond problem leads to memory duplication and ambiguity, making it hard to determine which base subobject to use. Data members must also be used with care.
class Vehicle{
protected:
int weight;
};
class LandVehicle : public Vehicle{ };
class WaterVehicle : public Vehicle{ };
class AmphibiousVehicle: public LandVehicle, public WaterVehicle{
public:
AmphibiousVehicle(){
LandVehicle::weight = 10; // Refers to one copy of weight
WaterVehicle::weight = 10; // Refers to a different copy of weight
}
};
In this case, the AmphibiousVehicle object contains two separate weight members.
31.3. Virtual Inheritance
Virtual inheritance is the solution to the diamond problem. When a base class is declared as virtual in the inheritance list of intermediate classes, the compiler ensures that only one copy of its member data exists in any further derived classes. This resolves the ambiguity of member names and data duplication. Virtual inheritance should only be used when necessary, as it introduces some overhead and can be more complex. In some situations, a programmer might intentionally want two distinct copies of a base class's data members in the final derived class.
🔑 Definition — Virtual Inheritance: A C++ mechanism used in multiple inheritance to ensure that a common base class is shared, resulting in only one copy of its members in any further derived classes, thus solving the diamond problem.
📐 Formula (Syntax):
class IntermediateClass : public virtual BaseClass { ... };
class MostDerivedClass : public IntermediateClass1, public IntermediateClass2 { ... }; → Results in a single BaseClass subobject.
📌 Example: Using virtual inheritance to solve the diamond problem.
class Vehicle{
protected:
int weight;
};
class LandVehicle : public virtual Vehicle{ };
class WaterVehicle : public virtual Vehicle{ };
class AmphibiousVehicle: public LandVehicle, public WaterVehicle {
public:
AmphibiousVehicle(){
weight = 10; // No ambiguity: there is only one copy of weight from Vehicle
}
};
💡 Why this matters: Virtual inheritance prevents the memory duplication and naming ambiguity inherent in the diamond problem. It creates a single, shared base class instance for the entire inheritance hierarchy.
⭐ Key Takeaways
The most critical concepts from this lecture are that multiple inheritance allows a class to inherit from several base classes, but introduces the significant problem of name ambiguity when functions or data with the same name are inherited. You must use the scope resolution operator (ClassName::) to resolve these ambiguous calls. The diamond problem is a special case of ambiguity from multi-level multiple inheritance, creating two base class subobjects. The definitive solution to this is virtual inheritance, which guarantees a single shared copy of the base class, eliminating both memory redundancy and function ambiguity. Remember to use virtual inheritance only when a single common base is required; otherwise, multiple distinct copies can be intentional.
🧠 Quick Revision Questions
- How do you declare a class
Cthat publicly inherits from classAand privately inherits from classB? - What compile-time error occurs when a derived class inherits two functions with the same name from different base classes, and how do you fix it?
- Explain what the "diamond problem" is in C++ and why it causes ambiguity.
- Write the
classdeclaration forWaterVehiclethat virtually inherits fromVehicle. - In the diamond problem scenario with virtual inheritance, how many copies of the topmost base class's data members exist in the most derived class's object?
📘 Lecture 32 — Generic Programming
📖 Overview: This lecture introduces the concept of generic programming in C++, which allows writing single functions or classes that work with multiple data types. It explains the motivation behind generic programming, introduces templates as the mechanism for implementation, and covers function templates with practical examples including user-defined specializations.
🗂️ Topics Covered
The lecture begins by demonstrating the problem of code duplication when creating functions and classes for different data types, then introduces generic programming as a solution. It covers the advantages of this approach, explains function templates and class templates, details template declaration syntax, discusses explicit type parameterization, and concludes with user-defined specializations for handling type-specific cases.
📝 Lecture Summary
32.1. Generic Programming
Generic programming refers to programs containing generic abstractions (general code that is same in logic for all data types like the printArray function), then we instantiate that generic program abstraction (function, class) for a particular data type. Such abstractions can work with many different types of data.
💡 Why this matters: Instead of writing identical functions for int, char, double, float, short, and long arrays, generic programming lets us write one version that works for all.
Advantages of generic programming:
- Reusability: Code can work for all data types
- Writability: Code takes lesser time to write
- Maintainability: Code is easy to maintain as changes are needed to be made in a single function or class instead of many functions or classes
🔑 Definition — Generic Programming: A programming technique where programs contain generic abstractions (general code) that can be instantiated for particular data types, allowing the same code to work with many different types of data.
📌 Example — The Problem: Three nearly identical printArray functions for int, char, and double arrays:
void printArray(int* array, int size) { /* loop printing */ }
void printArray(char* array, int size) { /* loop printing */ }
void printArray(double* array, int size) { /* loop printing */ }
All do the same functionality but on different data types — this is code duplication.
32.2. Templates
In C++, generic programming is done using templates. The compiler generates different type-specific copies from a single template. This concept is similar to making a prototype in the form of a class for all objects of the same kind.
Two kinds of templates:
- Function Templates: For writing general functions like
printArray - Class Templates: For writing general classes like the
Arrayclass
🔑 Definition — Template: A C++ feature that enables generic programming by allowing the creation of functions and classes that can work with any data type. The compiler generates type-specific copies from a single template definition.
32.3. Function Templates
A function template can be parameterized to operate on different types of data types.
Declaration syntax:
template< class T >
void funName( T x );
// OR
template< typename T >
void funName( T x );
// OR
template< class T, class U, ... >
void funName( T x, U y, ... );
Note: T is a placeholder for the data type name. We use T instead of specific data types (like int, float, char) for which we want our function to work as a template. There is no difference between class T and typename T.
📐 Formula — Function Template Instantiation: template<typename T> void func(T param) → When called with an argument of type X, the compiler instantiates func(X param) automatically.
📌 Example — Function Template for printArray:
template< typename T >
void printArray( T* array, int size )
{
for ( int i = 0; i < size; i++ )
cout << array[ i ] << ", ";
}
Usage:
int main() {
int iArray[5] = { 1, 2, 3, 4, 5 };
printArray( iArray, 5 ); // Instantiated for int[]
char cArray[3] = { 'a', 'b', 'c' };
printArray( cArray, 3 ); // Instantiated for char[]
return 0;
}
Explicit Type Parameterization: When a function template does not have any parameter that uses the template type, we must explicitly specify the data type:
template <typename T>
T getInput() {
T x;
cin >> x;
return x;
}
int main() {
int x = getInput< int >(); // Explicitly instantiated for int
double y = getInput< double >(); // Explicitly instantiated for double
return 0;
}
Without explicit type parameterization, getInput() would cause a compilation error because the compiler cannot deduce the type from the function call alone.
🔑 Definition — Explicit Type Parameterization: When instantiating a function template that doesn't have parameters of the template type, the data type must be explicitly specified in angle brackets after the function name, e.g., getInput< int >().
User-defined Specializations: A template compiler-generated code may not handle all types successfully. In that case, we can give explicit specializations for a particular data type(s).
📌 Example — The Problem with Specialization:
template< typename T >
bool isEqual( T x, T y ) {
return ( x == y );
}
This works correctly for int, double, and char:
isEqual(6,6)→ returnstrueisEqual(6,7)→ returnsfalseisEqual(6.6,6.6)→ returnstrueisEqual('A','A')→ returnstrue
But isEqual("abc","xyz") fails because it compares char* pointers (compares memory addresses, not string contents). Since char* comparison only checks if the first element matches, isEqual("abc","acc") incorrectly returns true because both point to strings starting with 'a'.
📌 Example — User-defined Specialization Solution:
template< typename T >
bool isEqual( T x, T y ) {
return ( x == y );
}
// Specialization for const char*
template< >
bool isEqual< const char* >( const char* x, const char* y ) {
return ( strcmp( x, y ) == 0 );
}
int main() {
cout << isEqual( 5, 6 ); // OK - uses generic template
cout << isEqual( 7.5, 7.5 ); // OK - uses generic template
cout << isEqual( "abc", "aba" ); // OK - uses specialization, returns false
return 0;
}
🔑 Definition — User-defined Specialization: A specialized version of a function template for a specific data type that overrides the generic template behavior, used when the generic implementation would produce incorrect results for that particular type.
⭐ Key Takeaways
Generic programming using templates is a powerful C++ feature that eliminates code duplication by allowing a single function or class to work with any data type. Function templates use either typename or class keywords to declare type parameters, and the compiler automatically generates appropriate type-specific code through template instantiation. When a function template lacks parameters of the template type (like getInput()), explicit type parameterization using angle brackets is required. User-defined specializations provide a mechanism to override the generic template behavior for specific data types when the default implementation would produce incorrect results, such as comparing C-style strings where pointer comparison fails. The three major benefits of this approach are reusability, improved writability, and easier maintainability since changes only need to be made in one place.
🧠 Quick Revision Questions
- What are the two types of templates in C++ and what is each used for?
- Explain the difference between
template<typename T>andtemplate<class T>— is there any functional difference? - When would you need to use explicit type parameterization (e.g.,
getInput< int >()) instead of letting the compiler deduce the type? - Why does
isEqual("abc","xyz")fail with the generic template implementation, and how do user-defined specializations fix this? - List and explain the three major advantages of generic programming over writing separate functions for each data type.
📘 Lecture 33 — Templates: Advanced Concepts and Policy-Based Design
📖 Overview: This lecture explores advanced template concepts in C++, including multiple type arguments for conversion functions, using templates with user-defined types, and the distinction between function overloading and templates. The core focus is on using template arguments as "policy" to change behavior—demonstrated through a sophisticated string comparison function that can be configured for case-sensitive or case-insensitive comparisons at compile time.
🗂️ Topics Covered
Multiple type arguments for template functions enabling type conversion between different data types. Using user-defined types (like String classes) as template arguments and the importance of defining required operators (like ==) for template functions to work correctly. The distinction between function overloading (different implementations for different types) and function templates (identical operations across types). Template arguments as policy to change behavior at compile time, illustrated through three solutions for implementing case-sensitive and case-insensitive string comparison.
📝 Lecture Summary
Recap
Templates are generic abstractions in C++ that come in two kinds: Function Templates and Class Templates. A general template can be specialized to handle a particular type, such as the char [] type.
33.1. Multiple Type Arguments
When writing code to convert different types into one another (like char to int or float to int), the concept of templates can be used with multiple type arguments. This avoids writing many separate functions for each type conversion.
template< typename T, typename U >
T my_cast( U u ) {
return (T)u; // U type will be converted to T type and will be returned
}
int main() {
double d = 10.5674;
int j = my_cast( d ); // Error: cannot deduce T
int i = my_cast< int >( d ); // OK: explicitly specify T as int
return 0;
}
🔑 Definition — Multiple Type Arguments: Template parameters that allow a function template to accept and work with two or more different data types simultaneously.
📐 Formula: template< typename T, typename U > T functionName( U u ) → Converts type U to type T and returns the result.
📌 Example: When calling my_cast< int >( d ) where d is a double (10.5674), the function returns (int)10.5674 which equals 10, performing an explicit type conversion from double to int.
💡 Why this matters: The compiler cannot deduce type T when it's only used as the return type and not in the parameter list—you must explicitly specify it.
33.2. User-Defined Types
Besides primitive types, user-defined types can also be passed as type arguments to templates. The compiler performs static type checking to diagnose type errors at compile time.
Consider a String class without an overloaded == operator:
class String {
char* pStr;
// Operator "==" not defined
};
template< typename T >
bool isEqual( T x, T y ) {
return ( x == y );
}
int main() {
String s1 = "xyz", s2 = "xyz";
isEqual( s1, s2 ); // Error! No == operator defined for String
return 0;
}
To use the String class with the isEqual template, we must define the overloaded == operator as a friend function:
class String {
char* pStr;
friend bool operator ==( const String&, const String& );
};
bool operator ==( const String& x, const String& y ) {
return strcmp(x.pStr, y.pStr) == 0;
}
template< typename T >
bool isEqual( T x, T y ) {
return ( x == y );
}
int main() {
String s1 = "xyz", s2 = "xyz";
isEqual( s1, s2 ); // OK now
return 0;
}
🔑 Definition — Static Type Checking: Compile-time verification that the types used with a template support all the operations performed inside the template function.
33.3. Overloading vs. Templates
Function templates are used when we want to have exactly identical operations on different data types. With templates, we cannot change the implementation from one data type to another without using specialization.
Function overloading is used when we want to have similar but slightly different operations on different data types, allowing different implementations for each type.
📌 Example:
- Overloading: The
+operator is overloaded for different types of operands (String+String, char*+String, etc.) with different implementations in each case. - Templates: A single function template can calculate the sum of an array of many numeric types with identical logic.
// Function Overloading (different implementations)
String operator +( const String& x, const String& y ) { ... }
String operator +( const char * str1, const String& y ) { ... }
// Function Template (identical operations)
template< class T >
T sum( T* array, int size ) {
T sum = 0;
for (int i = 0; i < size; i++)
sum = sum + array[i];
return sum;
}
33.4. Template Arguments as Policy
We can change the behavior of a template using a template parameter. We can pass a template argument to enforce some rule or policy. The problem: "Write a function that compares two given character strings" that can perform both case-sensitive and case-insensitive comparisons.
33.5. First Solution: Two Separate Functions
Write two separate functions and call the appropriate one as needed.
Case-sensitive comparison (returns 0 if strings are identical and same length):
int caseSencompare( char* str1, char* str2 ) {
for (int i = 0; i < strlen( str1 ) && i < strlen( str2 ); ++i)
if ( str1[i] != str2[i] )
return str1[i] - str2[i];
return strlen(str1) - strlen(str2);
}
Case-insensitive comparison (returns 0 if strings have same alphabets ignoring case):
int nonCaseSencompare( char* str1, char* str2 ) {
for (int i = 0; i < strlen( str1 ) && i < strlen( str2 ); i++)
if ( toupper( str1[i] ) != toupper( str2[i] ) )
return str1[i] - str2[i];
return strlen(str1) - strlen(str2);
}
33.6. Second Solution: Using a Bool Parameter
Write a single compare function and pass a bool parameter to indicate the type of comparison.
int compare( char* str1, char* str2, bool caseSen ) {
for (int i = 0; i < strlen( str1 ) && i < strlen( str2 ); i++)
if ( (caseSen && str1[i] != str2[i]) ||
(!caseSen && toupper(str1[i]) != toupper(str2[i])) )
return str1[i] - str2[i];
return strlen(str1) - strlen(str2);
}
🔑 Definition — Policy Flag: A boolean parameter used at runtime to determine which behavior to execute within a single function implementation.
When caseSen is true (1), it activates the left sub-expression (caseSen && str1[i] != str2[i]). When caseSen is false, it activates the right sub-expression (!caseSen && toupper(str1[i]) != toupper(str2[i])).
33.7. Third Solution: Template Policy Classes (Most Elegant)
Write two classes—one for case-sensitive and one for case-insensitive comparison—and pass one of these classes as a template argument. The template function calls the isEqual function of the passed class.
class CaseSenCmp {
public:
static int isEqual( char x, char y ) {
return x == y;
}
};
class NonCaseSenCmp {
public:
static int isEqual( char x, char y ) {
return toupper(x) == toupper(y);
}
};
template< typename C >
int compare( char* str1, char* str2 ) {
for (int i = 0; i < strlen( str1 ) && i < strlen( str2 ); i++)
if ( !C::isEqual (str1[i], str2[i]) )
return str1[i] - str2[i];
return strlen(str1) - strlen(str2);
}
int main() {
int i, j;
char *x = "hello", *y = "HELLO";
i = compare< CaseSenCmp >(x, y);
j = compare< NonCaseSenCmp >(x, y);
cout << "Case Sensitive: " << i; // Output: 32 (Not Equal)
cout << "\nNon-Case Sensitive: " << j; // Output: 0 (Equal)
return 0;
}
🔑 Definition — Policy Class: A class that encapsulates a specific behavior (like comparison logic) and is passed as a template argument to configure the template's behavior at compile time.
33.8. Default Policy
We can set a default class type as the default comparison type, similar to setting default parameters in constructors.
template< typename C = CaseSenCmp >
int compare( char* str1, char* str2 ) {
for (int i = 0; i < strlen( str1 ) && i < strlen( str2 ); i++)
if ( !C::isEqual (str1[i], str2[i]) )
return str1[i] - str2[i];
return strlen(str1) - strlen(str2);
}
int main() {
int i, j;
char *x = "hello", *y = "HELLO";
i = compare(x, y); // Uses default: CaseSenCmp
j = compare< NonCaseSenCmp >(x, y); // Explicit: NonCaseSenCmp
cout << "Case Sensitive: " << i;
cout << "\nNon-Case Sensitive: " << j;
return 0;
}
🔑 Definition — Default Template Argument: A type parameter with a default value (= CaseSenCmp) that allows the template to be used without explicitly specifying the policy class.
⭐ Key Takeaways
You must understand that templates can accept multiple type arguments for type conversion functions, but the compiler cannot deduce return-type-only template parameters—they must be explicitly specified. When using templates with user-defined types, ensure all required operators (like ==) are properly defined, or the compiler will produce type errors during static checking. The critical distinction is that function templates provide identical operations across types while function overloading allows different implementations. Template arguments can serve as compile-time "policies" to change behavior without runtime overhead, which is the most elegant solution compared to using separate functions or runtime boolean flags. Default template arguments allow you to provide sensible default behavior while still enabling customization.
🧠 Quick Revision Questions
- When using multiple type arguments in a template function, why must you explicitly specify the return type parameter in the function call?
- What must be defined for a user-defined class to work correctly as a template argument for a function template that uses
==? - What is the key difference between function overloading and function templates regarding implementation flexibility?
- In the third solution for string comparison, why are the
isEqualfunctions declared asstaticinside the policy classes? - How does setting a default template argument (like
= CaseSenCmp) affect how thecomparefunction is called?
📘 Lecture 34 — Generic Algorithms - A Case Study
📖 Overview: This lecture demonstrates how to transform a type-specific function into a fully generic algorithm that works independently of both data types and data structures. It walks through the step-by-step evolution of a
findfunction from array-specific code to a genuinely generic algorithm, and then extends the same principles to create a class template for a Vector container.
🗂️ Topics Covered
The lecture covers the concept of generic algorithms that are independent of both data types and data structures, starting with the transformation of the printArray template function into a more flexible form. It then works through a case study of converting an integer-specific find function into a fully generic template function by removing size parameters, using "beyond" pointers, and finally eliminating pointer dependencies. The lecture concludes with class templates and a complete implementation of a generic Vector class.
📝 Lecture Summary
34.1. Generic Algorithms
Generic programming aims to create functions that work for all types of containers, not just arrays. The lecture uses a find function as a case study to demonstrate step-by-step evolution toward true genericity. Starting with an integer-specific version that takes an array and its size, the function is progressively transformed into a fully generic algorithm.
The first step is converting the function into a template function so it works for all data types:
template< typename T >
T* find( T* array, int _size, const T& x ) {
T* p = array;
for (int i = 0; i < _size; i++) {
if ( *p == x )
return p;
p++;
}
return 0;
}
The next step replaces the size parameter with a "beyond" pointer — a pointer to one position past the last element. This simplifies the code and makes it more generic:
template< typename T >
T* find( T* array, T* beyond, const T& x ) {
T* p = array;
while ( p != beyond ) {
if ( *p == x )
return p;
p++;
}
return beyond;
}
💡 Why this matters: Using a "beyond" pointer instead of a size parameter allows the function to work with any contiguous memory range, including sub-arrays or slices of larger arrays.
The function is further simplified by combining the termination check and comparison in the while condition, and using a single return statement:
template< typename T >
T* find( T* array, T* beyond, const T& x ) {
T* p = array;
while ( p != beyond && *p != x )
p++;
return p;
}
The final and most important step removes all pointer notation, making the function truly independent of data structure. The function now works with any container that supports two operations: increment operator (++) and dereference operator (*):
template< typename P, typename T >
P find( P start, P beyond, const T& x ) {
while ( start != beyond && *start != x )
start++;
return start;
}
This generic version can be called with arrays, lists, or any container that supports ++ and *:
int main() {
int iArray[5];
iArray[0] = 15;
iArray[1] = 7;
iArray[2] = 987;
// ...
int* found;
found = find(iArray, iArray + 5, 7);
return 0;
}
🔑 Definition — Generic Algorithm: A function or algorithm that works for any data type and any container (data structure), as long as the container supports the required operations (like ++ and *).
34.2. Class Templates
A class template provides functionality to operate on different types of data, facilitating reuse of classes. Just as function templates allow a single function to work with multiple types, class templates allow a single class definition to work with multiple types.
A class template can be defined in two equivalent ways:
template< class T > class Xyz { ... };template< typename T > class Xyz { ... };
The keyword class and typename are interchangeable in this context. The lecture now builds a template Vector class that can store any type of data, including objects that are themselves collections.
🔑 Definition — Class Template: A blueprint for creating classes that operate on different data types. The actual class is generated when the template is instantiated with a specific type.
34.3. Example – Class Template
The lecture presents a complete Vector class template that can store data elements of different types. Without templates, a separate Vector class would be needed for each data type.
Class Definition:
template< class T >
class Vector {
private:
int size;
T* ptr;
public:
Vector<T>( int = 10 );
Vector<T>( const Vector< T >& );
~Vector<T>();
int getSize() const;
const Vector< T >& operator =( const Vector< T >& );
T& operator []( int );
};
Implementation:
// Constructor
template< class T >
Vector<T>::Vector<T>( int s ) {
size = s;
if ( size != 0 )
ptr = new T[size];
else
ptr = 0;
}
// Copy Constructor
template< class T >
Vector<T>:: Vector<T>( const Vector<T>& copy ) {
size = copy.getSize();
if (size != 0) {
ptr = new T[size];
for (int i = 0; i < size; i++)
ptr[i] = copy.ptr[i];
}
else ptr = 0;
}
// Destructor
template< class T >
Vector<T>::~Vector<T>() {
delete [] ptr;
}
// getSize function
template< class T >
int Vector<T>::getSize() const {
return size;
}
// Assignment Operator
template< class T >
const Vector<T>& Vector<T>::operator =( const Vector<T>& right) {
if ( this != &right ) {
delete [] ptr;
size = right.size;
if ( size != 0 ) {
ptr = new T[size];
for(int i = 0; i < size;i++)
ptr[i] = right.ptr[i];
}
else
ptr = 0;
}
return *this;
}
// Subscript Operator
template< class T >
T& Vector< T >::operator []( int index ) {
if ( index < 0 || index >= size ) {
cout << "Error: index out of range\n";
exit( 1 );
}
return ptr[index];
}
Instantiation examples:
Vector< int > intVector; // Creates a Vector of integers
Vector< char > charVector; // Creates a Vector of characters
The Vector class is a parameterized class — it must always be instantiated for a particular type. You cannot create an object of type Vector alone; it must be Vector<int>, Vector<float>, etc.
🔑 Definition — Parameterized Class: A class template that must be instantiated with a specific data type before objects can be created.
📌 Example:
Vector< int > intVector; // Valid - creates Vector of ints
Vector< char > charVector; // Valid - creates Vector of chars
// Vector unknownVector; // Invalid - must specify type
⭐ Key Takeaways
The most critical concept is that generic algorithms achieve true independence from both data types and data structures by using template parameters and supporting only essential operations like ++ and *. The step-by-step evolution of the find function — from integer-specific to template, then replacing size with a beyond pointer, and finally removing all pointer notation — demonstrates this principle clearly. The Vector class template shows how the same generic approach applies to classes, allowing a single class to work with any data type while handling dynamic memory management through constructors, destructors, and copy assignment.
🧠 Quick Revision Questions
- What are the two essential operations a container must support for a generic algorithm to work with it?
- Why is using a "beyond" pointer better than passing the size of an array to a generic function?
- In the final generic
findfunction, what does the template parameterPrepresent? - What is the difference between a function template and a class template?
- Why must the Vector class always be instantiated with a specific type (e.g.,
Vector<int>) rather than justVector?
📘 Lecture 35 — Member Templates and Class Template Specialization
📖 Overview: This lecture covers advanced template concepts in C++ — member templates that allow template classes to handle multiple data types, and class template specialization for handling specific types differently. Understanding these concepts is crucial for writing flexible, type-safe generic code that handles edge cases properly.
🗂️ Topics Covered
Member function templates within template classes for cross-type operations like copy constructors, the need for explicit template parameters beyond the class template parameter, code generation optimization to avoid code bloat, and class template specialization for char arrays to handle deep copying and memory management correctly.
📝 Lecture Summary
35.1. Member Templates
Member functions of a template class implicitly become function templates that work for instantiations (int, char, float, double, etc.) of that class. However, situations arise where we need explicit template functions taking more template parameters besides the implicit one (the parameter given to the class when creating its object).
A class or class template can have member functions that are themselves templates. When declaring a class in C++, there is no need to mention the template parameter for class member functions as the compiler implicitly understands it. However, if using another template parameter (like in a copy constructor), we must give its name explicitly.
🔑 Definition — Member Template: A template member function within a class template that takes additional template parameters beyond the class's own template parameter.
The problem arises when assigning Complex class instances of different types:
template<typename T> class Complex {
T real, imag;
public:
Complex(T r, T im) : real(r), imag(im) {}
Complex(const Complex<T>& c) : real(c.real), imag(c.imag) {}
};
int main() {
Complex<float> fc(0, 0);
Complex<double> dc = fc; // Error!
return 0;
}
This fails because the copy constructor only accepts same-type arguments. When creating a Complex<double> object, the compiler generates:
class Complex<double> {
double real, imag;
public:
Complex(double r, double im) : real(r), imag(im) {}
Complex(const Complex<double>& c) : real(c.real), imag(c.imag) {}
};
The solution is to make the copy constructor an explicit member template:
template<typename T> class Complex {
T real, imag;
public:
Complex(T r, T im) : real(r), imag(im) {}
template <typename U>
Complex(const Complex<U>& c) : real(c.real), imag(c.imag) {}
};
Now the assignment works:
int main() {
Complex<float> fc(0, 0);
Complex<double> dc = fc; // OK
return 0;
}
Here, the copy constructor is instantiated with implicit template type T = double and explicit template type U = float.
Important notes:
- Only the required instantiation of the copy constructor is generated
- Good compilers only generate required template function instances to avoid code bloat (unnecessary code generation)
For Complex<double> dc = fc;, only the <double> instantiation gets the member template copy constructor, while the <float> instantiation doesn't generate one since it's not needed.
💡 Why this matters: Member templates allow type conversion between template instantiations without writing separate conversion functions for each type pair, while keeping code generation minimal.
35.2. Class Template Specialization
Like function templates, a class template may not handle all types successfully. For *char arrays (char)**, the behavior of a template class Vector may not be as desired.
For a Vector<int> (integers), shallow copy works fine. But for Vector<char*>:
int main() {
Vector<char*> sv1(2);
sv1[0] = "Aamir"; // stores pointer to const string
sv1[1] = "Nasir";
Vector<char*> sv2(sv1); // shallow copy issue
Vector<char*> sv3(2);
sv3 = sv1; // shallow copy issue
return 0;
}
We write an explicit specialization for Vector<char*>:
template<>
class Vector<char*> {
private:
int size;
char** ptr;
public:
Vector(int = 10);
Vector(const Vector<char*>&);
virtual ~Vector();
int getSize() const;
const Vector<char*>& operator=(const Vector<char*>&);
const char*& operator[](int);
void insert(char*, int);
};
Key implementation differences from the general template:
Constructor: Allocates array of char pointers, initializes each to null
template<>
Vector<char*>::Vector(int s) {
size = s;
if (size != 0) {
ptr = new char*[size];
for (int i = 0; i < size; i++) ptr[i] = 0;
} else ptr = 0;
}
Copy constructor: Performs deep copy — allocates new memory and uses strcpy
template<>
Vector<char*>::Vector(const Vector<char*>& copy) {
size = copy.getSize();
if (size == 0) { ptr = 0; return; }
ptr = new char*[size];
for (int i = 0; i < size; i++)
if (copy.ptr[i] != 0) {
ptr[i] = new char[strlen(copy.ptr[i]) + 1];
strcpy(ptr[i], copy.ptr[i]);
} else ptr[i] = 0;
}
Destructor: Deletes each string and the array
template<>
Vector<char*>::~Vector() {
for (int i = 0; i < size; i++) delete[] ptr[i];
delete[] ptr;
}
Assignment operator: Self-assignment check, clean up old memory, deep copy all strings
template<>
const Vector<char*>& Vector<char*>::operator=(const Vector<char*>& right) {
if (this == &right) return *this;
for (int i = 0; i < size; i++) delete[] ptr[i];
delete[] ptr;
size = right.size;
if (size == 0) { ptr = 0; return *this; }
ptr = new char*[size];
for (int i = 0; i < size; i++)
if (right.ptr[i] != 0) {
ptr[i] = new char[strlen(right.ptr[i]) + 1];
strcpy(ptr[i], right.ptr[i]);
} else ptr[i] = 0;
}
Subscript operator: Returns reference to char pointer
template<>
const char*& Vector<char*>::operator[](int index) {
if (index < 0 || index >= size) {
cout << "Error: index out of range\n";
exit(1);
}
return ptr[index];
}
Insert function: Properly handles dynamic memory
template<>
void Vector<char*>::insert(char* str, int i) {
delete[] ptr[i];
if (str != 0) {
ptr[i] = new char[strlen(str) + 1];
strcpy(ptr[i], str);
} else ptr[i] = 0;
}
Usage in main:
int main() {
Vector<char*> sv1(2);
sv1.insert("Aamir", 0); // must use insert, not assignment
sv1.insert("Nasir", 1);
Vector<char*> sv2(sv1); // deep copy works
Vector<char*> sv3(2);
sv3 = sv1; // deep copy works
return 0;
}
Note: The subscript operator returns a const reference, so direct assignment like sv1[0] = "Aamir" fails. The insert function must be used instead.
💡 Why this matters: Class template specialization allows custom behavior for specific types that need special handling (like dynamic memory management for char arrays), while keeping the same interface as the general template.
⭐ Key Takeaways
Class template specialization is essential when the generic template behavior is incorrect for specific types, particularly when dealing with pointers and dynamic memory. Member templates enable cross-type operations within template classes by adding explicit template parameters. Good compilers optimize code generation to avoid bloat by only instantiating what's needed. For char arrays in class templates, always provide explicit specialization with deep copy semantics to prevent dangling pointers and memory leaks. The insert function approach for char arrays ensures proper memory management compared to direct assignment via subscript operators.
🧠 Quick Revision Questions
- Why does
Complex<double> dc = Complex<float> fcfail with a regular copy constructor, and how does a member template fix it? - What is code bloat and how do good compilers avoid it when instantiating member templates?
- What issues arise when using a generic Vector class template with char* data?
- In the char* specialization, why does the subscript operator return a const reference, and what must be used instead?
- How does the copy constructor in the char* specialization differ from the general template version?
📘 Lecture 36 — Member Templates & Specializations
📖 Overview: This lecture explores advanced template concepts in C++, including member templates for ordinary classes, partial and complete specialization of class and function templates, and non-type parameters. Understanding these concepts is crucial for creating flexible, type-safe generic code that can handle different data types efficiently.
🗂️ Topics Covered
The lecture covers member templates for ordinary classes, partial specialization vs complete specialization of class templates, function template specializations, using different specializations in practice, non-type parameters in templates, template class Array with non-type parameters, default non-type parameters, and default type parameters.
📝 Lecture Summary
Recap
We saw in previous lecture how we can implement our programming problems easily using Generic Algorithms. Then we saw how we can add explicit template functions to our Class templates to add our desired functionality. A class template may not handle all the types successfully; explicit specializations are required to deal with such types. We can implement the concept of template specialization for such classes as well as we did for function templates.
36.1. Member Templates Revisited
We can add member templates for ordinary classes as well. For example, the following code adds any instance of Complex class to ComplexSet class. The Complex class can be instantiated for int, float, or double, and ComplexSet class will be a collection of Complex class instantiations.
class ComplexSet {
template<class T>
insert(Complex<T> c) // any instance of complex class
{
// Add instance Complex class to Complex set class
}
};
int main() {
Complex<int> ic(10, 5);
Complex<float> fc(10.5, 5.7);
Complex<double> dc(9.567898, 5);
ComplexSet cs;
cs.insert(ic);
cs.insert(fc);
cs.insert(dc);
return 0;
}
This shows that even an ordinary (non-template) class can have template member functions, allowing it to accept arguments of various template instantiations.
💡 Why this matters: Member templates in ordinary classes enable generic member functions without making the entire class a template.
36.2. Partial Specialization
We can perform partial specialization instead of complete specialization. A partial specialization of a template exists between general specialization and complete specialization. For example, we can specialize a class to behave in a certain manner in case of pointers or in case of a parameter of a certain type.
The difference between complete and partial specialization:
template<class T, class U, class V> // general template
template<class T, class U, int> // partial specialization
template<class T, float, int> // partial specialization
template<int, float, int> // complete specialization
In partial specialization, the number of template parameters remains the same; however, their nature varies (they become more specific).
Example – Partial Specialization:
template<class T>
class Vector { };
template<class T>
class Vector<T*> { }; // Here T can take any type pointer
template<class T, class U, class V>
class A {};
template<class T, class V>
class A<T, T*, V> {}; // parameters in header are two but in class using same three parameters
template<class T, class U, int I>
class A<T, U, I> {}; // changed third parameter to non-type parameter
template<class T>
class A<int, T*, 5> {}; // first parameter non-type, second T*, third constant 5
Example – Complete Specialization:
template<class T>
class Vector { };
template<>
class Vector<char*> { };
template<class T, class U, class V>
class A {};
template<>
class A<int, char*, double> {};
🔑 Definition — Non-type parameters: Non-type parameters are those parameters which are not template parameters (not types like class T, but values like int I).
36.3. Function Templates
Similar to class templates, a function template may also have partial specializations.
Example – Partial Specialization for functions:
template<class T, class U, class V>
void func(T, U, V);
template<class T, class V>
void func(T, T*, V);
template<class T, class U, int I>
void func(T, U);
template<class T>
void func(int, T, 7);
36.4. Complete Specialization
We have already used complete specialization in case of function templates.
Example – Complete specialization with isEqual:
template<typename T>
bool isEqual(T x, T y) {
return (x == y);
}
template<typename T>
bool isEqual(T* x, T* y) {
return (*x == *y);
}
template<>
bool isEqual<const char*>(const char* x, const char* y) {
return (strcmp(x, y) == 0);
}
The complete specialization isEqual<const char*> provides custom behavior for C-style strings, using strcmp instead of operator==.
36.5. Using Different Specializations
The code below shows how we can use different types of specializations for the isEqual function:
int main() {
int i, j;
char* a, b;
Shape *s1 = new Line();
Shape *s2 = new Circle();
isEqual(i, j); // Template (general)
isEqual(a, b); // Complete Specialization (const char*)
isEqual(s1, s2); // Partial Specialization (T*)
return 0;
}
36.6. Non-type Parameters
The parameters given in template definition other than those used for mentioning template types are called non-type parameters. For example:
template <class T, class U, int I>
Here int I is a non-type parameter. Template parameters may include non-type parameters, and non-type parameters may have default values:
template <class T, class U, int I = 5>
They are treated as constants and are commonly used for static memory allocation — meaning when we want to pass the length of memory we need in the template at compile time (statically).
36.7. Example – template class Array
Consider the Array class again that we discussed in template classes introduction. In this template array class, we create a C++ array of any built-in data type by passing array size in Array object constructor:
template<class T>
class Array {
private:
T* ptr;
public:
Array(int size);
~Array();
// ...
};
template<class T>
Array<T>::Array() {
if (size > 0)
ptr = new T[size];
else
ptr = NULL;
}
int main() {
Array<char> cArray(10);
Array<int> iArray(15);
Array<double> dArray(20);
return 0;
}
We can do the same by passing the array size as a non-type parameter while creating an Array class object itself:
template<class T, int SIZE>
class Array {
private:
T ptr[SIZE];
public:
Array();
// ...
};
int main() {
Array<char, 10> cArray;
Array<int, 15> iArray;
Array<double, 20> dArray;
return 0;
}
Now the array size is part of the type and known at compile time, allowing stack allocation (T ptr[SIZE] instead of dynamic allocation with new).
📐 Formula: Array<T, SIZE> → A class template where SIZE is a compile-time constant determining array length
36.8. Default Non-type Parameters
We can set default values for non-type parameters, as we do for parameters passed in ordinary functions:
template<class T, int SIZE = 10>
class Array {
private:
T ptr[SIZE];
public:
void doSomething();
// ...
};
int main() {
Array<char> cArray; // here Array of size 10 will be created
return 0;
}
When no size is specified, the default value 10 is used.
36.9. Default Type Parameters
We can also specify default type for type parameters (template parameters like T, U, V). Consider the Vector class again:
template<class T = int> // default type for Vector class is now int
class Vector {
// ...
};
Vector<> v; // same as Vector<int> v
When no type is specified, the default type (int) is used.
⭐ Key Takeaways
Member templates allow ordinary (non-template) classes to have template member functions, enabling them to accept various template instantiations. Partial specialization provides a middle ground between general and complete templates, allowing specific behaviors for certain parameter patterns (like pointers) while keeping other parameters generic. Non-type parameters enable compile-time constants to be passed as template arguments, which is essential for static memory allocation and compile-time optimizations. Default values for both type and non-type parameters make templates more convenient to use by providing sensible defaults. Understanding the hierarchy of general template, partial specialization, and complete specialization is critical for designing flexible yet type-safe generic code in C++.
🧠 Quick Revision Questions
- What is the difference between partial specialization and complete specialization of a class template?
- How would you create an Array class that uses a non-type parameter for size, and what advantage does this provide over passing size to the constructor?
- What happens when you write
Vector<> v;if the Vector template has a default type parameter ofint? - Write a template function
isEqualwith a general version, a partial specialization for pointers, and a complete specialization forconst char*. - Can an ordinary (non-template) class have template member functions? Provide an example involving a
ComplexSetclass and aComplex<T>class.
📘 Lecture 37 — Resolution Order
📖 Overview: This lecture explains how the compiler resolves which template specialization to use when multiple specializations exist, covering both class templates and function templates. It also details how inheritance works with templates, including rules and valid/invalid derivation patterns across general, partial, and complete specializations.
🗂️ Topics Covered
The lecture covers resolution order for class template specializations, function template overloading and its resolution order, templates and inheritance rules, derivations for general template classes, partially specialized classes, completely specialized classes, and ordinary classes — with numerous code examples showing valid and invalid inheritance patterns.
📝 Lecture Summary
37.1. Resolution Order
When a compiler encounters a template class with multiple specializations (from partial to complete), it searches through these specializations in a specific sequence called resolution order. This is the order from most specific to most general.
🔑 Definition — Resolution Order: The sequence in which the compiler searches for the required template specialization: first complete specialization, then partial specialization, then general template.
Resolution Order: a. First, compiler looks for complete specialization b. If not found, it searches for partial specialization c. Finally, it searches for the general template
Example – Resolution Order
template< typename T >
class Vector { ... }; // general template
template< typename T >
class Vector< T* > { ... }; // partial specialization for pointers
template< >
class Vector< char* > { ... }; // complete specialization for char*
int main() {
Vector< char* > strVector; // complete specialization used
Vector< int* > iPtrVector; // partial specialization used
Vector< int > intVector; // general template used
return 0;
}
Explanation:
- For
Vector< char* >: Compiler finds the complete specializationVector< char* >first and stops. - For
Vector< int* >: No complete specialization exists forint*(only forchar*), so compiler moves to partial specialization and findsVector< T* >which works for all pointer types. - For
Vector< int >: No complete or partial specialization matchesint, so compiler uses the general templateVector< T >.
💡 Why this matters: Understanding resolution order ensures you know exactly which version of a template will execute, preventing unexpected behavior.
37.2. Function Template Overloading
We can also specialize function templates, which is called function template overloading.
template< typename T >
void sort( T ); // general template function
template< typename T >
void sort( Vector< T > & ); // specialization for Vector of any type
template< >
void sort< Vector<char*> >( Vector< char* > & ); // specialization for Vector<char*>
void sort( char* ); // ordinary function for char*
37.3. Resolution Order
For function templates, the compiler searches in this order:
- Ordinary Function (non-template function with same name)
- Complete Specialization
- Partial Specialization
- Generic Template
Example – Resolution Order
int main() {
char* str = “Hello World!”;
sort(str); // Ordinary function sort( char* )
Vector<char*> v1 = {“ab”, “cd”, ... };
sort(v1); // Complete specialization sort( Vector<char*> & )
Vector<int> v2 = { 5, 10, 15, 20 };
sort(v2); // Partial specialization sort( Vector<T> &)
int iArray[] = { 5, 2, 6, 70 };
sort(iArray); // General template sort( T )
return 0;
}
This follows the same order as class templates but with ordinary functions added at the highest priority.
💡 Why this matters: Ordinary functions always take precedence over template versions, which prevents unexpected template instantiation when a specific non-template function exists.
37.4. Templates and Inheritance
We can use inheritance with templates or their specializations, but must follow one key rule:
- If we have a template class, all classes derived from it should also be class templates.
- The derived class must take at least as many template parameters as the base class requires for instantiation.
37.5. Derivations in case of a General Template class
A class template may inherit from another class template:
template< class T >
class A { ... };
template< class T >
class B : public A< T > // same template parameter T ensures same instantiation type
{ ... };
int main() {
A< int > obj1;
B< int > obj2;
return 0;
}
A partial specialization may inherit from a class template:
template< class T >
class B< T* > : public A< T > // same template parameter T
{ ... };
int main() {
A< int > obj1;
B< int* > obj2;
return 0;
}
Complete specialization or ordinary class CANNOT inherit from a class template:
template< >
class B< char* > : public A< T > { ... };
// Error: 'T' undefined — derived class takes fewer parameters than base
class B : public A< T > { ... };
// Error: 'T' undefined — ordinary class cannot inherit from template
Derivations in case of a partially specialized class
A class template may inherit from a partial specialization:
class A { ... };
template< class T >
class A< T* > { ... };
template< class T >
class B : public A< T* >
{ ... };
int main() {
A< int* > obj1;
B< int > obj2;
return 0;
}
A partial specialization may inherit from a partial specialization:
template< class T >
class B< T* > : public A< T* >
{ ... };
int main() {
A< int* > obj1;
B< int* > obj2;
return 0;
}
Complete specialization or ordinary class CANNOT inherit from a partial specialization:
template< >
class B< int* > : public A< T* > { ... };
// Error: Undefined 'T'
class B : public A< T* > { ... };
// Error: Undefined 'T'
Derivations in case of Completely Specialized class
A class template may inherit from a complete specialization:
template< class T >
class B : public A< float* >
{ ... };
int main() {
A< float* > obj1;
B< int > obj2;
return 0;
}
A partial specialization may inherit from a complete specialization:
template< class T >
class B< T* > : public A< float* >
{ ... };
int main() {
A< float* > obj1;
B< int* > obj2;
return 0;
}
A complete specialization may inherit from a complete specialization:
template< >
class B< double* > : public A< float* >
{ ... };
int main() {
A< float* > obj1;
B< double* > obj2;
return 0;
}
An ordinary class may inherit from a complete specialization:
class B : public A< float* >
{ ... };
int main() {
A< float* > obj1;
B obj2;
return 0;
}
Derivations in case of Ordinary Class
A class template may inherit from an ordinary class:
class A { ... };
template< class T >
class B : public A
{ ... };
int main() {
A obj1;
B< int > obj2;
return 0;
}
A partial specialization may inherit from an ordinary class:
class A{ };
template<class T>
class B {};
template <class T>
class B<T*>: public A{ };
int main() {
A obj1;
B <int *> obj2;
return 0;
}
A complete specialization may inherit from an ordinary class:
template <class T>
class B{};
template< >
class B< char* > : public A
{ ... };
int main() {
A obj1;
B< char* > obj2;
return 0;
}
⭐ Key Takeaways
The resolution order for class templates is complete specialization → partial specialization → general template, while for function templates it adds ordinary functions first. Inheritance with templates requires the derived class to be a template taking at least as many parameters as the base. Complete specializations and ordinary classes cannot inherit from general or partial template classes because template parameters become undefined. However, any template type (general, partial, or complete) can inherit from a complete specialization or an ordinary class, as these provide concrete types.
🧠 Quick Revision Questions
- What is the exact resolution order the compiler follows when searching for class template specializations?
- Why does
class B : public A< T >cause a compilation error? - In function template overloading, what takes priority: an ordinary function or a complete specialization?
- Can a complete specialization inherit from a partial specialization? Why or why not?
- Which of the following is valid: a general template inheriting from an ordinary class, or an ordinary class inheriting from a general template?
📘 Lecture 38 — Templates and Friends
📖 Overview: This lecture explores how templates interact with the friend feature in C++. It establishes four rules governing the relationship between template classes/functions and friend declarations, explaining when friendship grants access to private members across different template instantiations.
🗂️ Topics Covered
The lecture covers four rules regarding templates and friends: Rule 1 explains that ordinary functions or classes declared as friends become friends of every template instantiation. Rules 2-4 address template friends and class template friends, specifying when friendship applies to specific or all instantiations based on type parameter relationships between the granting class and the friend.
📝 Lecture Summary
38.1. Templates and Friends
Templates and their specializations are compatible with the friendship feature of C++. Several rules apply when dealing with templates and friend functions/classes.
38.2. Templates and Friends – Rule 1
When an ordinary function or class is declared as a friend of a class template, it becomes a friend of each instantiation of that template class. This means the ordinary friend can access private members of any instantiation of the template class.
🔑 Definition — Rule 1: An ordinary (non-template) function or class declared as friend of a class template is a friend of every specialization of that template.
📌 Example (friend class): If class A is declared as friend of template<class T> class B, then A can access private data of both B<int> ib and B<char> cb.
📌 Example (friend function): If void doSomething(B<char>&) is declared as friend of template<class T> class B, then inside doSomething, we can access private data of both B<int> and B<char> objects.
38.3. Templates and Friends – Rule 2
Rule 2 applies when a template function or template class is made a friend of another template class. When a friend function template or friend class template is instantiated with the same type parameters as the class template granting friendship, then its instantiation for a specific type is a friend only of that class template instantiation for that particular type.
🔑 Definition — Rule 2: Using the same type parameter T for both the class template and its template friend means friendship is restricted to matching instantiations only.
📌 Example: In template<class T> class B, if we declare friend void doSomething(T) and friend A<T>, then:
doSomething(5)instantiatesdoSomething<int>which can access onlyB<int>'s private data.- If inside
doSomethingwe instantiateB<int>, it's OK only whendoSomethingis instantiated forint.
📌 Example (error): If inside doSomething<U> we write B<int> ib, then:
doSomething( i )withint iworksdoSomething( c )withchar cgenerates error becausedoSomething<char>tries to accessB<int>but friendship was granted only when T matches
38.4. Templates and Friends – Rule 3
When a friend function/class template takes different type parameters from the class template granting friendship, then each instantiation of the friend is a friend of each instantiation of the class template granting friendship. This removes the restriction imposed by Rule 2.
🔑 Definition — Rule 3: Using different type parameters for the class template and its template friend means every friend instantiation is a friend of every class template instantiation.
📌 Example: In template<class T> class B, if we declare:
template<class W> friend void doSomething(W);
template<class S> friend class A;
Then inside doSomething<U>, we can write B<int> ib even if doSomething is instantiated for char.
📌 Example (class): template<class T> class A can access B<char>, B<int>, or any B instantiation because friendship is universal across types.
38.5. Templates and Friends – Rule 4
Rule 4 states that when we declare a template as friend of any class, then all kinds of specializations of that template — explicit, implicit, and partial — also become friends of the class granting friendship.
🔑 Definition — Rule 4: Declaring a template as friend grants friendship to all its specializations (implicit, explicit, partial).
📌 Example (functions): If template<class U> friend void doSomething(U) is declared in class B, then:
- The general template
template<class U> void doSomething(U u)can accessB<int> ib.data - Even the explicit specialization
template<> void doSomething<char>(char u)can also accessB<int> ib.data
📌 Example (classes): If template<class U> friend class A is declared in template<class T> class B, then:
- The general template
template<class U> class Acan accessB<int> ib.data - The partial specialization
template<class U> class A<U*>can also accessB<int> ib.data
💡 Why this matters: Rule 2 is the most complex and requires careful attention. Rules 1, 3, and 4 are simpler — Rule 1 for ordinary friends, Rule 3 for template friends with different type parameters, and Rule 4 extending friendship to all specializations.
⭐ Key Takeaways
Rule 1 establishes that ordinary (non-template) friends of a class template gain access to all its instantiations. Rule 2 restricts friendship to matching type instantiations when the same type parameter is used for both the class template and its template friend. Rule 3 provides universal friendship when different type parameters are used — each friend instantiation can access any class template instantiation. Rule 4 extends friendship to all specializations (explicit, implicit, partial) of a template declared as friend. The most challenging for students is Rule 2, where mismatched instantiations cause compilation errors if a friend template tries to access a class template instantiation of a different type.
🧠 Quick Revision Questions
- Under Rule 1, if
class Ais declared as friend oftemplate<class T> class B, which instantiations of B can class A access? - In Rule 2, what happens if inside
doSomething<U>we instantiateB<int>but calldoSomething('c')from main? - How does Rule 3 differ from Rule 2 regarding the type parameters used in friend declarations?
- According to Rule 4, if we declare
template<class U> friend class Ain a class template, does the partial specializationA<U*>have friend access? - What is the key difference in behavior between Rule 1 (ordinary friend) and Rule 2 (template friend with same type parameter)?
📘 Lecture 39 — Templates & Static Members
📖 Overview: This lecture explores the behavior of static members within template classes, demonstrating how each instantiation of a template class has its own copy of static data members. It concludes the topic of templates with advantages and disadvantages, then revisits generic algorithms by applying them to a custom Vector container class.
🗂️ Topics Covered
The lecture covers static members in template classes with examples showing separate copies for different data types and shared copies for same type objects. It concludes the templates topic with advantages and disadvantages, then revisits generic algorithms by applying the find algorithm to a custom Vector container class, discussing implementation details and problems with the current approach.
📝 Lecture Summary
39.1. Templates & Static Members
In template classes, static members behave similarly to general classes, but with a crucial difference: each instantiation of a template class for a different data type gets its own copy of static data members. When the compiler creates template class implementations for different data types as required, each implementation has its own copy of static members.
🔑 Definition — Static Members in Template Classes: Static members in template classes are created when the template class is instantiated for each data type, and each instantiation has its own separate copy of static data members.
📐 Formula: template<class T> int A<T>::data = 0; → This initializes the static member data to 0 for each instantiation of template class A.
📌 Example 1 — Separate copies for different types:
template< class T > class A {
public:
static int data;
};
template<class T> int A<T>::data = 0;
int main() {
A< int > ia; // gets its own data
A< char > ca; // gets separate data
ia.data = 5;
ca.data = 7;
// Output: ia.data = 5, ca.data = 7
}
This demonstrates that A<int> and A<char> have independent static data members.
📌 Example 2 — Same type objects share static members:
A< int > ia, ib, ic; // all share same static data
ia.data = 5;
ib.data = 7;
ic.data = 9;
// Output: ia.data = 9, ib.data = 9, ic.data = 9
All objects of the same type (A<int>) share a single copy of the static member.
💡 Why this matters: Understanding that each template instantiation gets its own static members is crucial for correctly managing shared state in generic programming, especially when using static counters or shared resources.
39.2. Templates – Conclusion
Advantages of Templates:
- Reusability: Write code once for all data types
- Writability: Less code to write and maintain
Disadvantages of Templates:
- Can consume memory if used without care
- Reliability issues: Templates give the same implementation for all data types, which may not be correct for a particular data type
📌 Example of reliability problem:
template< typename T >
bool isEqual( T x, T y ) {
return ( x == y );
}
This function produces incorrect results for char* because it compares memory addresses (first character only) instead of string contents:
char* str1 = "Hello";
char* str2 = "World!";
isEqual( str1, str2 ); // Compiles but gives wrong result
39.3. Generic Algorithms Revisited
A generic algorithm is type-independent and independent of the underlying data structure. The find algorithm developed earlier is an example:
template< typename P, typename T >
P find( P start, P beyond, const T& x ) {
while ( start != beyond && *start != x )
++start;
return start;
}
🔑 Definition — Generic Algorithm: An algorithm that works for any aggregate object (container) that defines three operations: increment operator (++), dereferencing operator (*), and inequality operator (!=).
39.4. Generic Algorithms Revisited (continued)
To apply the generic find algorithm to a custom Vector template class, the Vector must support three operations:
- Increment operator (
++) - Dereferencing operator (
*) - Inequality operator (
!=)
The modified Vector class adds an integer data member index to track traversal position, with getter (getIndex()) and setter (setIndex()) methods.
Key Vector operators implementation:
operator++(): Increments index if less than sizeoperator*(): Returnsptr[index]operator!=(): Compares size and index, then element-by-element comparison
📌 Example — Applying find to Vector:
int main() {
Vector<int> iv(3); // First Vector: stores data
iv[0] = 10; iv[1] = 20; iv[2] = 30;
Vector<int> beyond(iv); // Second Vector: marks end
beyond.setIndex(3); // Points beyond last element
Vector<int> found(3); // Third Vector: stores result
found = find(iv, beyond, 20);
cout<<"Index: "<<found.getIndex(); // Output: Index: 1
}
39.5. Generic Algorithm (execution)
The find algorithm iterates through Vector elements:
| Iteration | iv size | beyond size | iv index | beyond index | start != beyond | *start != x |
|---|---|---|---|---|---|---|
| 1 | 3 | 3 | 0 | 3 | true | true |
| 2 | 3 | 3 | 1 | 3 | true | true (found 20 at index 1, loop exits) |
| 3 | 3 | 3 | 2 | 3 | true | undefined (not reached) |
| 4 | 3 | 3 | 3 | 3 | false | undefined |
The algorithm stops at iteration 2 when element 20 is found, returning Vector with index = 1.
39.6. Problems
Three major problems exist with this generic algorithm implementation:
a. No support for multiple traversals: Can only move forward in single steps, which is inefficient for large Vector objects. No facility for simultaneous forward and reverse traversal.
b. Inconsistent behavior: Uses whole container objects as markers instead of simple pointers. The found object is returned when a simple pointer would suffice.
c. Single traversal strategy only: No way to change traversal strategy (e.g., moving beyond more than one value in a single step).
⭐ Key Takeaways
This lecture demonstrates that template classes create separate static members for each data type instantiation, a critical distinction from general classes. The find algorithm showcases generic programming's power but reveals limitations when applied to custom containers like Vector, which requires proper operator overloading for ++, *, and !=. The three main problems with the current approach—lack of multiple traversals, inconsistent use of containers as markers, and single traversal strategy—highlight the need for a more robust abstraction like iterators. Students must understand that while templates offer reusability, they can lead to reliability issues when the same implementation is inappropriate for certain data types (e.g., char* comparison).
🧠 Quick Revision Questions
- What happens to static data members when a template class is instantiated for two different data types (e.g.,
A<int>andA<char>)? - Why does the
isEqualfunction template produce incorrect results forchar*pointers? - What three operations must a container support for the generic
findalgorithm to work? - In the Vector example, why must three separate Vector instances be created to use the
findalgorithm? - What are the three main problems identified with the current generic algorithm implementation?
📘 Lecture 40 — Iterators
📖 Overview: This lecture addresses the limitations of using external pointers (cursors) for container traversal, particularly the violation of data hiding and issues with non-contiguous containers. It introduces Iterators as a superior solution that allows multiple traversals on a single container without exposing its internal representation, while maintaining data abstraction and encapsulation principles.
🗂️ Topics Covered
The lecture first reviews the drawbacks of the previous approach using external pointers for container traversal, including no support for multiple traversals and inconsistent behavior. It then introduces cursors (external pointers) as an improvement, demonstrating their implementation in a Vector class with first(), beyond(), and next() methods. The lecture examines how cursors work well for contiguous containers like arrays but fail with non-contiguous containers like Sets. Finally, it presents Iterators as the complete solution — internal objects that traverse containers while hiding internal representation, implemented as a generic template class with operator overloading.
📝 Lecture Summary
Recap — Limitations of External Pointers
Previously, the generic find algorithm was applied to custom containers by supporting three operations: * (dereference), ++ (increment), and != (inequality comparison). However, this approach required creating three distinct Vector objects for a single find operation, which is inefficient for large containers. The major drawbacks included no support for multiple simultaneous traversals, support for only a single traversal strategy, and inconsistent behavior.
40.1. Cursors
Cursors are external pointers declared outside the container/aggregate object that traverse its elements. The aggregate object provides three helper methods to support cursor traversal:
T* first()— returns pointer to first elementT* beyond()— returns pointer to one position past the last elementT* next(T*)— returns pointer to the next element
📌 Example — Vector Class Implementation:
template<class T>
T* Vector<T>::first() {
return ptr;
}
template<class T>
T* Vector<T>::beyond() {
return (ptr + size);
}
template<class T>
T* Vector<T>::next(T* current) {
if (current < (ptr + size))
return (current + 1);
return current;
}
💡 Why this matters: Using cursors eliminates the need to create multiple Vector objects for traversal. Instead, we create multiple pointers (cursors) pointing into the same Vector object, which is far more memory-efficient.
📌 Example — Using Cursors:
int main() {
Vector<int> iv(3);
iv[0] = 10; iv[1] = 20; iv[2] = 30;
int* first = iv.first();
int* beyond = iv.beyond();
int* found = find(first, beyond, 20);
return 0;
}
The generic find method remains unchanged:
template<typename P, typename T>
P find(P start, P beyond, const T& x) {
while (start != beyond && *start != x)
++start;
return start;
}
Limitation — Non-contiguous containers: Cursors work fine for contiguous sequences like C++ arrays or Vector (implemented with arrays) where elements are at consecutive memory locations. However, cursors fail with non-contiguous containers (like Set, which will be studied later). The ++ operation on a simple pointer doesn't work because elements are not placed at consecutive memory addresses.
📌 Example — Problem with Set:
int main() {
Set<int> is(3);
is.add(10); is.add(20); is.add(30);
ET* first = iv.first(); // Error — incompatible types
ET* beyond = iv.beyond(); // Error
ET* found = find(first, beyond, 20); // Error — ++start fails
}
Solution for non-contiguous containers: Use the container's own traversal operations:
template<typename CT, typename ET>
P find(CT& cont, const ET& x) {
ET* start = cont.first();
ET* beyond = cont.beyond();
while (start != beyond && *start != x)
start = cont.next(start);
return start;
}
Cursors — Conclusion: The main benefit is using external pointers, allowing any number of simultaneous traversals — one pointer for each traversal. However, cursors cannot replace pointers for all containers.
40.2. Iterators
Iterators are internal data members of a container that traverse it without exposing its internal representation. They are for containers exactly like pointers are for ordinary data structures. Unlike cursors, iterators do not violate data hiding principles.
🔑 Definition — Iterator: An object that allows traversing a container's elements while maintaining data abstraction by emulating pointer behavior through operator overloading.
Generic Iterators: A general Iterator class can point to any container because it is implemented using templates. The container must still provide three methods: first(), beyond(), and next().
📌 Example — Generic Iterator Class:
template<class CT, class ET>
class Iterator {
CT* container;
ET* index;
public:
Iterator(CT* c, bool pointAtFirst = true);
Iterator(Iterator<CT, ET>& it);
Iterator& operator ++();
ET& operator *();
bool operator !=(Iterator<CT, ET>& it);
};
Constructor: Takes a pointer to a container and a boolean flag. If pointAtFirst is true, index points to the first element; otherwise, it points to beyond the last element.
template<class CT, class ET>
Iterator<CT,ET>::Iterator(CT* c, bool pointAtFirst) {
container = c;
if (pointAtFirst)
index = container->first();
else
index = container->beyond();
}
📌 Example — Complete Iterator Usage:
int main() {
Vector<int> iv(2);
iv[0] = 10; iv[1] = 20;
// Create two Iterator objects for the SAME Vector object
Iterator<Vector<int>, int> it(&iv); // points to first element
Iterator<Vector<int>, int> beyond(&iv, false); // points beyond last
Iterator<Vector<int>, int> found = find(it, beyond, 20);
return 0;
}
💡 Why this matters: We are NOT creating multiple Vector objects! We create multiple Iterator objects against one Vector object, which is efficient and respects data hiding.
Iterators — Advantages
- Multiple traversals: More than one traversal can be pending on a single container simultaneously
- Flexible strategy: Iterators allow changing the traversal strategy without changing the aggregate object
- Data abstraction: They contribute towards data abstraction by emulating pointers without exposing internal details
⭐ Key Takeaways
Cursors provide external pointers for container traversal but violate data hiding and fail with non-contiguous containers. Iterators solve both problems by being internal objects that traverse containers while hiding their internal representation. The Iterator class is implemented as a generic template requiring two type parameters (container type and element type) and overloads three operators: *, ++, and !=. The container must still provide first(), beyond(), and next() methods, but these are accessed through the Iterator, not directly by client code. The key advantage is that multiple Iterator objects can traverse the same container object without creating duplicate containers.
🧠 Quick Revision Questions
- What are the three main drawbacks of using external pointers for container traversal?
- Explain why cursors work for Vector but fail for Set containers.
- What is the fundamental difference between cursors and iterators regarding data hiding?
- Why does the Iterator class require two template parameters (CT and ET)?
- How does the Iterator constructor distinguish between pointing to the first element versus beyond the last element?
📘 Lecture 41 — Standard Template Library
📖 Overview: This lecture introduces the Standard Template Library (STL), a collection of standardized, reusable C++ template solutions for common programming problems. It covers the three core components of STL—containers, iterators, and algorithms—and provides a detailed examination of various container types, including sequence containers, associative containers, and container adapters, along with their common functions.
🗂️ Topics Covered
The lecture begins with an introduction to the STL and its three key components: containers, iterators, and algorithms. It then delves into STL containers, categorizing them into sequence containers (vector, deque, list), associative containers (set, multiset, map, multimap), and container adapters (stack, queue, priority_queue). The discussion includes examples for each container type, followed by an overview of common functions applicable to all containers and those specific to first-class containers. Finally, it covers container requirements, emphasizing the need for copy, assignment, and comparison operators in element classes.
📝 Lecture Summary
41.1. Standard Template Library:
The Standard Template Library (STL) is a collection of standardized, reusable C++ template solutions for common programming problems like searching and comparison. These solutions were developed and approved by the C++ standardization committee to operate efficiently across many different applications. STL consists of three key components: Containers, Iterators, and Algorithms. STL promotes reuse by eliminating the need to rewrite standard code, saving development time and cost. These solutions are thoroughly tested, reducing the chance of errors.
41.2. STL Containers
A Container is an object that holds a collection of data elements. STL provides three kinds of containers: Sequence Containers, Associative Containers, and Container Adapters.
Sequence Containers: A sequence organizes a finite set of objects, all of the same type, into a strictly linear arrangement. These include:
- vector: Supports rapid insertions and deletions at the back end and random access to elements.
- deque: Behave like a queue, supporting rapid insertions and deletions at the front or back, and random access to elements.
- list: A doubly linked list that allows rapid insertions and deletions anywhere. A list is a linear data structure, but access to elements requires sequential movement from the start.
💡 Why this matters: Sequence containers maintain elements in a linear order, but each type offers different performance trade-offs for insertion, deletion, and access operations, making it crucial to choose the right container for a given task.
Example – STL Vector: This code snippet demonstrates the use of a std::vector<int>. The user enters two integers, which are added to the vector using push_back(). The program outputs the current capacity and size of the vector.
#include <vector>
int main() {
std::vector<int> iv;
int x, y;
char ch;
do {
cout<<"Enter the first integer:";
cin >> x;
cout<<"Enter the second integer:";
cin >> y;
iv.push_back( x );
iv.push_back( y );
cout << “Current capacity of iv = “ << iv.capacity() << endl;
cout << “Current size of iv =“<< iv.size() << endl;
cout<<"Do you want to continue?";
cin >> ch;
} while ( ch == 'y' );
return 0;
}
Sample Output:
Enter the first integer: 1
Enter the second integer: 2
Current capacity of iv = 2
Current size of iv = 2
Do you want to continue? y
Enter the first integer: 3
Enter the second integer: 4
Current capacity of iv = 4
Current size of iv = 4
Do you want to continue? y
Enter the first integer: 5
Enter the second integer: 6
Current capacity of iv = 8
Current size of iv = 6
Do you want to continue? n
📐 Formula: capacity() returns the currently allocated storage space for the vector (which can grow automatically), while size() returns the number of elements currently stored. The capacity grows dynamically as needed (e.g., from 4 to 8 in the output) to accommodate more elements.
Example – STL Deque: This code snippet demonstrates the use of a std::deque<int>, adding elements at the front and back, and removing them from the front and back.
#include <deque>
int main() {
std::deque<int> dq;
dq.push_front( 3 );
dq.push_back( 5 );
dq.pop_front();
dq.pop_back();
return 0;
}
Example – STL List: This code snippet demonstrates the use of a std::list<float> and an iterator. It adds elements to the list and inserts a new element at a specific position using an iterator.
#include <list>
int main() {
std::list<float> _list;
_list.push_back( 7.8 );
_list.push_back( 8.9 );
std::list<float>::iterator it = _list.begin();
_list.insert( ++it, 5.3 );
return 0;
}
Associative Containers: An associative container provides fast retrieval of data based on keys. Elements are added using a key-based formula (e.g., value % 10) and retrieved using the same formula, allowing direct access without sequential traversal.
🔑 Definition — Associative Container: A container that provides fast retrieval of data based on keys, eliminating the need for linear search.
Associative containers include:
- set: No duplicates allowed.
- multiset: Duplicates allowed.
- map: No duplicate keys allowed.
- multimap: Duplicate keys allowed.
Example – STL Set: This code snippet demonstrates a std::set<char>. It inserts characters 'a', 'b', and 'b' again, and the output shows a size of 2 because sets do not allow duplicates.
#include <set>
int main() {
std::set<char> cs;
cout << “Size before insertions: “ << cs.size() << endl;
cs.insert( ‘a’ );
cs.insert( ‘b' );
cs.insert( ‘b' );
cout << “Size after insertions: ” << cs.size();
return 0;
}
Output:
Size before insertions: 0
Size after insertions: 2
Example – STL Multi-Set: This code snippet demonstrates a std::multiset<char>, which allows duplicates. The output shows a size of 3 after inserting 'a', 'b', and 'b'.
#include <set>
int main() {
std::multiset<char> cms;
cout << "Size before insertions: " << cms.size() << endl;
cms.insert( 'a' );
cms.insert( 'b' );
cms.insert( 'b' );
cout << "Size after insertions: " << cms.size();
return 0;
}
Output:
Size before insertions: 0
Size after insertions: 3
Example – STL Map: This code snippet demonstrates a std::map<int, char>. It inserts key-value pairs (1,'a'), (2,'b'), (3,'c') and uses find(2) to retrieve the iterator and print the value at key 2.
#include <map>
int main() {
typedef std::map<int, char> MyMap;
MyMap m;
m.insert(MyMap::value_type(1, 'a'));
m.insert(MyMap::value_type(2, 'b'));
m.insert(MyMap::value_type(3, 'c'));
MyMap::iterator it = m.find( 2 );
cout << "Value @ key " << it->first << " is " << it->second;
return 0;
}
Output:
Value @ key 2 is b
Example – STL Multi-Map: This code snippet demonstrates a std::multimap<int, char>, which allows duplicate keys. It inserts (1,'a'), (2,'b'), (3,'b') and retrieves values at keys 2 and 3.
#include <map>
int main() {
typedef std::multimap<int, char> MyMap;
MyMap m;
m.insert(MyMap::value_type(1, 'a'));
m.insert(MyMap::value_type(2, 'b'));
m.insert(MyMap::value_type(3, 'b'));
MyMap::iterator it1 = m.find( 2 );
MyMap::iterator it2 = m.find( 3 );
cout << "Value @ key " << it1->first << " is " << it1->second << endl;
cout << "Value @ key " << it2->first << " is " << it2->second << endl;
return 0;
}
Output:
Value @ key 2 is b
Value @ key 3 is b
First-class Containers: Sequence and associative containers are collectively referred to as the first-class containers.
Container Adapters: A container adapter is a constrained version of a first-class container. These include:
- stack: Follows Last In First Out (LIFO) principle. It can adapt a vector, deque, or list.
- queue: Follows First In First Out (FIFO) principle. It can adapt a deque or list.
- priority_queue: Also follows FIFO, but elements are added according to a priority (e.g., a printer queue with priority options). It can adapt a vector or deque.
41.3. Common Functions for All Containers
These are general functions that can be used with any container class:
- Default constructor
- Copy Constructor
- Destructor
- empty(): Returns true if the container contains no elements.
- max_size(): Returns the maximum number of elements the container can hold.
- size(): Returns the current number of elements.
operator = (): Assigns one container instance to another.operator < (): Returns true if the first container is less than the second.operator <= (): Returns true if the first container is less than or equal to the second.operator > (): Returns true if the first container is greater than the second.operator >= (): Returns true if the first container is greater than or equal to the second.operator == (): Returns true if the first container is equal to the second.operator != (): Returns true if the first container is not equal to the second.- swap(): Swaps the elements of two containers.
41.4. Functions for First-class Containers
First-class containers (sequence and associative) have the following additional functions:
- begin(): Returns an iterator object referring to the first element of the container.
- end(): Returns an iterator object referring to the position just beyond the last element.
- rbegin(): Returns an iterator object referring to the last element of the container (reverse begin).
- rend(): Returns an iterator object referring to the position before the first element (reverse end).
- erase(iterator): Removes an element pointed to by the iterator.
- erase(iterator, iterator): Removes a range of elements specified by two iterators.
- clear(): Erases all elements from the container.
41.5. Container Requirements
Elements added to containers must provide basic functionality because containers perform operations like copying and comparison on them.
-
When an element is inserted, a copy of that element is made using the Copy Constructor and Assignment Operator. Therefore, elements must support copying and assignment. Built-in C++ data types already provide this, and the compiler generates a default copy constructor and assignment operator for user-defined types (classes/structures) if not explicitly defined.
-
Associative containers and many algorithms compare elements. Therefore, elements must support the
operator ==andoperator <for comparison. C++ does not provide comparison operators by default for user-defined types, so developers must define them in the element class to use it in associative containers.
⭐ Key Takeaways
- The STL has three core components: containers (which hold data), iterators (which navigate through data), and algorithms (which process data).
- Sequence containers (vector, deque, list) maintain a linear arrangement with different performance characteristics; associative containers (set, map, etc.) use keys for fast retrieval and do not allow or allow duplicates based on the type; container adapters (stack, queue, priority_queue) provide constrained interfaces.
- Common functions like
size(),empty(),begin(),end(), anderase()are available across all containers, with first-class containers having additional functions likerbegin(),rend(), andclear(). - For elements to be used in STL containers, they must support copy construction and assignment. For use in associative containers and algorithms, they must also support
operator ==andoperator <. - The STL promotes code reuse and reliability by providing thoroughly tested, standard solutions that save development time and reduce errors.
🧠 Quick Revision Questions
- What are the three key components of the Standard Template Library (STL)?
- Name two sequence containers and two associative containers. What is the main difference in how they organize data?
- In the STL
vectorexample, why did the capacity change from 4 to 8 after adding elements in the third iteration? - What is a container adapter? Give one example and explain the principle it follows (e.g., FIFO or LIFO).
- Why must a user-defined class provide its own
operator ==andoperator <if its objects are to be used in an STL associative container likestd::set?
📘 Lecture 42 — Iterators
📖 Overview: This lecture provides a comprehensive overview of STL Iterators in C++, covering the different categories of iterators, their capabilities, and which container types support them. Understanding iterators is essential for efficiently traversing and manipulating STL containers without accessing their internal details.
🗂️ Topics Covered
The lecture covers STL iterators and their categories including input, output, forward, bidirectional, and random-access iterators. It explains which iterator types are compatible with different container categories (sequence containers, associative containers, and container adapters), details the operations supported by each iterator category, and provides multiple code examples demonstrating iterator usage. The lecture concludes with an introduction to STL algorithms that work with iterators.
📝 Lecture Summary
42.1. Iterators
We have studied about Iterators before; they are used to traverse Containers efficiently without accessing internal details. Now we see Iterators provided to us in standard template library. STL Iterators provide pointer operations such as * and ++, and they work for containers like pointers work for ordinary data structures.
💡 Why this matters: Iterators provide a uniform interface for accessing elements in different container types, allowing algorithms to work with any container without knowing its internal structure.
42.2. Iterator Categories
We can divide Iterators given to us in STL in the following categories:
a. Input Iterators b. Output Iterators c. Forward Iterators d. Bidirectional Iterators e. Random-access Iterators
Input Iterators Using input iterators we can read an element, and we can only move in forward direction one element at a time. These can be used in implementing those algorithms that can be solved in one pass (moving container once in single direction from start to end like the find algorithm we studied in last lecture).
Output Iterators Using output iterators we can read an element, and we can only move in forward direction one element at a time. These can be used in implementing those algorithms that can be solved in one pass (moving container once in single direction from start to end like the find algorithm we studied in last lecture).
Forward Iterators Forward iterators have the capabilities of both input and output Iterators. In addition, they can bookmark a position in the container (we can set one position as a bookmark while traversing the container, which will be more understandable when we see the example below).
Bidirectional Iterators They have all the capabilities of forward Iterators plus they can be moved in backward direction. As a result, they support multi-pass algorithms (algorithms that need to traverse the container more than once).
Random Access Iterators They have all the capabilities of bidirectional Iterators plus they can directly access any element of a container.
42.3. Iterator Summary
The following diagram shows the capabilities and scope of different iterators. You can see that Random access iterators have all the capabilities and input and output iterators have the least capabilities.
Forward
Input
Output
Random Access
Bidirectional
Forward
Input Output
42.4. Container and Iterator Types
We can use different types of iterators with different types of containers (according to the nature of the container).
42.5. Sequence Containers
| Container Type | Iterator Type | Reason |
|---|---|---|
| vector | random access | (as we can access any element of vector using its index, so we can use random access iterator) |
| deque | random access | (in deque we can add elements only in front and back, however we can access any element of deque using its index, so we can use random access Iterator) |
| list | bidirectional | (in list we can move in both directions in sequence, however cannot access an element at specific index randomly, so we can use bidirectional iterator with list) |
42.6. Associative Containers
In associative containers we save values based on keys, and we cannot access elements randomly based on indexes as elements are not stored at contiguous memory locations. However, we can traverse them in both directions, so we can use bidirectional iterators with them.
| Container Type | Iterator Type |
|---|---|
| set | bidirectional |
| multiset | bidirectional |
| map | bidirectional |
| multimap | bidirectional |
42.7. Container Adapters
Container adapters are made with special restrictions. The most important restriction is that they don't allow free traversal of their elements, so we CANNOT use iterators with them.
| Container Type | Iterator Type |
|---|---|
| stack | (none) |
| queue | (none) |
| priority_queue | (none) |
42.8. Iterator Operations
Iterators support the following operations:
All Iterators support:
p1 = p2— Assignment (two Iterators)p1 == p2— Equality operatorp1 != p2— Inequality operatorp->— Member access operator
Input Iterators support:
++p— Pre-increment an iteratorp++— Post-increment an Iterator*p— Dereference operator used as rvalue (right value, meaning they can be used on right side of expression) for reading only, not for assigning value as lvalue (left value, assignment only taken place when we are allowed to use them as left value)
Output Iterators support:
*p— Dereference operator (here we can use it for assigning new value as lvalue because the iterator is an output iterator that can be used to set any value)p1 = p2— Assignment
Forward Iterators: As Forward Iterators have combined properties of both input and output iterators, they support operations of both input and output Iterators.
Bidirectional Iterators: As bidirectional iterators can move in backward direction also, they support decrementing operations (moving pointer one element back):
--p— Pre-decrement operatorp--— Post-decrement operator
Random-access Iterators: Besides the operations of bidirectional Iterators, they also support:
Access Operator:
p + i— Result is an iterator pointing at p + ip – i— Result is an iterator pointing at p – ip += i— Increment iterator p by i positionsp –= i— Decrement iterator p by i positionsp[ i ]— Returns a reference of element at p + ip1 < p2— Returns true if p1 is before p2 in the containerp1 <= p2— Returns true if p1 is before p2 in the container or p1 is equal to p2p1 > p2— Returns true if p1 is after p2 in the containerp1 >= p2— Returns true if p1 is after p2 in the container or p1 is equal to p2
Example – Random Access Iterator
typedef std::vector< int > IntVector;
int main() {
const int SIZE = 3;
int iArray[ SIZE ] = { 1, 2, 3 };
IntVector iv(iArray, iArray + SIZE);
IntVector::iterator it = iv.begin();
cout << "Vector contents: ";
for ( int i = 0; i < SIZE; ++i )
cout << it[i] << ", ";
return 0;
}
Sample Output: Vector contents: 1, 2, 3,
Example – Bidirectional Iterator (using index operator – ERROR)
typedef std::set< int > IntSet;
int main() {
const int SIZE = 3;
int iArray[ SIZE ] = { 1, 2, 3 };
IntSet is( iArray, iArray + SIZE );
IntSet::iterator it = is.begin();
cout << "Set contents: ";
for (int i = 0; i < SIZE; ++i)
cout << it[i] << ", "; // Error — cannot use [] with bidirectional iterator
return 0;
}
Example – Bidirectional Iterator (using increment operator – CORRECT)
typedef std::set< int > IntSet;
int main() {
const int SIZE = 3;
int iArray[ SIZE ] = { 1, 2, 3 };
IntSet is( iArray, iArray + SIZE );
IntSet::iterator it = is.begin();
cout << "Set contents: ";
for ( int i = 0; i < SIZE; ++i )
cout << *it++ << ", "; // OK
return 0;
}
Sample Output: Set contents: 1, 2, 3,
Example – Bidirectional Iterator (traversing backward)
typedef std::set< int > IntSet;
int main() {
const int SIZE = 3;
int iArray[ SIZE ] = { 1, 2, 3 };
IntSet is( iArray, iArray + SIZE );
IntSet::iterator it = is.end();
cout << "Set contents: ";
for (int i = 0; i < SIZE; ++i)
cout << *--it << ", ";
return 0;
}
Sample Output: Set contents: 3, 2, 1,
Example – Input Iterator
#include <iostream>
using std::cin;
using std::cout;
using std::endl;
#include <iterator>
int main() {
int x, y, z;
cout << "Enter three integers:\n";
std::istream_iterator< int > inputIt( cin );
x = *inputIt++;
y = *inputIt++;
z = *inputIt;
cout << "x = " << x << endl;
cout << "y = " << y << endl;
cout << "z = " << z << endl;
return 0;
}
Example – Output Iterator (trying to write to input iterator – ERROR)
int main() {
int x = 5;
std::istream_iterator< int > inputIt( cin );
*inputIt = x; // Error — cannot assign to input iterator
return 0;
}
Example – Output Iterator
int main() {
int x = 1, y = 2, z = 3;
std::ostream_iterator< int > outputIt( cout, ", " );
*outputIt++ = x;
*outputIt++ = y;
*outputIt++ = z;
return 0;
}
Example – Output Iterator (trying to read from output iterator – ERROR)
int main() {
int x = 1, y = 2, z = 3;
std::ostream_iterator< int > outputIt( cout, ", " );
x = *outputIt++; // Error — cannot read from output iterator
return 0;
}
42.9. Algorithms
STL includes 70 standard algorithms.
These algorithms may use Iterators to manipulate containers.
STL algorithms also work for ordinary pointers and data structures.
An algorithm works with a particular container only if that container supports a particular Iterator category.
A multi-pass algorithm, for example, requires bidirectional Iterator(s) at least.
Algorithm Examples:
Mutating-Sequence Algorithms (that require changing of elements position):
copycopy_backwardfillfill_ngenerategenerate_niter_swappartition- ...others...
Non-Mutating-Sequence Algorithms (that don't require changing of element position):
adjacent_findcountcount_ifequalfindfind_eachfind_endfind_first_of- ...others...
Numeric Algorithms (involves mathematical calculation):
accumulateinner_productpartial_sumadjacent_difference
Example – copy Algorithm
#include <iostream>
using std::cout;
#include <vector>
#include <algorithm>
typedef std::vector< int > IntVector;
int main() {
int iArray[] = {1, 2, 3, 4, 5, 6};
IntVector iv( iArray, iArray + 6 );
std::ostream_iterator< int > output( cout, ", " );
std::copy( iv.begin(), iv.end(), output );
return 0;
}
Output: 1, 2, 3, 4, 5, 6,
Example – fill Algorithm
#include <iostream>
using std::cout;
using std::endl;
#include <vector>
#include <algorithm>
typedef std::vector< int > IntVector;
int main() {
int iArray[] = { 1, 2, 3, 4, 5 };
IntVector iv( iArray, iArray + 5 );
std::ostream_iterator< int > output( cout, ", " );
std::copy( iv.begin(), iv.end(), output );
std::fill(iv.begin(), iv.end(), 0);
cout << endl;
std::copy( iv.begin(), iv.end(), output );
return 0;
}
⭐ Key Takeaways
The five categories of STL iterators form a hierarchy where each adds capabilities: input iterators (read-only forward), output iterators (write-only forward), forward iterators (read/write with bookmarking), bidirectional iterators (forward/backward traversal for multi-pass algorithms), and random-access iterators (direct element access with arithmetic operations). Container-iterator compatibility is determined by container structure: vector and deque support random-access iterators, list and all associative containers (set, multiset, map, multimap) support bidirectional iterators, while container adapters (stack, queue, priority_queue) do not support iterators at all. STL algorithms like copy and fill work with iterators to manipulate containers, and an algorithm's required iterator category determines which containers it can operate on.
🧠 Quick Revision Questions
- What is the key difference between a forward iterator and a bidirectional iterator?
- Why can't you use a random-access iterator with a
listcontainer? - What operations does an input iterator support that an output iterator does not, and vice versa?
- Which iterator category is required for multi-pass algorithms, and why?
- What happens when you try to use the
[]operator (indexing) with aset::iterator?
📘 Lecture 43 — Techniques for Error Handling
📖 Overview: This lecture explores various error handling techniques in programming, ranging from basic abnormal termination to sophisticated exception handling. Understanding these methods is crucial for building robust, fault-tolerant applications that can gracefully recover from runtime errors without losing data.
🗂️ Topics Covered
The lecture covers five error handling techniques: abnormal termination, graceful termination, returning illegal values, returning error codes, and exception handling. It examines the limitations of traditional error handling approaches, including increased code complexity and mixing of error handling with program logic. The major focus is on exception handling using try, catch, and throw mechanisms, demonstrating how to separate main program logic from error handling code through custom exception classes and proper catch handler organization.
📝 Lecture Summary
Techniques for Error Handling
Programs may terminate abnormally or crash due to incorrect memory access, input/output errors, program faults, or external resource errors. Without error handling, work can be lost (e.g., text editor closing without saving). Five techniques are used: abnormal termination, graceful termination, return illegal value, return error code, and exception handling.
43.1. Example – Abnormal Termination
In abnormal termination, the program does nothing when an error occurs and is terminated abnormally by the operating system without saving data. The example shows a division by zero causing the program to crash.
void GetNumbers(int &a, int &b) { ... }
int Quotient(int a, int b) { return a / b; }
📌 Example: When user enters 10 and 0, output shows: "Program terminated abnormally" after displaying "Enter two integers".
43.2. Graceful Termination
Graceful termination uses if conditions to check for expected errors and performs clean-up tasks before exiting, preventing resource wastage.
int Quotient(int a, int b) {
if(b == 0) {
cout << "Denominator can't be zero" << endl;
exit(1); // clean exit with error code
}
return a / b;
}
📌 Example: Output shows "Denominator can't be zero" instead of abnormal termination.
43.3. Error Handling
a. Return Illegal Value
The clean-up tasks are of local nature only, and information loss remains possible. The programmer changes the value to prevent crash but puts program in inconsistent state.
int Quotient(int a, int b) {
if(b == 0)
b = 1; // illegal value substitution
OutputQuotient(a, b, a/b);
return a / b;
}
📌 Example: Input 10 and 0 shows "Quotient of 10 and 1 is 1" — wrong result, program continues in inconsistent state.
b. Return Error Code
Programmer avoids system crash but the program is now in an inconsistent state. The function returns a boolean to indicate success or failure.
bool Quotient(int a, int b, int &retVal) {
if(b == 0) return false;
retVal = a / b;
return true;
}
📌 Example: When denominator is zero, user is prompted repeatedly: "Denominator can't be Zero. Give input again" until a valid input is provided.
💡 Why this matters: Error codes pollute function interfaces and force callers to check return values, which they might ignore.
Issues in Error Handling
Traditional error handling creates several problems:
- Programmer must change design to incorporate error handling
- Must check return type of function to detect errors
- Calling function can ignore return value
- Function result might contain illegal value, causing later system crash
- Program complexity increases — error handling code mixes with program logic, code becomes less readable and difficult to modify
📌 Example comparison: Without error handling: function1(); function2(); function3(); which is clean and simple. With error handling: Nested if-statements checking each function's return value, creating deeply nested, hard-to-read code.
43.4. Exception Handling
Exception handling is a much more elegant solution compared to other error handling mechanisms. It enables separation of main logic and error handling code, keeping program flow clean.
43.5. Exception Handling Process
- Code suspected to cause an exception is written in try block
- Code encountering an error throws an object representing the exception
- Catch blocks follow the try block to catch thrown objects
Syntax – Throwing an Exception
The keyword throw is used to throw an exception. Any expression can represent the exception. Examples:
throw 1; // literal constant
throw (a); // variable
throw obj; // object
throw Exception(); // anonymous object
throw 1+2*9; // mathematical expression
🔑 Definition — Exception Object: Primitive data types should be avoided as throw expressions due to ambiguity. Define new classes to represent exceptions — this reduces ambiguity.
Syntax – Try and Catch
try {
// code that may throw exception
}
catch (Exception1) { ... } // caught if exception was thrown
catch (Exception2 obj) { ... } // caught if exception was thrown
Catch Rules:
- Catch handler must be preceded by a try block or another catch handler
- Catch handlers only execute when exception occurs
- Differentiated on basis of argument type
- Tried in order written
- Work like switch statements without needing
break
Complete Example of try, catch, and throw
class DivideByZero {
public:
DivideByZero() {}
};
int Quotient(int a, int b) {
if(b == 0) {
throw DivideByZero(); // throw class object as exception
}
return a / b;
}
Main program with try-catch:
for(int i = 0; i < 10; i++) {
try {
GetNumbers(a,b);
quot = Quotient(a,b);
OutputQuotient(a,b,quot);
sum += quot;
}
catch(DivideByZero) {
i--;
cout << "\nAttempt to divide numerator with zero";
}
}
📌 Output when denominator is zero: "Attempt to divide numerator (dividend) with zero" — program continues normally for remaining iterations.
Catch Handler
The catch handler catches the DivideByZero exception through an anonymous object. Program logic and error handling code are now separated cleanly.
💡 Why this matters: The object can be modified to carry information about the cause of the error, making error reporting more detailed.
Separation of Program Logic and Error Handling
With exception handling, the main program logic remains clean and simple:
try {
function1();
function2();
function3();
}
catch(ErrorX) { ... }
catch(ErrorY) { ... }
catch(ErrorZ) { ... }
The error handling code is now completely separate from the business logic, making code more readable, maintainable, and easier to modify.
⭐ Key Takeaways
Exception handling is the most elegant error handling technique because it completely separates error handling logic from main program flow, unlike other methods that mix concerns and increase complexity. The try block contains code that might fail, the throw statement sends an exception object (preferably a custom class, not primitive types) when an error occurs, and catch blocks handle specific exception types in order of declaration. Return error codes and illegal values force programmers to change function interfaces and check return values, while abnormal termination loses all data without cleanup. Using dedicated exception classes instead of primitive types prevents ambiguity and allows carrying rich error information.
🧠 Quick Revision Questions
- What are the five error handling techniques discussed, and what is the main disadvantage of the first four compared to exception handling?
- Why should primitive data types be avoided when throwing exceptions, and what should be used instead?
- In the graceful termination example, what keyword is used to exit the program after performing cleanup?
- How does exception handling improve code readability compared to return error codes?
- What determines which catch block executes when multiple catch blocks follow a try block?
📘 Lecture 44 — Stack Unwinding in Exception Handling
📖 Overview: This lecture examines what happens to local variables when exceptions are thrown, introducing the concept of stack unwinding—how try-catch blocks are unwound during nested function calls or nested try-catch blocks. It also covers catch handler behavior, inheritance of exceptions, re-throwing, and the order of handlers.
🗂️ Topics Covered
Stack unwinding with nested try-catch blocks and nested functions; destruction of local objects and dynamically allocated memory during exceptions; examples of stack unwinding with function calls and nested blocks; catch handler modifications to carry error information; passing exceptions by reference; destruction of exception objects; avoiding too many catch handlers using inheritance; catching every exception with catch(...); re-throwing exceptions for partial handling; and the order of handlers.
📝 Lecture Summary
Previous Lecture Example of Exception Handling
This lecture begins with a review example showing the DivideByZero exception class and the Quotient function that throws this exception when the divisor is zero. The main function uses a try-catch block to call Quotient and catch the DivideByZero exception.
Now, we want to see what happens to local variables in a try block when an exception is thrown; this concept is called stack unwinding, which tells how try-catch blocks are unwound (executed) when there are nested function calls involving try-catch blocks or nested try-catch blocks themselves.
44.1.Stack Unwinding
The flow control (the order in which code statements and function calls are made) as a result of throw statement is referred to as "stack unwinding".
Stack unwinding can take place in the following two ways:
- Nested try-catch blocks (one try-catch block inside another):
try {
try {
// code
} catch(Exception e) {
// handler
}
} catch(exception e) {
// handler
}
- Exception thrown from nested functions having try-catch blocks:
void function1() {
throw Exception();
}
void function2() {
function1();
}
int main() {
try {
function2();
} catch(Exception) { }
return 0;
}
Stack unwinding is more complex than simple nested function calls (or recursive function calls) as in the case of nested try-catch blocks, exceptions can be thrown from any try block, so transfer of control to catch handler is complex.
First note these points:
- ✅ All the local objects of an executing block are destroyed when an exception is thrown
- ✅ Dynamically allocated memory is not destroyed automatically
- ✅ If no catch handler catches the exception, the function terminate is called, which by default calls function abort
— Examples —
Nested Functions example:
In the example below, we have two functions function1 and function2; function2 calls function1. In function1 we have added exception throwing code, so it is necessary to call function1 in try-catch blocks; otherwise the compiler will generate an error. We call function2 in main; note that function2 itself calls function1 that needs a try-catch block, so we need to call function2 in a try-catch block. In case function1 code generates an exception, stack unwinding takes place: control will be returned to function2 which will return control to main.
main() -> function2() -> function1() throws exception
↓
main() ← function2() ← (stack unwinding occurs)
Nested Try-catch blocks example: Stack unwinding is also performed if we have nested try-catch blocks:
int main() {
try {
try {
throw 1;
}
catch(float) { }
}
catch(int) {
}
return 0;
}
📌 Example — Stack Unwinding Order:
-
If exception is thrown from the innermost try block:
- Firstly, the catch handler with float parameter is tried (innermost); this catch handler will not be executed as its parameter is of a different type — no coercion (match)
- Secondly, the catch handler with int parameter is tried and executed
-
If exception is thrown from the outer try block, there is no other try block above it, so only this block's catch handler will be matched with the exception. If it matches, the catch block is executed; otherwise, the default terminate and abort functions will be called.
Catch Handler
- We can modify the code in the catch handler to use the exception object to carry information about the cause of error
- The exception object thrown is copied to the object given in the handler
- We pass the exception by reference instead of by value in the catch handler to avoid problems caused by shallow copy
Example:
We have added a method Print in our exception class to show the user the cause of error:
class DivideByZero { // exception class
int numerator;
public:
DivideByZero(int i) { // constructor taking one parameter (dividend)
numerator = i;
}
void Print() const {
cout << endl << numerator << " was divided by zero";
}
};
int Quotient(int a, int b) {
if(b == 0) {
throw DivideByZero(a);
}
return a / b;
}
for (int i = 0; i < 10; i++) {
try {
GetNumbers(a, b);
quot = Quotient(a, b);
// ...
} catch(DivideByZero & obj) {
obj.Print();
i--;
}
}
Output:
Enter two integers
10
10
Quotient of 10 and 10 is 1
Enter two integers
10
0
10 was divided by zero
...
🔑 Catch Handler: The object thrown as exception is destroyed when the execution of the catch handler completes.
Avoiding too many Catch Handlers
There are two ways to catch more than one object in a single catch handler:
- Use inheritance
- Catch every exception
Inheritance of Exceptions
In inheritance, we group all exceptions according to their categories and catch a single exception for the whole category. For example, for code below we have divided the exceptions as follows:
- Math exceptions (DivideByZero and IntegerOutOfRange exception)
- Input Output exceptions (InputStreamError)
Without inheritance:
try {
// ...
}
catch(DivideByZero) { ... }
catch(IntegerOutOfRange) { ... }
catch(InputStreamError) { ... }
Example — With Inheritance:
try {
// ...
}
catch(MathError) { }
catch(InputStreamError) { }
By using a base class MathError for both DivideByZero and IntegerOutOfRange, we can catch them both with a single catch(MathError) handler.
Catch Every Exception
C++ provides a special syntax that allows catching every object thrown:
catch(...) {
// ...
}
Re-Throw
A function can catch an exception and perform partial handling. Re-throw is a mechanism of throwing the exception again after partial handling:
throw; /*without any expression*/
Example:
void Function() {
try {
/*Code that might throw an Exception*/
}
catch(Exception&) {
if(can_handle_completely) {
// handle exception
} else {
// partially handle exception
throw; //re-throw exception
}
} // end of catch
} // end of function
int main() {
try {
Function();
}
catch(Exception&) {
// ...
}
return 0;
}
Order of Handlers
The order of more than one catch handlers can cause logical errors when using inheritance or catch all (although the compiler will not generate any error in this case):
try {
// ...
}
catch(...) { } // catches EVERYTHING
catch(MathError) { ... }
catch(DivideByZero) { ... }
// last two handlers can NEVER be invoked
// as general exception class will catch all exceptions
// including the next two
💡 Why this matters: The last two handlers (MathError and DivideByZero) can never be invoked because the catch(...) handler catches all exceptions first, making subsequent handlers unreachable. Always place more specific handlers before more general ones.
⭐ Key Takeaways
Stack unwinding is the process of destroying local objects and returning control through nested function calls or nested try-catch blocks when an exception is thrown. All local objects in an executing block are destroyed, but dynamically allocated memory must be manually freed. Catch handlers can receive exception objects by reference to avoid shallow copy problems, and the exception object is destroyed when the handler completes. To avoid too many catch handlers, use inheritance (group related exceptions under a base class) or the catch(...) syntax to catch all exceptions. When using inheritance or catch-all handlers, always order them from most specific to most general; otherwise, more specific handlers will never execute. Re-throwing (throw; without an expression) allows partial handling of an exception before passing it up the call stack.
🧠 Quick Revision Questions
- What is stack unwinding, and in what two situations does it occur?
- What happens to local objects and dynamically allocated memory when an exception is thrown?
- Why should exception objects be passed by reference rather than by value in a catch handler?
- What are the two techniques to reduce the number of catch handlers in a program?
- If you place
catch(...)before a more specific handler likecatch(MathError), what will happen, and why?
📘 Lecture 45 — Resource Management
📖 Overview: This lecture focuses on managing resources in C++ in the presence of exceptions, including proper resource release, exceptions in constructors and destructors, and exception specifications. It concludes with a comprehensive course review covering all major topics from Object Orientation to Exception Handling.
🗂️ Topics Covered
The lecture covers resource management techniques when exceptions occur, including methods to ensure file resources are properly released, exception handling in constructors (including initialization lists), rules for exceptions in destructors, exception specification syntax and behavior, and ends with a full course review of Object Oriented Programming concepts.
📝 Lecture Summary
45.1. Resource Management
A function that acquires a resource must properly release it. Throwing an exception can cause resource wastage because the code to release the resource may be skipped.
🔑 Definition — Resource Management: The practice of ensuring that acquired resources (like file handles, memory) are properly released, even when exceptions occur.
📌 Example — Without proper management:
int function1(){
FILE *fileptr = fopen("filename.txt","w");
// ...
throw exception();
// ...
fclose(fileptr); // This line is never reached if exception is thrown
return 0;
}
In case of exception, the call to fclose will be ignored and the file will remain opened.
First Attempt — Using try-catch:
int function1(){
try{
FILE *fileptr = fopen("filename.txt","w");
fwrite("Hello World",1,11,fileptr);
// ...
throw exception();
fclose(fileptr);
} catch(...) {
fclose(fileptr); // adding fclose in catch handler as well
throw;
}
return 0;
}
But this results in code duplication.
Second Attempt — Using a separate class:
class FilePtr{
FILE * f;
public:
FilePtr(const char *name, const char * mode) {
f = fopen(name, mode);
}
~FilePtr() {
fclose(f);
}
operator FILE * () {
return f;
}
};
int function1(){
FilePtr file("filename.txt","w");
fwrite("Hello World",1,11,file);
throw exception();
// ...
return 0;
}
The destructor of the FilePtr class will close the file. The programmer does not have to close the file explicitly in case of error as well as in normal case. Objects and local variables in try block are destroyed automatically when try block completes its execution or in case exception is thrown, so this file object will automatically be destroyed.
💡 Why this matters: This pattern (Resource Acquisition Is Initialization - RAII) ensures automatic resource cleanup regardless of how the block is exited, making code safer and simpler.
Exception in Constructors
An exception thrown in a constructor causes the destructor to be called for any object built as part of the object being constructed before the exception is thrown. However, the destructor for the partially constructed object itself is not called.
📌 Example:
class Student{
String FirstName;
String SecondName;
String EmailAddress;
// ...
};
If the constructor of SecondName throws an exception, then the destructor for FirstName will be called. Generally, if an exception is thrown in a constructor, all objects created so far are destroyed. If EmailAddress String object had thrown an exception, then SecondName and FirstName objects will be destroyed using their destructors. However, the destructor of the Student class itself will not be called in any case, as its object was not completely constructed.
🔑 Definition — Partially Constructed Object: An object whose constructor did not complete execution due to an exception. Its destructor is not called.
Exception in Initialization List
An exception due to the constructor of any contained object or the constructor of a parent class can be caught in the member initialization list.
📌 Example:
Student::Student (String aName) : name(aName)
/*The constructor of String can throw a exception*/
{
// ...
}
The programmer may want to catch the exception and perform some action to rectify the problem:
Student::Student (String aName)
try : name(aName) {
// ...
}
catch(...) {
}
🔑 Definition — Function-try-block: A try block that encloses the entire function body, including the member initialization list, allowing exceptions from constructors of base or member objects to be caught.
Exceptions in Destructors
An exception should not leave the destructor. When a destructor is running, it means there is a stack unwinding process going on that has run this destructor to delete this object. If this exception is allowed to propagate, it will run another stack unwinding mechanism, which is not allowed. C++ allows running only one stack unwinding process at a time.
If a destructor is called due to stack unwinding, and an exception leaves the destructor, then the function std::terminate() is called, which by default calls std::abort().
📌 Example — Incorrect:
class Complex{
// ...
public:
~Complex(){
throw Exception();
}
};
int main(){
try{
Complex obj;
throw Exception();
// ...
}
catch(...){
}
return 0;
}
// The program will terminate abnormally
✅ Solution — Catch the exception inside the destructor itself:
Complex::~Complex()
{
try{
throw Exception();
}
catch(...){
}
}
In this case, a single stack unwinding process may handle the situation.
🔑 Definition — std::terminate(): A function called when the exception handling mechanism cannot find a handler for a thrown exception, or when an exception propagates out of a destructor during stack unwinding. It typically terminates the program.
Exception Specification
A program can specify the list of exceptions a function is allowed to throw. This list is also called a throw list. If we write an empty list, the function won't be able to throw any exception.
📐 Syntax:
void Function1() {...} // Can throw any exception
void Function2() throw () {...} // Cannot throw any exception
void Function3() throw (Exception1, ...){} // Can throw Exception1 or derived types
Here:
Function1can throw any exception.Function2cannot throw any exception.Function3can throw any exception of typeException1or any class derived from it.
If a function throws an exception other than those specified in the throw list, the function unexpected is called, which in turn calls terminate and terminates the program. If the programmer wants to handle such cases, they must provide a handler function and tell the compiler to call that handler using set_unexpected.
🔑 Definition — Exception Specification (throw list): A C++ feature that declares which exception types a function may throw. Violating this specification calls std::unexpected().
Course Review
The course covered the following major topics:
Object Orientation:
- What is an object
- Object-Oriented Model: Information Hiding, Encapsulation, Abstraction
- Classes
Object Orientation (continued):
- Inheritance: Generalization, Sub-Typing, Specialization
- "IS-A" relationship
- Abstract classes
- Concrete classes
Object Orientation (continued):
- Multiple inheritance
- Types of association: Simple association, Composition, Aggregation
- Polymorphism
Classes – C++ Constructs:
- Classes: Data members, Member functions
- Access specifier
- Constructors
- Copy Constructors
- Destructors
Classes – C++ Constructs (continued):
thispointer- Constant objects
- Static data member
- Static member function
- Dynamic allocation
Classes – C++ Constructs (continued):
- Friend classes
- Friend functions
- Operator overloading: Binary operator, Unary operator, operator[], Type conversion
Inheritance – C++ Constructs:
- Public inheritance
- Private inheritance
- Protected inheritance
- Overriding
- Class hierarchy
Polymorphism – C++ Constructs:
- Static type vs. dynamic type
- Virtual function
- Virtual destructor
- V-tables
- Multiple inheritance
- Virtual inheritance
Templates – C++ Constructs:
- Generic programming
- Classes template
- Function templates
- Generic algorithm
- Templates specialization: Partial Specialization, Complete specialization
Templates – C++ Constructs (continued):
- Inheritance and templates
- Friends and templates
- STL: Containers, Iterators, Algorithms
Writing Reliable Programs:
- Error handling techniques: Abnormal termination, Graceful termination, Return the illegal value, Return error code from a function, Exception handling
⭐ Key Takeaways
Resource management with RAII (using class destructors to release resources) is the safest way to handle exceptions as it ensures automatic cleanup without code duplication. Exceptions in constructors cause destruction of fully constructed sub-objects but not the partially constructed object itself; use function-try-blocks to catch exceptions from initialization lists. Exceptions must never leave a destructor, especially during stack unwinding, as this will call std::terminate(). Exception specifications (throw lists) declare what exceptions a function may throw, and violating them triggers std::unexpected(). The course review confirms that object orientation, inheritance, polymorphism, templates, STL, and exception handling are all interconnected tools for building reliable C++ programs.
🧠 Quick Revision Questions
- What happens to the file handle in the first example when an exception is thrown before
fclose()is called, and how does the RAII approach withFilePtrsolve this? - If a constructor for
Studentcontains threeStringmember objects and the constructor for the third member throws, which destructors are called and why isn'tStudent's destructor called? - What is the behavior when an exception propagates out of a destructor that was already called during stack unwinding from another exception?
- How would you write a function that is guaranteed to throw no exceptions using an exception specification?
- What function is called if a function throws an exception not listed in its throw list, and what is the default behavior?