# Exceptions ___ ## Throw If you want to throw an exception you can simply call `throw` ```C++ void foo() { throw "Error"; } ``` If you want to catch an error you need to use `try-catch` ```C++ void bar() { try { foo(); } catch (const std::exception& err) { std::cout << err.what() << '\n'; } } ``` ___ ## What happens during throw? Programm will stop executing the next line, when reach `throw` command. It will startup **unwind** a stack untill it reach the first `try-catch` block which will catch this exception. Because the stack is unwinded all variables which go out of scope will be properly destructed.
```C++ struct Foo { explicit Foo(int id): id(id) { std::cout << "C'tor id: " << id << "\n"; } ~Foo() { std::cout << "D'tor id: " << id << "\n"; } int id; }; void fun2() { Foo foo(2); std::cout << "This will print!\n"; throw std::runtime_error("Bad!"); std::cout << "This will not print!\n"; } void fun1() { std::cout << "We start here!\n"; Foo foo(1); std::cout << "After construction of Foo we call function fun2 which throw\n"; fun2(); std::cout << "This will not print!\n"; } int main() { try { fun1(); std::cout << "This will not print!\n"; } catch (const std::exception& err) { std::cout << "exception: " << err.what() << '\n'; } std::cout << "Exit\n"; } ```
```bash We start here! C'tor id: 1 After construction of Foo we call function fun2 which throw C'tor id: 2 This will print! D'tor id: 2 D'tor id: 1 exception: Bad! Exit ```
___ ## Hierarchy There are more than 30 exceptions in C++! Each exception could be a base class of other (see cppreference). The most common are: * logic_error * out_of_range * bad_optional_access * runtime_error * bad_weak_ptr * bad_alloc ___ ## How to properly catch an error ```C++ void doSth() { std::cout << "Hello! "; throw std::invalid_argument("SthBad!"); } int main() { try { doSth(); } catch (const std::invalid_argument& err) { std::cout << "invalid_argument: " << err.what() << '\n'; } catch (const std::logic_error& err) { std::cout << "logic_error: " << err.what() << '\n'; } catch (const std::exception& err) { std::cout << "exception: " << err.what() << '\n'; } catch (...) { std::cout << "Undefined error!\n"; } } ``` Output: `Hello! invalid_argument: SthBad!`. ___ What happens when I will try to catch an error with an unordered hierarchy? ```C++ void doSth() { std::cout << "Hello! "; throw std::invalid_argument("SthBad!"); } int main() { try { doSth(); } catch (const std::logic_error& err) { std::cout << "logic_error: " << err.what() << '\n'; } catch (const std::invalid_argument& err) { std::cout << "invalid_argument: " << err.what() << '\n'; } catch (...) { std::cout << "Undefined error!\n"; } } ``` Output: `Hello! logic_error: SthBad!` ```bash main.cpp: In function ‘int main()’: main.cpp:21:7: warning: exception of type ‘std::invalid_argument’ will be caught 21 | } catch (const std::invalid_argument& err) { | ^~~~~ main.cpp:19:7: warning: by earlier handler for ‘std::logic_error’ 19 | } catch (const std::logic_error& err) { ``` ___ You can quickly create your error class by inheriting from the base class `std::exception`. We need to `override` one method which will return a proper error message: `virtual const char* what() const noexcept` ```C++ class MyError : public std::exception { public: explicit MyError(const std::string& what): _what(what) {} explicit MyError(const char* what): MyError(std::string(what)) {} explicit MyError(std::string_view what): MyError(std::string(what)) {} const char* what() const noexcept override { return _what.c_str(); } private: std::string _what; }; class OtherError : public MyError { public: using MyError::MyError; }; void doSth() { throw OtherError("SthBad!"); } int main() { try { doSth(); } catch (const OtherError& err) { std::cout << "OtherError: " << err.what() << '\n'; } catch (const MyError& err) { std::cout << "MyError: " << err.what() << '\n'; } catch (const std::exception& err) { std::cout << "exception: " << err.what() << '\n'; } } ``` ___ It's important to use your error classes instead of those defined by the standard library. It will allow you to determine if the error comes from the standard library or your functions. ```C++ class MyError : public std::exception { public: explicit MyError(const std::string& what): _what(what) {} const char* what() const noexcept override { return _what.c_str(); } private: std::string _what; }; class Foo { public: const std::string& getProcessName(uint64_t pid) { return _processes.at(pid); } void addProcess(uint64_t pid, const std::string& processName) { if (_processes.contains(pid)) { throw MyError(std::format("Process with PID {} already exist", pid)); } _processes[pid] = processName; } private: std::map _processes; }; int main() { try { Foo foo; const auto& processName = foo.getProcessName(20); foo.addProcess(1234, "MySuperprocess!"); } catch (const MyError& err) { std::cout << "MyError: " << err.what() << '\n'; } catch (const std::exception& err) { std::cout << "exception: " << err.what() << '\n'; } } ``` ___ ## Ways to signal an error * throw an exception * return enum class ErrorCode * return nullptr * return false * return std::optional (C++17) * return std::expected (C++23) ___ ## Descriptive errors We have three ways to strictly describe what happens wrong: `exception`, `enum class`, and `std::expected`. Using these types we can say what exactly goes wrong, like: providing the wrong credentials, or we can't find some element, etc...
```C++ int foo(const std::string& num) { if (num != "42") { throw std::runtime_error("Number is different than 42!"); } return 42; } enum class StatusCode { Ok, Not42Number }; StatusCode foo2(const std::string& num, int& res) { if (num != "42") { return StatusCode::Not42Number; } res = 42; return StatusCode::Ok; } ```
```C++ enum class ParserError { invalid_input, overflow }; std::expected parse(std::string_view str) { if (str.size() > 1'000) { return std::unexpected(ParserError::overflow); } else if (str.empty()) { return std::unexpected(ParserError::invalid_input); } return 42; } int main() { if (const auto num = parse("something")) { std::cout << "value: " << *num << '\n'; } else if (num.error() == ParserError::overflow) { std::cout << "error: overflow\n"; } else if (num.error() == ParserError::invalid_input) { std::cout << "invalid_input!\n"; } } ```
___ ## Non descriptive errors There are also three ways how to inform about the error but without describing why. This is useful for simple functions that may have only one error like whether there is a value inside the map or not. ```C++ std::unique_ptr foo3(const std::string& num) { if (num != "42") { return nullptr; } return std::make_unique(42); } bool foo4(const std::string& num, int& res) { if (num != "42") { return false; } res = 42; return true; } std::optional foo5(const std::string& num) { if (num != "42") { return std::nullopt; } return 42; } ``` ___ ### std::optional - more info Introduced in c++17 `std::optional` is a useful class that may have a value or not. * Doesn't allocate data on heap! * Require additional storage info -> bigger size * Not as efficient as normal integer -> need to perform additional actions ```C++ class Foo { int a; int b; int c; int d; }; int main() { std::cout << sizeof(int) << '\n'; std::cout << sizeof(std::optional) << '\n'; std::cout << sizeof(Foo) << '\n'; std::cout << sizeof(std::optional) << '\n'; } ``` ```C++ 4 8 16 20 ``` ___ ### std::expected - more info Introduced in c++23 `std::expected` is a useful class that may store one of two values: * expected -> the return value which we expect * unexpected -> the value that will be returned when something goes wrong In most cases as an unexpected value, we return an enum code with a description of what went wrong, but we can return anything (except reference) ```C++ struct Foo { void print() const { std::cout << "Sorry your function doesn't work!"; } }; std::expected parse(std::string_view str) { if (str.empty()) { return std::unexpected(Foo{}); } return "42"; } int main() { if (const auto num = parse("")) { std::cout << "value: " << *num << '\n'; } else { num.error().print(); } } ``` ```bash Sorry your function doesn't work! ``` ___ ## Exercise * Create an exception class `Exception` which inherits from `std::exception` and two classes `ParserException` and `ReaderException` which inherit form class `Exception`. * Rewrite a code to `ParserException` in the method `parse` and `ReaderException` in the method `read`. * Try to catch errors in `main` and print them * Rewrite a code once again and return `nullopt` when an error occurs instead of throwing an exception