Skip to content

Inheritance, Polymorphism, Error Handling

Inheritance

One of the key pillars of object orientation is inheritance, which allows us to specialize an existing class for specific cases — extending or refining its existing properties and functions.
To explore how inheritance works and what possibilities the language offers, let’s first define a base class.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class Student {
  std::string neptun;
protected:
  std::string name;
  unsigned enrolledHours;
public:
  Student(const std::string& name = "John Doe", unsigned enrolledHours = 0) :
    name(name), enrolledHours(enrolledHours) {
    neptun = name + std::to_string(enrolledHours);
  }

  const std::string & getName() const { return name; }
  const std::string & getNeptun() const { return neptun; }
  unsigned getEnrolledHours() const { return enrolledHours; }

  void attendClasses() const {
    std::cout << "The student attends classes: " << neptun << std::endl;
  }
};

As we can see, we created a Student class.
The neptun data member is private, meaning it can only be accessed within this class.
However, the name and enrolledHours members are not the usual private — they are marked as protected.
Protected members can also be accessed by derived classes, as we have seen before.

This means that name and enrolledHours can be directly modified in derived types, while neptun (the university identifier) remains a private property of each instance — inaccessible and unmodifiable from outside the class.

A PhD student is just like any other student, except that besides attending classes, they can also teach — that is, the original type is extended into a more specialized case.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class PhDStudent : public Student {
  unsigned taughtHours;
public:
  PhDStudent(const std::string& name, unsigned enrolledHours, unsigned taughtHours) :
    Student(name, enrolledHours),
    taughtHours(taughtHours) {}

  unsigned getTaughtHours() const { return taughtHours; }

  void teach() const {
    std::cout << "The PhD student is teaching a class" << std::endl;
  }
};

With this, we have created a PhDStudent type derived from the Student class.
You can imagine this as placing a larger shell around the original object sphere:

derived

Visibility of inheritance

1
class PhDStudent : public Student {

Inheritance is specified after a colon (:), but it is very important to define its visibility.
As shown here, the inheritance is public. This means that whatever the child class inherits from the parent, their visibility remains unchanged.

If the inheritance type is protected, then the parent’s originally public members will behave as if they were marked protected in the child class.
If we use private — or omit the specifier entirely — the inherited members will appear in the child class with private visibility.

Let’s illustrate this with a small example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <iostream>

class Base {
  int priv;
protected:
  int prot;
public:
  int pub;
};

class PublicChild : public Base {
public:
  void goo() { 
    std::cout << prot << " " << pub << std::endl; 
  }  // 'priv' is private in the base class, so the child class cannot access it
};

class ProtectedChild : protected Base {
  void goo() { 
    std::cout << prot << " " << pub << std::endl; 
  } 
};

class PrivateChild : Base { 
  // or equivalently: class PrivateChild : private Base {
public:
  void goo() {
    std::cout << prot << " " << pub << std::endl;  
  }
};

int main() {
  Base b;
  std::cout << b.pub << std::endl;
  // std::cout << b.prot << std::endl; // compilation error, inaccessible
  // std::cout << b.priv << std::endl; // compilation error, inaccessible

  PublicChild publ_c;
  std::cout << publ_c.pub << std::endl;   // Since 'public' remained public, it’s accessible
  // std::cout << publ_c.prot << std::endl; // Still not accessible, same for 'priv'

  ProtectedChild prot_c;
  // std::cout << prot_c.pub << std::endl; // Now even this is inaccessible, since 'pub' became protected in this class

  PrivateChild priv_c;
  priv_c.goo();   // Accessible, since the class itself can access its inherited members!
}

It is clear that the visibility of inheritance does not hide members from the derived class itself — instead, it changes the visibility of inherited members within that class.

Even if the inheritance is declared as private, the derived class can still access the inherited members that were not private in the base class.
What changes is that other classes will no longer be able to access those inherited members through this derived class or type.

Base Class Constructor

As shown in the figure above, the derived class contains the base class.
Therefore, if we want to fully initialize the derived class, we must also initialize the base class within it.

If there is no default initialization (i.e., no default constructor), we must specify how or which constructor should be used to initialize the base class.

We did this in the case of PhD_hallgato as follows:

1
2
3
PhD_student(const std::string name, unsigned enrolledHours, unsigned taughtHours) :
  Student(name, enrolledHours),
  taughtHours(taughtHours) {}

It can be seen that the first element in the initializer list is a call to a Student constructor.
This is how we specify what data should be passed to the base part of the object.
It is important that base class constructors are always called first — only after that can we initialize the derived class’s own data members!

Base Class Member Functions

We have seen how to initialize data belonging to the base class and that we cannot access the base class’s private members directly.
However, the information passed to the constructor is not lost, since if the base class provides methods (such as getters) that access private data, those methods can be used in the derived class as well — provided they are not private themselves.

In the following example, we demonstrate that a member function of PhD_student can call the inherited Student::attendClasses() method, which in turn works with the base class’s private data.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class Student {
  std::string id;
protected:
  std::string name;
  unsigned enrolledHours;
public:
  Student(const std::string& name = "John Doe", unsigned enrolledHours = 0) :
    name(name), enrolledHours(enrolledHours) {
      id = name + " " + std::to_string(enrolledHours);
  }

  const std::string & getName() const { return name; }
  const std::string & getId() const { return id; }
  unsigned getEnrolledHours() const { return enrolledHours; }

  void attendClasses() const {
    std::cout << "The student is attending a class: " << id << std::endl;
  }
};

class PhD_student : public Student {
  unsigned taughtHours;
public:
  PhD_student(const std::string name, unsigned enrolledHours, unsigned taughtHours) :
    Student(name, enrolledHours),
    taughtHours(taughtHours) {}

