

Modern C++ Basics - Error Handling
Exceptions: Because `noexcept` Was a Lie and `std::terminate` is Judging You
Error Code Extension#
std::optional<T>
#
-
It uses an additional
bool
to denote “exist or not”, and empty value is then introduced asstd::nullopt
. -
Ctor/
operator=
/swap
/emplace
/std::swap
/std::make_optional
- Ctor can also accept
(std::in_place, Args)
. - By default, it’s constructed as
std::nullopt
. - Use
.reset()
to make it null.
- Ctor can also accept
-
operator<=>
: no value is considered as smallest. -
std::hash
: It is guaranteed to have the same hash asstd::hash<T>
if it’s notstd::nullopt
. -
operator->
/operator*
: UB if it’s in factstd::nullopt
. -
.has_value()
/operator bool
-
.value()
: throwstd::bad_optional_access
-
.value_or(xx)
-
Monadic operations:
.and_then(F)
/.transform(F)
/.or_else(F)
std::optional<UserProfile> fetchFromCache(int userId);
std::optional<UserProfile> fetchFromServer(int userId);
std::optional<int> extractAge(const UserProfile& profile);
int main() {
const int userId = 114514;
const auto ageNext = fetchFromServer(userId)
.or_else([&]() { return fetchFromServer(userId); })
.and_then(extractAge)
.transform([](int age) { return age + 1; });
if (ageNext) {
std::println("Next year, the user will be {} years old.", *ageNext);
} else {
std::println("Failed to determine user's age.");
}
}
cppstd::excepted<T, E>
#
- Ctor:
- For normal value, just use
T
, orstd::in_place
with arguments. - For error value, you can use
std::unexpected{xx}
, orstd::unexpect
with its arguments.
- For normal value, just use
enum class parse_error
{
invalid_input,
overflow
};
auto parse_number(std::string_view& str) -> std::expected<double, parse_error>
{
const char* begin = str.data();
char* end;
double retval = std::strtod(begin, &end);
if (begin == end)
return std::unexpected(parse_error::invalid_input);
else if (std::isinf(retval))
return std::unexpected(parse_error::overflow);
str.remove_prefix(end - begin);
return retval;
}
cpp- Doesn’t support
std::hash
. - only supports
operator==/!=
. .value()
: may throwstd::bad_expect_access
.error()
: get the error.- Monadic operations: adds a
.transform_error(Err)
.
Exception#
Basics#
- You only need to catch exception when this method can handle it.
- Though you can throw any type, it’s recommended to throw a type inherited from
std::exception
.- Reason:
catch (const std::exception&)
- Reason:
- Exception should definitely be caught by
const Type&
. - Use
throw;
instead ofthrow ex;
. - If another exception is thrown during internal exception handling,
std::terminate
will also be called.
Exception safety#
-
No guarantee
-
Basic guarantee
-
Strong guarantee
-
Nothrow guarantee
-
Exception safety means that when an exception is thrown and caught, program is still in a valid state and can correctly run.
-
RAII (Resource acquirement is initialization): acquire resources in ctor and release them in dtor.
std::unique_ptr
to manage heap memory instead ofnew
/delete
.std::lock_guard
to manage mutex instead oflock
/unlock
.std::fstream
to manage file instead ofFILE* fopen
/fclose
.- Your class should also obey this rule!
Exception in ctor#
- All members that have been fully constructed will be destructed, but dtor of itself won’t be called.
- If you have to own a raw pointer that has ownership to the memory (which is weird), then don’t initialize it in the list.
class Foo {
public:
Foo(int id) : ptr1 { nullptr }, ptr2 { nullptr } {
auto init_ptr1 = std::unique_ptr<int>{ new int { id }};
auto init_ptr2 = std::unique_ptr<int>{ new int { id }};
// only release when all exceptions are possibly thrown!
ptr1 = init_ptr1.release();
ptr2 = init_ptr2.release();
}
~Foo() {
delete ptr1;
delete ptr2;
}
private:
int* ptr1;
int* ptr2;
};
cpp- Function-try-block
catch
has to rethrow the current exception or throw a new exception.- In catch block, you shouldn’t use any uninitialized member either.
class A {
public:
A(const std::string_view& name)
try : name_{ name } {
std::println("init");
} catch (const std::exception& e) {
std::println("{}", e.what());
throw;
}
private:
std::string name_;
};
cppCopy-and-swap idiom#
template <typename T>
class Vector {
public:
Vector(std::size_t num, const T& val);
Vector(const Vector& another) {
auto size = another.size();
std::unique_ptr<T[]> arr { new T[size] };
std::ranges::copy(another.first_, another.last_, arr.get());
first_ = arr.release();
last_ = end_ = first_ + size;
}
Vector& operator=(const Vector& another) {
Vector vec { another };
swap(vec, *this);
return *this;
}
friend void swap(Vector& vec1, Vector& vec2) noexcept {
std::ranges::swap(vec1.first_, vec2.first_);
std::ranges::swap(vec1.last_, vec2.last_);
std::ranges::swap(vec1.end_, vec2.end_);
}
std::size_t size() const noexcept;
auto& operator[](std::size_t idx) noexcept;
const auto& operator[](std::size_t idx) const noexcept;
private:
T* first_;
T* last_;
T* end_;
};
cpp- Pros:
- Provide strong exception guarantee.
- Increase code reusability.
- Cons:
- Allocating memory before releasing, which increases peak memory.
- May be not optimal for performance.
Exception safety of containers#
- All read-only &
.swap()
don’t throw at all. - For
std::vector
.push_back()
/.emplace_back()
, or.insert()
/.emplace()
/.insert_range()
/.append_range()
only one element at back provide strong exception guarantee..insert()
/.emplace()
/…, if you guarantee copy / move ctor & assignment / iterator move not to throw, then still strong exception guarantee.- Otherwise only basic exception guarantee.
- For
std::list
/std::forward_list
, all strong exception guarantee. - For
std::deque
, it’s similar tostd::vector
, adding push at front. - For associative containers,
.insert()
/… a node / only a single element has strong exception guarantee.
noexcept
#
- If your function is labeled as
noexcept
but it throws exception, thenstd::terminate
will be called. noexcept
is also an operator.- destructor & deallocation is always assumed to be
noexcept
by standard library; you must obey it.- Dtor is the only function by default
noexcept
if all dtors of members arenoexcept
. Compiler-generated
ctor / assignment operators are alsonoexcept
if all corresponding ctors / assignment operators of members arenoexcept
.
- Dtor is the only function by default
- For normal methods, only when the operation
obviously
doesn’t throw should you addnoexcept
.swap()
should always benoexcept
.
Conclusion#
- Pros:
- Propagate by stack unwinding; only process exception when the method can.
- Force programmers to pay attention to errors (terminate the program).
- Cons:
- Not good for performance-critical sessions; not proper to use in hot path.
- Not convenient for cross-module try-catch.
- Many compilers don’t optimize it for multi-threading programs.
- Actually one more: code size may bloat (but this is usually not cared currently).
Assertion#
assert(expression && "SomeInfo"))
- Particularly, this check is only done when macro
NDEBUG
is not defined (like Debug mode in VS); otherwise this macro does nothing (equivalent to(void)0
). assert
is done in runtime.- If you want to determine in compile time, you can use keyword
static_assert(expression, "SomeInfo")
- If you want to determine in compile time, you can use keyword
Debug helpers#
std::source_location
std::stacktrace
- debugging:
std::breakpoint
Unit test#
- catch2