dokee's site

Back

Modern C++ Basics - Error HandlingBlur image

Error Code Extension#

std::optional<T>#

  • It uses an additional bool to denote “exist or not”, and empty value is then introduced as std::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.
  • operator<=>: no value is considered as smallest.

  • std::hash: It is guaranteed to have the same hash as std::hash<T> if it’s not std::nullopt.

  • operator->/operator*: UB if it’s in fact std::nullopt.

  • .has_value()/operator bool

  • .value(): throw std::bad_optional_access

  • .value_or(xx)

  • Monadic operations: .and_then(F)/.transform(F)/.or_else(F)

std::excepted<T, E>#

  • Ctor:
    • For normal value, just use T, or std::in_place with arguments.
    • For error value, you can use std::unexpected{xx}, or std::unexpect with its arguments.
  • Doesn’t support std::hash.
  • only supports operator==/!=.
  • .value(): may throw std::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&)
  • Exception should definitely be caught by const Type&.
  • Use throw; instead of throw 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 of new/delete.
    • std::lock_guard to manage mutex instead of lock/unlock.
    • std::fstream to manage file instead of FILE* 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.
  • 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_;
};
cpp

Copy-and-swap idiom#

  • 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 to std::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, then std::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 are noexcept.
    • Compiler-generated ctor / assignment operators are also noexcept if all corresponding ctors / assignment operators of members are noexcept.
  • For normal methods, only when the operation obviously doesn’t throw should you add noexcept.
    • swap() should always be noexcept.

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")

Debug helpers#

  • std::source_location
  • std::stacktrace
  • debugging: std::breakpoint

Unit test#

  • catch2
Modern C++ Basics - Error Handling
https://dokee.moe/blog/c/modern-c-basics/error-handling
Author dokee
Published at March 7, 2025
Comment seems to stuck. Try to refresh?✨