CS253: Software Development with C++

Fall 2021

Exceptions

Show Lecture.Exceptions as a slide show.

CS253 Exceptions

Loud

These example use my non-standard Loud class.

// A “Loud” class.  It announces whenever its methods are called.
#ifndef LOUD_H_INCLUDED
#define LOUD_H_INCLUDED

#include <iostream>

class Loud {
    char c;
    void hi(const char *s) const {
        std::cout << "Loud::" << s;
        if (c) std::cout << " [c='" << c << "']";
        std::cout << std::endl;  // flush debug output
    }
  public:
    Loud(char ch = '\0') : c(ch) { hi("Loud()"); }
    ~Loud() { hi("~Loud()"); }
    Loud(const Loud &l) : c(l.c) { hi("Loud(const Loud &)"); }
    Loud(Loud &&l) : c(l.c) { hi("Loud(Loud &&)"); }
    Loud& operator=(const Loud &l) { c=l.c; hi("operator=(const Loud &)"); return *this; }
    Loud& operator=(Loud &&l) { c=l.c; hi("operator=(Loud &&)"); return *this; }
    Loud& operator=(char ch) { c = ch; hi("operator=(char)"); return *this; }
    Loud& operator++() { ++c; hi("operator++()"); return *this; }
    Loud operator++(int) { hi("operator++(int)"); const auto save = *this; ++*this; return save; }
    Loud operator+(const Loud &l) const { hi("operator+(const Loud &)"); return Loud(c+l.c); }
};

#endif /* LOUD_H_INCLUDED */

Basic Syntax

try { … }
Execute some code, which might throw an exception.
throw value;
Throw an exception. At that point, program execution transfers to any applicable catch clause. If no catch clause applies, then the program dies.
catch (specification) { … }
Deal with an exception.
specification is very much like a function argument definition.

Normal Operation

void snap() {
    cout << "mind\n";
    throw "Thanos";
    throw "time";       // 💀💀💀
    cout << "space\n";  // 💀💀💀
}

int main() {
    cout << "reality\n";
    try {
        cout << "soul\n";
        snap();
        cout << "gauntlet\n";  // 💀💀💀
    }
    catch (const char *who) {
        cout << "Caught " << who << '\n';
    }
    cout << "50%\n";
}
reality
soul
mind
Caught Thanos
50%

throw without catch

If something is thrown but never caught, then the special function terminate() is called, which complains in an implementation-defined manner, and stops the program by calling abort().

throw "ouch";
terminate called after throwing an instance of 'char const*'
SIGABRT: Aborted

A Special Case

For g++, an uncaught standard exception (derived from std::exception) gets its .what() string displayed.

throw overflow_error("Politician IQ underflow!");
terminate called after throwing an instance of 'std::overflow_error'
  what():  Politician IQ underflow!
SIGABRT: Aborted

This behavior is a GNU extension, so not mandated by the standard, and it sure makes it easier to debug an uncaught exception.

Objects get destroyed appropriately

Loud a('a');

void foo() {
    Loud b('b');
    Loud c('c');
}

int main() {
    Loud d('d');
    foo();
    Loud e('e');
    return 0;
}
Loud::Loud() [c='a']
Loud::Loud() [c='d']
Loud::Loud() [c='b']
Loud::Loud() [c='c']
Loud::~Loud() [c='c']
Loud::~Loud() [c='b']
Loud::Loud() [c='e']
Loud::~Loud() [c='e']
Loud::~Loud() [c='d']
Loud::~Loud() [c='a']

throw without catch

Loud a('a');

void foo() {
    Loud b('b');
    throw 42;
    Loud c('c');
}

int main() {
    Loud d('d');
    foo();
    Loud e('e');
    return 0;
}
Loud::Loud() [c='a']
Loud::Loud() [c='d']
Loud::Loud() [c='b']
terminate called after throwing an instance of 'int'
SIGABRT: Aborted

You can throw anything—doesn’t have to be a special type or object.

I mean anything

Really—you can throw and catch any type. For example:

