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
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']
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!