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::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.");
    }
}
cpp

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.
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 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.
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_;
};
cpp

Copy-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 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://astro-pure.js.org/blog/c/modern-c-basics/error-handling
Author dokee
Published at March 7, 2025
Comment seems to stuck. Try to refresh?✨