CS253: Software Development with C++

Spring 2020

Exceptions

Show Lecture.Exceptions as a slide show.

CS253 Exceptions

Loud

These example use my non-standard Loud class.

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";
    cout << "power\n";  // πŸ’€πŸ’€πŸ’€
}

int main() {
    cout << "reality\n";
    try {
        cout << "soul\n";
        snap();
        cout << "space\n";  // πŸ’€πŸ’€πŸ’€
    }
    catch (const char *who) {
        cout << "Caught " << who << '\n';
    }
    cout << "time\n";
}
reality
soul
mind
Caught Thanos
time

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

I don’t believe that this behavior is mandated by the standard, but 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 β”‚
        	    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

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!

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 "Son of a gun!";
}

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?

catch #2ΒΎ

try {
    throw "That’s better."s;
}

catch (string s) {
    cout << "Got a string: " << s << "\n";
}
Got a string: That’s better.

Hey, that worked!

catch #2β…ž

try {
    throw "Even better."s;
}

catch (const string &s) {
    cout << "Got a string: " << s << "\n";
}
Got a string: Even better.

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:4: 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!