  unsigned getTaughtHours() const { return taughtHours; }

  void teach() const {
    std::cout << "The PhD student is teaching a class" << std::endl;
  }

  void attendClasses() const {
    std::cout << "Now the PhD student went to attend a class" << std::endl;
    Student::attendClasses();
  }
};

int main() {
    PhD_student phd("Meme Joe", 5, 3);
    phd.attendClasses();
}

Output

Now the PhD student went to attend a class
The student is attending a class: Meme Joe 5

In C++, it is possible for both the base class and the derived class to contain methods with the same signature.
In this example, the attendClasses method is such a case.

If we want to explicitly call the version of the method defined in the Student (base) class from within the PhD_student class, we must use the scope resolution operator ::.

In the main function, we create a PhD_student object and first call the attendClasses method defined in the PhD_student class.
This method, in turn, uses scope resolution to call the identically named method defined in the base class (Student).

Polymorphism

One of the key features of object-oriented programming languages is dynamic polymorphism — when, at a given point in the program, we reference an object through its base class type, yet the object is able to respond according to its actual (derived) type when methods are called on it.

In the case of Student and PhD_student, we can write functions that expect a Student, but receive a PhD_student.
Within these functions, we can only access the methods defined in the Student class through the parameter.
However, in some cases, we may want the overridden version of a method in the derived class to be called instead of the base version.

For overriding to occur, the following must match between the base and derived class methods:

  • the method name
  • the method parameters
  • the method qualifiers (e.g., const)

In the example above, the method void attendClasses() const satisfies these conditions in both the Student and PhD_student classes.

Let’s see what happens if we write a function that takes a Student or a Student& parameter and calls its attendClasses method!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include <string>
#include <iostream>

class Student {
  std::string id;
protected:
  std::string name;
  unsigned enrolledHours;
public:
  Student(const std::string& name = "John Doe", unsigned enrolledHours = 0) :
    name(name), enrolledHours(enrolledHours) {
    id = name + " " + std::to_string(enrolledHours);
  }

  const std::string & getName() const { return name; }
  const std::string & getId() const { return id; }
  unsigned getEnrolledHours() const { return enrolledHours; }

  void attendClasses() const {
    std::cout << "The student is attending a class: " << id << std::endl;
  }
};

class PhD_student : public Student {
  unsigned taughtHours;
public:
  PhD_student(const std::string name, unsigned enrolledHours, unsigned taughtHours) :
    Student(name, enrolledHours),
    taughtHours(taughtHours) {}

  unsigned getTaughtHours() const { return taughtHours; }

  void teach() const {
    std::cout << "The PhD student is teaching a class" << std::endl;
  }