throw 42;
terminate called after throwing an instance of 'int'
SIGABRT: Aborted
throw "alpha";
terminate called after throwing an instance of 'char const*'
SIGABRT: Aborted
throw "beta"s;
terminate called after throwing an instance of 'std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >'
SIGABRT: Aborted

I mean anything

int n=42; throw "Value no good: " + to_string(n);
terminate called after throwing an instance of 'std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >'
SIGABRT: Aborted
throw logic_error("That failed miserably");
terminate called after throwing an instance of 'std::logic_error'
  what():  That failed miserably
SIGABRT: Aborted

It’s nice that the what() value for a std::exception gets displayed.

Standard Exceptions

There are a number of classes defined by the C++ standard, in <stdexcept>. It’s best to use them, or classes derived from them, as opposed to rolling your own.

Standard Exception Class Hierarchy

        	      ┌─────────────────────────┐
        	      │	       exception	│
        	      └─────────────────────────┘
        		△		      △
            ┌───────────┴──────────┐	  ┌───┴───────────────┐
            │	   logic_error	   │	  │   runtime_error   │
            └──────────────────────┘	  └───────────────────┘
             △	     △	 △   △	 △	   △	    △	△   △
    ┌────────┴─────┐ │	 │   │	 │ ┌───────┴──────┐ │	│   │
    │ domain_error │ │	 │   │	 │ │ range_error  │ │	│   │
    └──────────────┘ │	 │   │	 │ └──────────────┘ │	│   │
    ┌────────────────┴─┐ │   │	 │   ┌──────────────┴─┐ │   │
    │ invalid_argument │ │   │	 │   │ overflow_error │ │   │
    └──────────────────┘ │   │	 │   └────────────────┘ │   │
            ┌────────────┴─┐ │	 │	┌───────────────┴─┐ │
            │ length_error │ │	 │	│ underflow_error │ │
            └──────────────┘ │	 │	└─────────────────┘ │
        	┌────────────┴─┐ │	       ┌────────────┴─┐
        	│ out_of_range │ │	       │ system_error │
        	└──────────────┘ │	       └──────────────┘
        	    ┌────────────┴─┐
        	    │ future_error │
        	    └──────────────┘

Of, if you like a diagram:

try #1

Loud a('a');

void foo() {
    Loud b('b');
    throw "oops!";
    Loud c('c');
}

int main() {
    Loud d('d');
    try {
        foo();
    }
    catch (const char *error) {
        cout << "Caught: " << error << "\n";
    }

    Loud e('e');
    return 0;
}
Loud::Loud() [c='a']
Loud::Loud() [c='d']
Loud::Loud() [c='b']
Loud::~Loud() [c='b']
Caught: oops!
Loud::Loud() [c='e']
Loud::~Loud() [c='e']
Loud::~Loud() [c='d']
Loud::~Loud() [c='a']

try #2

Loud a('a');

void foo() {
    Loud b('b');
    throw "oops!";
    Loud c('c');
}

void bar() {
    Loud d('d');
    foo();
}

int main() {
    Loud e('e');
    try {
        bar();
    }
    catch (const char *error) {
        cout << "Caught: “" << error << "”\n";
    }
    Loud f('f');
}
Loud::Loud() [c='a']
Loud::Loud() [c='e']
Loud::Loud() [c='d']
Loud::Loud() [c='b']
Loud::~Loud() [c='b']
Loud::~Loud() [c='d']
Caught: “oops!”
Loud::Loud() [c='f']
Loud::~Loud() [c='f']
Loud::~Loud() [c='e']
Loud::~Loud() [c='a']

catch #1

try {
    throw "oops!";
}

catch (int i) {
    cout << "int " << i << "\n";
}

catch (const char *error) {
    cout << "C string: " << error << "\n";
}

catch (...) {  // Gotta catch ’em all!
    cout << "something\n";
}
C string: oops!

Yes, literally, catch (...). Dot dot dot. There’s no variable, so the code can’t tell what was thrown.

Multiple catches

try { }
catch (...) { }
catch (int) { }
c.cc:2: error: '...' handler must be the last handler for its try block

catch #2

try {
    throw 42;
}

