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%
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']
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 │
└──────────────┘
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']
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']
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
- Multiple catch clauses are executed in order written.
- Therefore, catch the most specific things first,
and the least specific things last.
catch (...)
is the least specific of all, so it must go last.
- A good compiler may warn of an unreachable catch;
don’t count on it.
try { }
catch (...) { }
catch (int) { }
c.cc:2: error: '...' handler must be the last handler for its try block
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)
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?
try {
throw "That’s better."s;
}
catch (string s) {
cout << "Got a string: " << s << "\n";
}
Got a string: That’s better.
Hey, that worked!
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.
// 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.
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: 🍩🍩🍩
// 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.
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
- vector::at() runs (slightly) slowly, clutters up your code, and
catches your mistakes.
[]
runs quickly, is terse, and doesn’t help you when you screw up.
Your choice!