Skip to the content of the web site.

Throwing and catching exceptions and inheritance

We are now going to introduce you two two topics: exceptions and inheritance.

To begin, up to this point, we have had only two choices:

  • either the code ran correctly, or
  • assertions could be used to flag explicit errors in our programming.

An assertion terminates the execution of the program, and there is no means of preventing such a termination.

Exceptions

Exceptions are very similar: every exception has the same form of constructor: it takes an argument that is a string that gives some information about what is wrong.

Because these are classes, you can declare and initialize a local variable as an instance of these classes:

#include <iostream>
#include <stdexcept>

// Function declarations
int main();

// Function definitions
int main() {
    std::runtime_error issue{ "This is the message" };

    std::cout << issue.what() << std::endl;

    return 0;
}

Throwing exceptions

Now, it is possible to throw such an exception, just like you can raise an exception in Python.

def my_sin( x ):
	if (x < 0) or (x > 1.57079632679489662):
		raise Exception( "The argument must be in [0, pi/2], but got {}".format( x ) );
	
	return ((-0.110739816361840741*x - 0.0573853410271094288)*x + 1.0)*x

In C++, this would be considered a domain error: the function expects an argument in the domain $\left[0, \frac{\pi}{2}\right]$, but if it gets an argument outside this interval, we have an issue.

#include <iostream>
#include <cmath>
#include <stdexcept>

// Function declarations
int main();
double my_sin( double x );

// Function definitions
int main() {
    std::cout << my_sin( 1.5 ) << std::endl;
    std::cout << my_sin( 2.5 ) << std::endl;

    return 0;
}

double my_sin( double x ) {
    if ( (x < 0) || (x > M_PI_2) ) {
        throw std::domain_error( "The argument 'x' must be in [0, pi/2], but got " + std::to_string( x ) );
    }

    return ((
        -0.110739816361840741*x - 0.0573853410271094288
    )*x + 1.0)*x;
}

You can try this out at repl.it; however, the output from my version of C++ is:

0.997136
terminate called after throwing an instance of 'std::domain_error'
  what():  The argument 'x' must be in [0, pi/2], but got 2.500000
Aborted (core dumped)

Thus, before the program terminated, the executable actually called the function what() and printed the output to the console.

Catching exceptions

To understand why exceptions are useful, suppose you have two systems that are communicating with each other. This requires that a connection be made, and then other functions are called to start the communication process. With hardware, there is always the possibility that the connection is lost, and that could happen at any time. In this case, as soon as the connection is lost, a function could throw a std::runtime_error{ "Connection lost" };. The top-level function that first established the connection is likely the only function that is able to also re-establish that connection,

void get_image_data( vector<int> &data_array, int socket_fd ) {
    uint8_t buffer[1024];
    bool finished{false};

    do {
        int result{ read( socket_fd, buffer, 1024 };

        if ( result < 0 ) {
            throw runtime_error{ "Connection lsot" };
        }

        // Continue reading and sending data to the new socket,
        // and put any data into the argument 'data_array'
    } while ( !finished );
}

The reason for throwing this exception is that this function does not, and should not, be programmed to determine what is wrong and to correct that issue. Instead, this function will simply throw an exception. Additionally, even the function that called this function, or the function that called that function may not know how to correct the issue.

Instead, the top-level function that first established the socket may have the requisite information to reestablish the socket, or to otherwise terminate the program. It can do so by catching the exception that was thrown:

int main() {
    bool finished{ false };
    unsigned short attempts{0};

    do {
        // Declare, bind and establish a socket

        try {
            // Communicate with that socket
            //  - This may call one or more functions,
            //    each of which may call multiple other functions,
            //    any of which could determine that there was an
            //    issue with the socket connection and throw a
	    //    std::runtime_error{...}

            finished = true;
        } catch ( std::runtime_error &exception ) {
            // Print the error message to the console log
            std::clog << exception.what() << std::endl;
            ++attempts;
        }
    } while( !finished && (attempts < 10) );

    if ( attempts == 10 ) {
        return EXIT_FAILURE;
    } else 
        // Do something with the data...

        return EXIT_SUCCESS;
    }
}

While you may not know exactly how sockets are established, you can see that at any time, that any function that is called inside the try block throws an exception, the execution of that function terminates immediately, and the exception is passed back until it is caught. Thus, if any exception is thrown, the printing to std::clog becomes the next command that is executed, and the exception that was thrown is assigned to the variable exception. The scope of that variable up until the end of the exception block.

