CS253: Software Development with C++

Spring 2022

Exceptions

Show Lecture.Exceptions as a slide show.

CS253 Exceptions

Overhead

To use the standard exceptions objects, you need to:

#include <stdexcept>

Nomenclature

As discussed in the Pet Peeves? lecture, sloppy people treat the word “throw” as synonymous with “produce an error message”. They speak of the make command “throwing an error”, which is foolish. The make command produces error messages, or produces errors, or errors out, or, simply, complains.

Reserve the word “throw” to refer to exceptions, in the throw/try/catch sense.

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

Note that our implementation displayed the type of the thing thrown, but not its contents.

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!