  void attendClasses() const {
    std::cout << "Now the PhD student went to attend a class" << std::endl;
    Student::attendClasses();
  }
};

/*void attendClass(Student student) {
  student.attendClasses();
}*/

void attendClass(Student& student) {
  student.attendClasses();
}

int main() {
  PhD_student phd("Meme Joe", 5, 3);
  Student student("Simple", 6);
  attendClass(phd);
  attendClass(student);
}

Output

The student is attending a class: Meme Joe 5
The student is attending a class: Simple 6

Whichever case we choose — whether we pass a Hallgato or a Hallgato& to our global oratHallgat function — it will always call the method defined in the Hallgato class.
Thus, dynamic polymorphism has not been achieved this way.
To understand why, let’s take a closer look at what happens in each case!

Pass-by-Value Parameter

In this case, we explicitly define that the oratHallgat function expects a parameter of type Hallgato.
When we pass a PhD_hallgato object to it, the system creates a copy of that object — but this copy only includes the data that corresponds to the static type, i.e., Hallgato.

polymorph_copy_non_virtual

Pass-by-Reference

As we’ve seen many times before, when passing by reference, we are referring to the original object,
but we view it as an instance of its base class.
This means that while we can only access the base part of the object, the complete derived object still exists around it.

polymorph_reference_non_virtual

However, even if we pass a PhD_hallgato object to the oratHallgat function as a simple Hallgato reference,
the method that gets called is still the one defined in the base class (Hallgato).
The reason is that the type used in the function call is determined statically (i.e., at compile time), not at runtime.

To ensure that the method corresponding to the dynamic type (the actual type of the object) is called instead,
we must use virtualization.
Virtualization uses a so-called virtual table (vtable) that describes how each object should behave during polymorphic calls.

In C++, this virtual table mechanism is not enabled by default, as it requires extra resources.
However, we can specify which methods should use it by marking them with the virtual keyword
(it is sufficient to do so in the base class).

Virtualization

As we’ve seen, virtualization is not enabled by default!
Let’s look at a modified example where we make the orakatHallgat method virtual.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#include <string>
#include <iostream>

using namespace std; // so we don't have to write "std::" before every standard element

class Student {
  string neptun;
protected:
  string name;
  unsigned enrolledHours;
public:
  Student(const string& name = "John Doe", unsigned enrolledHours = 0) :
    name(name), enrolledHours(enrolledHours) {
    neptun = name + " " + to_string(enrolledHours);
  }

  const string & getName() const { return name; }
  const string & getNeptun() const { return neptun; }
  unsigned getEnrolledHours() const { return enrolledHours; }

  virtual void attendClass() const {
    cout << "Student attending class: " << neptun << endl;
  }

};

class PhDStudent : public Student {
  unsigned taughtHours;
public:
  PhDStudent(const string name, unsigned enrolledHours, unsigned taughtHours) :
    Student(name, enrolledHours),
    taughtHours(taughtHours) {}

  unsigned getTaughtHours() const { return taughtHours; }

  void teachClass() const {
    cout << "PhD student is teaching a class" << endl;
  }

  virtual void attendClass() const {
    cout << "Now the PhD student went to a lecture" << endl;
    Student::attendClass();
  }

};

/*void attendLecture(Student student) {
  student.attendClass();
}*/ 
// Still results in a static type call, because this version uses pass-by-value

void attendLecture(Student& student) {
  student.attendClass();
} 
// For PhDStudent objects, the method defined in PhDStudent will be called (dynamic dispatch)

int main() {
  PhDStudent phd("Meme Joe", 5, 3);
  Student student("simple", 6);
  attendLecture(phd);
  attendLecture(student);
}

Output

Now the PhD student went to a lecture
Student attending class: Meme Joe 5
Student attending class: simple 6

When parameters are passed by reference, polymorphic calls are realized for methods marked with the virtual modifier.
However, when parameters are passed by value, the call remains bound to the static type, since the copied object “loses” the additional properties required for the overridden behavior.

Therefore, if we want to handle types polymorphically, we must do the following:

  • Use the virtual keyword for the desired methods
  • Use references (or pointers) for parameter passing

Override

Because of the strict requirements of virtual overriding, even small typos can lead to not properly overriding the intended method.
To prevent this, we can mark the overriding method with the override specifier, which makes the compiler verify that we are indeed overriding the correct method prototype.
If we mark a method with this specifier but no corresponding virtual method exists in any base class in the inheritance chain, we will get a compilation error.

In the example, the orakatHallgat method of the PhD_hallgato class can be written most safely as follows (the virtual keyword can be omitted, but the override specifier indicates that there must be a corresponding virtual method in the base class):

1
2
3
4
void attendClass() const {
  cout << "Now the PhD student went to a lecture" << endl;
  Student::attendClass();
}

Pure Virtual Methods

In the previous sections, we explored the essence of virtualization. Now, we will extend this concept with the notion of pure virtual methods.
As we have seen, the behavior of virtualized methods can be polymorphically overridden. In all our earlier examples, the method was also implemented in the base class, and the derived class simply modified that behavior.

However, there are many cases where the desired behavior in the base class cannot be clearly defined—or not defined at all. In such cases, it makes little sense to implement those methods in the base class. Likewise, it would make little sense to instantiate the base class itself, because only the more specific derived classes will have the necessary data and behavior to provide a meaningful implementation.

We have already encountered this concept in Java, where such methods and classes were called abstract methods and abstract classes. Abstract classes cannot be instantiated. While Java has a dedicated abstract keyword for this purpose, in C++ a class becomes abstract if it contains at least one pure virtual method. This also means that such a class cannot be instantiated.

A pure virtual method is a method without an implementation in the class (it doesn’t just have an empty body—there is no definition at all). If we only declared such a method without indicating it is pure, we would get a compilation error because a declaration without a definition is incomplete.
To mark a method as pure virtual, we assign it a value of 0. For example:

1
2
3
4
5
6
7
class UniversityCitizen {
    std::string name;
public:
    UniversityCitizen(const std::string& name) : name(name) {}   // There is only one constructor, must be called in derived classes

    virtual void performDuties() const = 0; // pure virtual
};

As seen, the performDuties method is virtual, but it lacks an implementation — this is indicated by writing = 0 instead of a function body. In this case, every class lower in the inheritance chain (direct descendants) must implement this method. If a derived class does not implement it, that class also becomes an abstract class.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
class Student : public UniversityMember {
  string neptun;
public:
  // A student is a UniversityMember, so its base part must also be initialized
  Student(const string& name, const string& neptun) : UniversityMember(name), neptun(neptun) {}

  // The student can be instantiated, so it must implement the pure virtual method!
  virtual void performDuties() const override {
    cout << "The student is attending a class." << endl;
  }
};

class Employee : public UniversityMember {
public:
  Employee(const std::string& name) : UniversityMember(name) {}

  virtual void attendMeeting() const {
    cout << "The employee went to a meeting." << endl;
  }
};

As you can see, Student is a university member and a type that can be instantiated, since it implements the pure virtual method.

In contrast, Employee is still a general entity (non-instantiable), as it could represent a research associate, someone working in administration, or many other roles. All employees attend meetings, but they may participate in different ways (polymorphic call → virtual method).
Since Employee is also abstract, it must have a pure virtual method — this is the inherited but NOT yet implemented performDuties pure virtual method.

1
2
3
4
5
6
7
class Economist : public Employee {
  public:
    Economist(const string& name) : Employee(name) {}
    void performDuties() const override {
        cout << "The economist handles financial matters — that's their duty." << endl;
    }
};

The Economist is a type we want to instantiate, so it must implement all pure virtual methods — in this case, the performDuties method.
Since only Economist and Student are instantiable types, objects can be created only from them, but all other types can still appear as base types or as parameters.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
void universityCitizen(UniversityCitizen& uc) {
    uc.performDuties();
}

void employee(Employee& e) {
    e.performDuties();
    e.attendMeeting();
}

int main() {

  Economist economist("An economist");

  // Employee economist("Economist name"); // We cannot do this, since it would create an Employee instance
  Employee* economist_as_employee = new Economist("Economist who is an employee");
  // This is allowed, since the pointer points to an Economist, which is an Employee.

  UniversityCitizen* economist_as_university_citizen = new Economist("Economist who is a university citizen");

  Student student("A student", "ha57z1");
  universityCitizen(economist);
  universityCitizen(*economist_as_employee);
  universityCitizen(*economist_as_university_citizen);
  universityCitizen(student);

  employee(economist);
  employee(*economist_as_employee);
  // employee(*economist_as_university_citizen); // ERROR!

  // We cannot have an Employee directly, but with a reference we guarantee that a derived instance is passed.
  Employee& economist_as_employee_ref = economist;
  employee(economist_as_employee_ref);
}

As we can see, the Economist and Student types can be instantiated, while the other types can only be used as parameter or variable types.
We can create a variable of type Employee polymorphically, but we must ensure that an actual Economist object (for example) is assigned to it.
This can be achieved by using a pointer or a reference.

It is very important to note that an Economist created as a UniversityCitizen cannot be passed as an Employee,
since the system only knows that it’s dealing with a UniversityCitizen type (or one of its derived types).
The runtime type and its specific behavior are no longer known in that context.

Error Handling

During program execution, various types of errors may occur — some of which we can prepare our program to handle.
These may include system-related errors (e.g., running out of memory, missing permissions to access a resource) or user errors (e.g., entering text instead of a number when calling a conversion function).
Such errors must be handled; otherwise, the program may continue running with undefined behavior.

As an example, let’s look at a simple case of error handling!
Suppose we have a string that we want to convert into a number.
If the conversion fails, the program’s execution stops, and an exception is thrown.
This exception can be of any data type, which we must catch and handle appropriately.

Such exceptions can be handled using a trycatch pair.
Every error that occurs within the try block can be handled by one or more corresponding catch blocks.
For example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#include <iostream>
#include <string>
using namespace std;

int main() {
  try {
    int a = stoi("not an integer");
    /*
      Code using 'a' that runs if everything goes well.
      If an error occurs, this part will not be executed.
    */
  }
  catch (const invalid_argument& error) {
    cerr << "Error during conversion: " << error.what() << endl;
  }
}

Output

Error during conversion: stoi

It can be seen that in the try block we attempt to execute a task, but an error occurs. In the catch block, however, several details should be highlighted:

  • const parameter: The catch block receives an object, which can be constant. Since we typically do not want to modify the error description ourselves, adding the const keyword ensures we can catch it more reliably.
  • Reference parameter: Error types are often defined by some class. As we’ve seen, using references is useful for polymorphic handling — errors should also be caught by reference for this reason.
  • what method: Built-in exception classes have a specific method called what, which describes the nature of the error.

std::exception

Most of the time, we create separate classes for different types of errors. These are derived from the std::exception class. The purpose of this class is to provide a polymorphic base for error handling, and it defines the virtual what method.

Since types can be used polymorphically, the previous example could have also caught the error using the std::exception type:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#include <iostream>
#include <string>

using namespace std;
int main() {
  try {
    int a = stoi("not an integer");
    /*
      Code using 'a' that runs if everything went well — 
      but won’t execute if an error occurs.
    */
  }
  catch(const exception& error) {
    cerr << "Error during conversion (using exception type): " << error.what() << endl;
  }
}

Output

Error during conversion (using exception type):: stoi

As you can see, we still get the 'stoi' message, because the std::invalid_argument error is thrown — we just handled it polymorphically.

Without using a reference, however, we would only get the std::exception message, because polymorphism does not work with pass-by-value.

INCORRECT USAGE:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#include <iostream>
#include <string>
using namespace std;

int main() {
  try {
    int a = stoi("not an integer");
    /*
      Code using 'a' that would run if everything went well; if an error occurs, this part is skipped.
    */
  }
  // IMPORTANT: The error is caught by value here. INCORRECT USAGE!
  catch(const exception error) {
    cerr << "Error occurred during conversion (using exception type): " << error.what() << endl;
  }
}

Output

Error occurred during conversion (using exception type): std::exception

It is important to note that this is not the best way to catch an error. The compiler will usually give a warning, which is worth paying attention to, as it can help identify, diagnose, and fix the error more accurately. Depending on the compiler, you might see a message like this:

warning: catching polymorphic type ‘const class std::exception’ by value [-Wcatch-value=] catch(const std::exception error) {

Multiple catch blocks

You can write multiple catch conditions for a try block, since different types of errors can occur during a single operation. These conditions are tested in order, and it is important to remember, as we learned earlier, that the first matching catch block will handle the error (not necessarily the most specific one, so if an error of a child type occurs but the catch block for the parent appears earlier in the code, the parent’s handler will always run).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
#include <string>
using namespace std;

int main() {
  try {
    int a = stoi("999999999999999999999999999999999999999999999999999999999999999999999999");
    /*
      Code using 'a' that executes if everything goes well; if an error occurs, this part is skipped.
    */
  }
  catch(const invalid_argument& error) {
    cerr << "Error during conversion (invalid_argument type): " << error.what() << endl;
  }
  catch(const out_of_range& error) {
    cerr << "Error during conversion (second catch block for out_of_range): " << error.what() << endl;
  }
  catch(const exception& error) {
    cerr << "General exception handling: " << error.what() << endl;
  }
}

Ouptut

Error during conversion (second catch block for out_of_range): stoi

It is visible that the program continues execution in the second catch block because the std::invalid_argument exception cannot polymorphically handle the currently thrown std::out_of_range exception. The error then moves to the next catch block, which can handle it, so it is handled there.

In the example, we also prepared to handle other errors, not just the two conversion errors. Since std::exception is the base class for a large group of exceptions, it can indeed handle many types of errors.

For this reason, if we place this catch block in the wrong order, i.e., before all the specific ones, we lose information. If written first, every error would be caught polymorphically by std::exception, so the control would never reach the specific catch blocks.

BAD EXAMPLE:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
#include <string>
using namespace std;

int main() {
  try {
    int a = stoi("999999999999999999999999999999999999999999999999999999999999999999999999");
    /*
      Code with 'a' that runs if everything went well; if an error occurs, it won't run.
    */
  }
  catch(const exception& error) {    // BAD! Catches all errors polymorphically
    cerr << "General exception handling: " << error.what() << endl;
  }
  catch(const invalid_argument& error) {
    cerr << "Error during conversion (exception type): " << error.what() << endl;
  }
  catch(const out_of_range& error) {
    cerr << "Error during conversion (second catch block): " << error.what() << endl;
  }
}

Output

General exception handling: stoi

Of course, the compiler also warns us about this:

warning: by earlier handler for ‘std::exception’ catch(const exception& error) { // BAD! Catches all errors polymorphically

Custom Error Type

We can also define our own error type and make it similarly general to the errors we have seen so far, by deriving it directly or indirectly from the std::exception class:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include <iostream>
#include <exception>
using namespace std;

// Simple custom error class
class SimpleCustomError : public exception {
  const char* what() const noexcept override {
    return "This is a simple custom error!";
  }
};

// More advanced custom error class
class CustomError : public std::exception {
  string message;
  int bad_number;
public:
  CustomError(const string& message, int bad_number = 0) : message(message), bad_number(bad_number) {}

  const char* what() const noexcept override {
    return message.c_str();
  }

  int getBadNumber() const {
    return bad_number;
  }
};

// Function to print the error
void printError(const exception& e) {
  cerr << e.what() << endl;
}

// Function to perform division with error checking
int divide(int first, int second) {
  if (second == 0) {
    throw CustomError("Division by zero", second);
  }
  return first / second;
}

int main() {
  SimpleCustomError simpleError;
  printError(simpleError);

  CustomError myError("This is my custom error. Ready to use");
  printError(myError);

  try {
    divide(342, -34);
  } catch (CustomError& exception) {
    cerr << exception.what() << endl;
    cerr << "The error was caused by the following number: " << exception.getBadNumber() << endl;
  }
}

Output

This is a simple custom error!
This is my custom error. Ready to use
Division by zero
The error was caused by the following number: -34

The SimpleCustomError type only overrides the built-in what method so that the message displayed when the error is printed is the one we specify (this could also be done by instantiating an exception, but this way we have our own custom error type and can write a custom error handler for our defined type). Since CustomError can also be used as a general std::exception, its usage is all that remains. It's not enough to just construct it; it must also be thrown. In addition to what we’ve seen so far, error objects are just like any other object—they can have member variables, different constructors, and methods (as we saw with the getters).

noexcept keyword

When overriding the what method, we had to specify the noexcept property. This indicates that the method is guaranteed not to throw an exception during its execution.

throw keyword

If a program encounters an error during execution, the programmer must ensure that an exception is thrown. This can be done using the throw keyword. You must specify what you want to throw. This can be, for example, an instance of CustomError.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iostream>
#include <exception>
#include <string>

class CustomError : public std::exception {
  std::string message;
public:
  CustomError(const std::string& message) : message(message) {}

  const char* what() const noexcept override {
    return message.c_str();
  }
};

void foo() {
  // many operations

  // error occurs:
  throw CustomError("Something went wrong during the execution of foo.");
}

int main() {
  try {
    foo();
  }
  catch(const CustomError& ce) {
    std::cerr << ce.what() << std::endl;
  }
}

Output

Something went wrong during the execution of foo.

Exception Handling Continued...

Non-class type errors

At the beginning of the error handling discussion, it was mentioned that errors are usually class types; however, in C++ this is not strictly required (in Java it had to be this way, and it also mattered which class the exception object derived from). In C++, anything can be thrown—any value; be it const char*, std::string, int, etc. Of course, these can also be caught by reference, but for primitive types this is not necessary.

What is important, however, is that if the type of the thrown exception does not derive from std::exception, then naturally it cannot be caught in a branch expecting an std::exception.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
int main() {
  try {
    throw 42; // Throwing an int
  }
  catch(const std::exception& e) { // Will not execute, even though it's first!
    std::cerr << "The error: " << e.what() << std::endl;
  }
  catch(int number) { // Catching the int
    std::cerr << "The error: " << number << std::endl;
  }

  try {
    throw "This is a const char* literal"; // Throwing a const char* literal
  }
  catch(const char* errorMessage) { // Catching the const char* exception
    std::cerr << errorMessage << std::endl;
  }
}

Output

The error: 42
This is a const char* literal

Catching All Exceptions

Since some errors do not fall under std::exception, they cannot be handled generally. However, if we want to ensure that we catch every error, we would need to write a catch block for every possible error type. This is not always feasible (either because there are many different types of errors we want to handle uniformly, or because we do not know exactly which errors the program should be prepared for). However, we can handle this by using ... (three dots) in the catch clause. This allows us to catch anything in the catch block.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
int main() {
  try {
    throw 42;  // Throw an integer error
  }
  catch(const std::exception& e) {    // Will not run
    std::cerr << e.what() << std::endl;
  }
  catch(...) {  // Catch any type of exception
    std::cerr << "Unknown error 1" << std::endl;
  }

  try {
    throw "Error message";  // Throw a const char* error
  }
  catch(const std::exception& e) {    // Will not run
    std::cerr << e.what() << std::endl;
  }
  catch(...) {  // Catch any type of exception
    std::cerr << "Unknown error 2" << std::endl;
  }
}

Output

Unknown error 1
Unknown error 2

Final classes and methods

In Java, it is possible to use the final keyword to ensure that a method cannot be overridden in a derived class, or to indicate that a class cannot be inherited from.

The C++11 standard also provides this possibility in C++, although in C++ final is not a keyword by itself. This means that in the appropriate context (after a method or class name) it serves the same purpose as in Java; in all other cases, it is just treated as a normal identifier.

Multiple inheritance

In C++, it is possible for a class to inherit from multiple other classes. The main "issue" in this case is handling name conflicts, i.e., when the class inherits elements with the same signature from multiple sources.
If the doubly inherited members are not virtual, we can simply refer to them in the derived class using their scoped names, whether they are data members or methods.

For virtual methods, the virtual table is inherited from the base classes. If we do not want to call a method directly on the derived type but only through a base class, it is clear which table should be used for resolving the call.
However, if we want to call a doubly inherited method through an object of a multiply derived class, this is only possible if the method has been overridden in the derived class.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#include <iostream>

class ClassA {
public:
  int value = 5;

  void nonVirtualMethod() {
    std::cout << "ClassA non-virtual method - value: " << value << std::endl;
  }

  virtual void virtualMethod() const {
    std::cout << "ClassA virtual method - value: " << value << std::endl;
  }
};

class ClassB {
public:
  int value;

  void nonVirtualMethod() {
    std::cout << "ClassB non-virtual method - value: " << value << std::endl;
  }

  virtual void virtualMethod() const {
    std::cout << "ClassB virtual method - value: " << value << std::endl;
  }
};

class ClassC : public ClassA, public ClassB {
public:
  void performActions() {
    // scoped references to non-virtual members
    ClassB::value = 1;
    ClassA::nonVirtualMethod();
    ClassB::nonVirtualMethod();
  }

  void virtualMethod() const override {
    std::cout << "ClassC virtual method - ClassA::value: " 
              << ClassA::value << "; ClassB::value: " << ClassB::value << std::endl;
    // inherited members with the same name can be accessed via scoped name
  }
};

void polymorphicCallA(const ClassA& a) {
  a.virtualMethod(); 
}

void polymorphicCallB(const ClassB& b) {
  b.virtualMethod();
}

int main() {
  ClassC c;
  c.performActions();
  c.virtualMethod(); // compilation error if virtualMethod is not overridden in ClassC
  polymorphicCallA(c);
  polymorphicCallB(c);
}

Output

ClassA non-virtual method - value: 5
ClassB non-virtual method - value: 1
ClassC virtual method - ClassA::value: 5; ClassB::value: 1
ClassC virtual method - ClassA::value: 5; ClassB::value: 1
ClassC virtual method - ClassA::value: 5; ClassB::value: 1


Last update: 2025-11-27
Created: 2025-11-27