Only if all the communication successfully completed with no exceptions thrown is the last statement in the try-block executed; that is, finished is assigned true, after which the do-while loop can finish.

If an exception is not caught, it terminates the program as shown above.

Now, in the standard library, the std::exception class is declared and defined. There are an entire collection of additional exceptions defined in the stdexcept library, and you can view all of them at cplusplus.com; however, there is a link between these called inheritance.

Inheritance

Suppose you've designed a class that has a number of member variables and member functions. For example, suppose you've created a Massive_object class as previously described. Under normal circumstances, one can assume that such objects are electrically neutral, and therefore the electromagnetic force need not be considered when calculating, for example, the accelleration caused by the forces between the two objects.

The first option is to create a new class. You cut-and-paste all of the code from the Massive_particle class, and then make changes to it to accomodate the electric charge. Most member functions, however, you will note, do not have to change; however, some, such as acceleration_toward(...) do need to change in order to account for this new force. This is a horrible idea. The most important reason for why it is so horrible is that if you make any changes to one class (add a new feature, fix a bug, etc.), you must explicitly to add that feature or fix that bug to the other class. This is a recipe for disaster, as it sets up a situation that is unmaintainable.

You could include an electric charge to your Massive_particle class, and simply set the net charge to zero; most users don't need this, and it takes up extra memory and extra calculations, so someone wanting to use this class to simply model massive particles may get frustrated at the additional memory and time costs.

Instead, inheritance allows the following: If you state that a new class is derived from an existing class:

  • the new class immediately gets all the member variables and member functions of the original class, but
  • in addition to this, the new class can add additional member variables and additional member functions not existing in the original class, and
  • specific functions can be rewritten specifically for this derived class.

Because the derived class is based on the member functions and variables of another class, we call that other class the base class.

Here is how inheritance works:

  • If a member function is defined in the base class, but not in the derived class, then when you call that member function on the derived class, the function defined in the base class is called.
  • If a member function is defined only in the derived class, then when you call that member function on the derived class, it calls explicitly that member function.
  • If a member function defined in both the base and the derived class, the member function defined in the derived class is called, but it can also call the member function of the base class to gain information.

You can see this in the following class:

o
#include <string>

// Class declarations
class A;
class B;
  ////////////////////////
 // Class A definition //
////////////////////////
class A {
  public:
    A( std::string a );
    std::string base_only() const;
    std::string both_base_and_derived() const;

  private:
    std::string a_;
};

// Class A member function definitions
A::A( std::string a ):
a_{a} {
  // Empty constructor
}

std::string A::base_only() const {
  return "In base A only: " + a_;
}

std::string A::both_base_and_derived() const {
  return "In A: " + a_;
}

  ////////////////////////
 // Class B definition //
////////////////////////

class B : public A {
  public:
    B( std::string a, std::string b );
    std::string both_base_and_derived() const;
    std::string derived_only() const;

  protected:
    std::string b_;
};

// Class A member function definitions
B::B( std::string a, std::string b ):
A{a},
b_{b} {
  // Empty constructor
}

std::string B::both_base_and_derived() const {
  return "In B (" + b_ + ") calling base class: "
        + A::both_base_and_derived();
}

std::string B::derived_only() const {
  return "Only declared in B: " + b_;
}

You can view this in repl.it.

Using inheritance

Suppose you want to throw a run-time exception, but you would like to include more data in the class, rather than just the string. In this case, you could create a derived class

#include <string>
#include <stdexcept>

class real_domain_error : public std::domain_error {
    public:
        real_domain_error( std::string )

        double lower() const;
        double upper() const;
        double value() const;

    private:
        double lower_;
        double upper_;
        double value_;
};

double real_domain_error::lower() const {
    return lower_;
}

double real_domain_error::upper() const {
    return upper_;
}

double real_domain_error::value() const {
    return value_;
}

std::string real_domain_error::what() {
    if ( value_ < lower_ ) {
        return domain_error::what() + ": "
             + to_string( value_ ) + " < [" 
             + to_string( lower_ ) + ", "
             + to_string( upper ) + "]";
    } else {
        return domain_error::what() + ": ["
             + to_string( lower_ ) + ", "
             + to_string( upper ) + "] > ";
             + to_string( value_ );
    }
}

Now, it is possible to throw and catch such an error: and when it is received, you can catch it either as:

  • a real_domain_error, in which case, you could call its associate member functions, or

  • either a std::domain_error or std::exception error, although, in these cases, you would only be able to access the what() member function.