catch (short s) {
    cout << "Got a short: " << s << "\n";
}

catch (long l) {
    cout << "Got a long: " << l << "\n";
}
terminate called after throwing an instance of 'int'
SIGABRT: Aborted

The type must match. No conversions! (almost)

catch #2½

try {
    throw "🐷 🐷 🐷 🐷 🐷 🐷";
}

catch (string s) {
    cout << "Got a string: " << s << "\n";
}
terminate called after throwing an instance of 'char const*'
SIGABRT: Aborted
Why didn’t that work?

"🐷 🐷 🐷 🐷 🐷 🐷" is a const char [], or const char *, and we’re catching a string. Those are different types.

catch #2¾

try {
    throw "🐵 🐵 🐵 🐵 🐵 🐵"s;
}

catch (string s) {
    cout << "Better: " << s << "\n";
}
Better: 🐵 🐵 🐵 🐵 🐵 🐵

Hey, that worked!

catch #2⅞

try {
    throw "🐼 🐼 🐼 🐼 🐼 🐼"s;
}

catch (const string &s) {
    cout << "Even betterer: " << s << "\n";
}
Even betterer: 🐼 🐼 🐼 🐼 🐼 🐼

This works, and is more efficient. Of course, efficiency may not be an issue here.

catch #3

// logic_error is-a exception
// runtime_error is-a exception
// overflow_error is-a runtime_error

try {
    throw overflow_error("Just too big!");
}
catch (const logic_error &e) {
    cout << "logic_error: " << e.what() << '\n';
}
catch (const runtime_error &e) {
    cout << "runtime_error: " << e.what() << '\n';
}
catch (const exception &e) {
    cout << "exception: " << e.what() << '\n';
}
runtime_error: Just too big!

The types must match, except that is-a is good enough.

catch #3½

Avoid catching a polymorphic type by value, because of object slicing:

try { throw overflow_error("🍕🍕🍕"); }
catch (exception e) { cout << "caught: " << e.what() << '\n'; }

try { throw overflow_error("🍩🍩🍩"); }
catch (const exception &f) { cout << "caught: " << f.what() << '\n'; }
c.cc:2: warning: catching polymorphic type 'class std::exception' by value
caught: std::exception
caught: 🍩🍩🍩

catch #4

// logic_error is-a exception
// runtime_error is-a exception
// overflow_error is-a runtime_error

try {
    throw overflow_error("Just too big!");
}
catch (const logic_error &e) {
    cout << "logic_error: " << e.what() << '\n';
}
catch (const exception &e) {
    cout << "exception: " << e.what() << '\n';
}
exception: Just too big!

No conversions, but is-a is good enough.

re-throw

void foo() {
    throw "Division by zero"s;
}

void bar() {
    try {
        foo();
    }
    catch (string msg) {
        if (msg == "Out of memory")  // I’ve got this!
            /* get more memory */;
        else
            throw;      // Throw up hands in despair.
    }
}

int main() {
    try {
        bar();
    }
    catch (string problem) {
        cout << "Problem in bar: " << problem << '\n';
    }

    return 0;
}
Problem in bar: Division by zero

Not a panacea

Exceptions are not a panacea, because not all problems cause exceptions.

The vector::at() and string::at() methods are defined to throw a specific exception, out_of_range:

vector<int> v = {11,22,33};
cout << v.at(1000000);
terminate called after throwing an instance of 'std::out_of_range'
  what():  vector::_M_range_check: __n (which is 1000000) >= this->size() (which is 3)
SIGABRT: Aborted

Catching

The exception thrown by vector::at() can be caught:

vector<int> v = {11,22,33};
try {
    cout << v.at(1000000);
}
catch (const exception &e) {
    cerr << "OOPS!  " << e.what() << '\n';
}
OOPS!  vector::_M_range_check: __n (which is 1000000) >= this->size() (which is 3)

Uncatchable

However, the problems caused by vector::operator[] can’t be caught, because they are not defined to throw exceptions:

vector<int> v = {11,22,33};
cout << v[1000000];
SIGSEGV: Segmentation fault

Your choice!