Show Lecture.Exceptions as a slide show.
CS253 Exceptions
Overhead
To use the standard exceptions objects, you need to:
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%
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']
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:
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!
Yes, literally, catch (...)
. Dot dot dot.
There’s no variable, so the code can’t tell what was thrown.
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 "🐷 🐷 🐷 🐷 🐷 🐷";
}
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.
try {
throw "🐵 🐵 🐵 🐵 🐵 🐵"s;
}
catch (string s) {
cout << "Better: " << s << "\n";
}
Better: 🐵 🐵 🐵 🐵 🐵 🐵
Hey, that worked!
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.
// 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:2: warning: catching polymorphic type ‘class std::exception’ by value
caught: std::exception
caught: 🍩🍩🍩
- The base class exception is fast, slim & trim, and has no
space for a message. Its virtual
.what()
method returns a fixed
string.
- Also, catching an object by value is slow, because the object gets
copied, same as passing an object to a function as an argument by value.
// 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
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!