COMP6771
Advanced C++ Programming
Week 4.1
Exceptions
Let's start with an example
- What does this produce?
#include <iostream>
#include <vector>
auto main() -> int {
std::cout << "Enter -1 to quit\n";
std::vector<int> items{97, 84, 72, 65};
std::cout << "Enter an index: ";
for (int print_index; std::cin >> print_index; ) {
if (print_index == -1) break;
std::cout << items.at(print_index) << '\n';
std::cout << "Enter an index: ";
}
}
Let's start with an example
- What does this produce?
#include <iostream>
#include <vector>
auto main() -> int {
std::cout << "Enter -1 to quit\n";
std::vector<int> items{97, 84, 72, 65};
std::cout << "Enter an index: ";
for (int print_index; std::cin >> print_index; ) {
if (print_index == -1) break;
try {
std::cout << items.at(print_index) << '\n';
items.resize(items.size() + 10);
} catch (const std::out_of_range& e) {
std::cout << "Index out of bounds\n";
} catch (...) {
std::cout << "Something else happened";
}
std::cout << "Enter an index: ";
}
}
Exceptions: What & Why?
-
What:
-
Exceptions: Are for exceptional circumstances
- Happen during run-time anomalies (things not going to plan A!)
-
Exception handling:
- Run-time mechanism
- C++ detects a run-time error and raises an appropriate exception
- Another unrelated part of code catches the exception, handles it, and potentially rethrows it
-
Exceptions: Are for exceptional circumstances
-
Why:
- Allows us to gracefully and programmatically deal with anomalies, as opposed to our program crashing.
What are "Exception Objects"?
- Any type we derive from std::exception
- throw std::out_of_range("Exception!");
- throw std::bad_alloc("Exception!");
- Why std::exception? Why classes?
Standard Exceptions
- #include <stdexcept>
- Your class can inherit from these types
Conceptual Structure
- Exceptions are treated like lvalues
- Limited type conversions exist (pay attention to them):
- nonconst to const
- other conversions we will not cover in the course
try {
// Code that may throw an exception
} catch (/* exception type */) {
// Do something with the exception
} catch (...) { // any exception
// Do something with the exception
}
Multiple catch options
- This does not mean multiple catches will happen, but rather that multiple options are possible for a single catch
#include <iostream>
#include <vector>
auto main() -> int {
auto items = std::vector<int>{};
try {
items.resize(items.max_size() + 1);
} catch (std::bad_alloc& e) {
std::cout << "Out of bounds.\n";
} catch (std::exception&) {
std::cout << "General exception.\n";
}
}
Catching the right way
- Throw by value, catch by const reference
- Ways to catch exceptions:
- By value (no!)
- By pointer (no!)
- By reference (yes)
- References are preferred because:
- more efficient, less copying (exploring today)
- no slicing problem (related to polymorphism, exploring later)
(Extra reading for those interested)
- https://blog.knatten.org/2010/04/02/always-catch-exceptions-by-reference/
Catch by value is inefficient
#include <iostream>
class Giraffe {
public:
Giraffe() { std::cout << "Giraffe constructed" << '\n'; }
Giraffe(const Giraffe &g) { std::cout << "Giraffe copy-constructed" << '\n'; }
~Giraffe() { std::cout << "Giraffe destructed" << '\n'; }
};
void zebra() {
throw Giraffe{};
}
void llama() {
try {
zebra();
} catch (Giraffe g) {
std::cout << "caught in llama; rethrow" << '\n';
throw;
}
}
auto main() -> int {
try {
llama();
} catch (Giraffe g) {
std::cout << "caught in main" << '\n';
}
}
Catch by value inefficiency
#include <iostream>
class Giraffe {
public:
Giraffe() { std::cout << "Giraffe constructed" << '\n'; }
Giraffe(const Giraffe &g) { std::cout << "Giraffe copy-constructed" << '\n'; }
~Giraffe() { std::cout << "Giraffe destructed" << '\n'; }
};
void zebra() {
throw Giraffe{};
}
void llama() {
try {
zebra();
} catch (const Giraffe& g) {
std::cout << "caught in llama; rethrow" << '\n';
throw;
}
}
int main() {
try {
llama();
} catch (const Giraffe& g) {
std::cout << "caught in main" << '\n';
}
}
Rethrow
- When an exception is caught, by default the catch will be the only part of the code to use/action the exception
- What if other catches (lower in the precedence order) want to do something with the thrown exception?
try {
try {
try {
throw T{};
} catch (T& e1) {
std::cout << "Caught\n";
throw;
}
} catch (T& e2) {
std::cout << "Caught too!\n";
throw;
}
} catch (...) {
std::cout << "Caught too!!\n";
}
(Not-advisable) Rethrow, catch by value
#include <iostream>
class Cake {
public:
Cake() : pieces_{8} {}
int getPieces() { return pieces_; }
Cake& operator--() { --pieces_; }
private:
int pieces_;
};
int main() {
try {
try {
try {
throw Cake{};
} catch (Cake& e1) {
--e1;
std::cout << "e1 Pieces: " << e1.getPieces() << " addr: " << &e1 << "\n";
throw;
}
} catch (Cake e2) {
--e2;
std::cout << "e2 Pieces: " << e2.getPieces() << " addr: " << &e2 << "\n";
throw;
}
} catch (Cake& e3) {
--e3;
std::cout << "e3 Pieces: " << e3.getPieces() << " addr: " << &e3 << "\n";
}
}
Exception safety levels
- This part is not specific to C++
- Operations performed have various levels of safety
- No-throw (failure transparency)
- Strong exception safety (commit-or-rollback)
- Weak exception safety (no-leak)
- No exception safety
No-throw guarantee
- Also known as failure transparency
- Operations are guaranteed to succeed, even in exceptional circumstances
- Exceptions may occur, but are handled internally
- No exceptions are visible to the client
- This is the same, for all intents and purposes, as noexcept in C++
- Examples:
- Closing a file
- Freeing memory
- Anything done in constructors or moves (usually)
- Creating a trivial object on the stack (made up of only ints)
Strong exception safety
- Also known as "commit or rollback" semantics
- Operations can fail, but failed operations are guaranteed to have no visible effects
- Probably the most common level of exception safety for types in C++
- All your copy-constructors should generally follow these semantics
- Similar for copy-assignment
- Copy-and-swap idiom (usually) follows these semantics (why?)
- Can be difficult when manually writing copy-assignment
Strong exception safety
- To achieve strong exception safety, you need to:
- First perform any operations that may throw, but don't do anything irreversible
- Then perform any operations that are irreversible, but don't throw
Basic exception safety
- This is known as the no-leak guarantee
- Partial execution of failed operations can cause side effects, but:
- All invariants must be preserved
- No resources are leaked
- Any stored data will contain valid values, even if it was different now from before the exception
- Does this sound familiar? A "valid, but unspecified state"
- Move constructors that are not noexcept follow these semantics
No exception safety
- No guarantees
- Don't write C++ with no exception safety
- Very hard to debug when things go wrong
- Very easy to fix - wrap your resources and attach lifetimes
- This gives you basic exception safety for free
noexcept specifier
- Specifies whether a function could potentially throw
- https://en.cppreference.com/w/cpp/language/noexcept_spec
- STL functions can operate more efficiently on noexcept functions
class S {
public:
int foo() const; // may throw
}
class S {
public:
int foo() const noexcept; // does not throw
}
Testing exceptions
CHECK_THROWS(expr);
CHECK_THROWS_AS(expr, type);
REQUIRES_THROWS* also available.
Checks expr throws an exception.
Checks expr throws type (or somthing derived from type).
CHECK_NOTHROW(expr);
Checks expr doesn't throw an exception.
Testing exceptions
REQUIRES_THROWS* also available.
CHECK_THROWS_MATCHES( expr, type, Matchers::Message("message"));
CHECK_THROWS_AS and CHECK_THROWS_WITH
in a single check.
namespace Matchers = Catch::Matchers;
CHECK_THROWS_WITH( expr, Matchers::Message("message"));
Checks expr throws an exception with a message.
COMP6771 20T2 - 4.1 - Exceptions
By cs6771
COMP6771 20T2 - 4.1 - Exceptions
- 879