primer::expected is our primary error handling
mechanism.
It is broadly similar in motivation, design, and implementation to the
std::expected type which was proposed for
the C++17 standard. [12]
primer::expected<T>
is a container which holds either a value of type T,
or a primer::error.
Given a value expected<T> e,
the operator bool
of e tests whether it has
a value or an error. true
indicates a value, false indicates
an error.
The value is accessed by reference using operator
*.
The error is accessed by reference using member function err().
Both kinds of access are unchecked, you get undefined
behavior if you use operator * when there actually is an error.
using primer::expected; expected<std::string> foo(expected<int> e) { if (e) { if (*e >= 7) { return std::string{"woof!"}; } else { return primer::error{"bad doggie!"}; } } else { return e.err(); } } void test_primer_expected() { auto result = foo(6); assert(!result); auto result2 = foo(7); assert(result2); assert(*result2 == "woof!"); auto result3 = foo(primer::error("404")); assert(!result3); assert(result3.err().what() == std::string{"404"}); assert(result3.err().str() == "404"); assert(result3.err().c_str() == result3.err().what()); }
// Define primary class template template <typename T, typename E> class expected { union { T ham_; E spam_; }; bool have_ham_; PRIMER_STATIC_ASSERT( std::is_nothrow_move_constructible<T>::value, "This class can only be used with types that are no-throw " "move constructible and destructible."); PRIMER_STATIC_ASSERT( std::is_nothrow_move_constructible<E>::value, "This class can only be used with types that are no-throw " "move constructible and destructible."); public: // Accessors and dereference semantics explicit operator bool() const noexcept { return have_ham_; } T & operator*() & noexcept; T && operator*() && noexcept; const T & operator*() const & noexcept; T * operator->() & noexcept; const T * operator->() const & noexcept; E & err() & noexcept; E && err() && noexcept; const E & err() const & noexcept; // Special member functions // expected<T> is only default constructible if T is, and it throws if T does expected(); expected(expected &&) noexcept; expected & operator=(expected &&) noexcept; expected(const expected &); expected & operator=(const expected &); ~expected() noexcept; // Conversion from T expected(const T & t); expected(T && t) noexcept; // Conversion from `E`. // This is to make it so that we can simply return `E` from // functions that return `expected<T>`. expected(const E & e); expected(E && e) noexcept; // Conversions to other `expected` types // This allows you to explicitly convert to expected<U> as long as U is // constructible from T. This unpacks and repacks the expected. template <typename U, typename EU = E> expected<U, EU> convert() const &; template <typename U, typename EU = E> expected<U, EU> convert() &&; // Map function. // This is like monadic bind. // If we have a value, apply this function to it and return the result. // If we have an error, just return the error. // If the function returns an expected type, collapse the // `expected<expected<...>>` return into a single `expected<...>`. template <typename F> auto map(F && f) & -> fold_expected_t<decltype(std::forward<F>(f)( *static_cast<T *>(nullptr))), E> { if (*this) { return std::forward<F>(f)(**this); } else { return this->err(); } } template <typename F> auto map(F && f) const & -> fold_expected_t<decltype(std::forward<F>(f)( *static_cast<const T *>(nullptr))), E> { if (*this) { return std::forward<F>(f)(**this); } else { return this->err(); } } template <typename F> auto map(F && f) && -> fold_expected_t<decltype(std::forward<F>(f)( std::declval<T>())), E> { if (*this) { return std::forward<F>(f)(std::move(**this)); } else { return std::move(this->err()); } } // value_or function. Similar to what it does in std::optional. template <typename U> T value_or(U && u) const & { if (*this) { return **this; } else { return std::forward<U>(u); } } template <typename U> T value_or(U && u) && { if (*this) { return std::move(**this); } else { return std::forward<U>(u); } } };
primer::expected<T&>
is specialized, since unions cannot contain references.
expected<T&>
is implemented as a special interface over expected<T*>.
// Define `T&` specialization template <typename T, typename E> class expected<T &, E> { expected<T *, E> internal_; public: // Accessors explicit operator bool() const noexcept { return static_cast<bool>(internal_); } T & operator*() & { return **internal_; } T & operator*() const & { return **internal_; } T & operator*() && { return **internal_; } T * operator->() & { return *internal_; } T * operator->() const & { return *internal_; } E & err() & { return internal_.err(); } const E & err() const & { return internal_.err(); } E && err() && { return std::move(internal_.err()); } // Not default constructible expected() = delete; // Defaulted special member functions expected(const expected &) = default; expected(expected &&) noexcept = default; expected & operator=(const expected &) = default; expected & operator=(expected &&) noexcept = default; ~expected() noexcept = default; // Additional ctors expected(T & t) noexcept // : internal_(&t) // {} expected(const E & e) : internal_(e) {} expected(E && e) noexcept // : internal_(std::move(e)) // {} // Map function. // This is like monadic bind. // If we have a value, apply this function to it and return the result. // If we have an error, just return the error. // If the function returns an expected type, collapse the // `expected<expected<...>>` return into a single `expected<...>`. template <typename F> auto map(F && f) const -> fold_expected_t<decltype(std::forward<F>(f)(*static_cast<T *>(nullptr))), E> { if (*this) { return std::forward<F>(f)(**this); } else { return this->err(); } } // value_or function. Similar to what it does in std::optional. template <typename U> T & value_or(U && u) const { if (*this) { return **this; } else { return static_cast<T &>(std::forward<U>(u)); } } };
primer::expected<void>
is specialized, in order to represent "successful completion of an
operation, or an error".
expected<void>
mostly has a similar interface to the others, in that operator
bool returns false
in the presence of an error. However, expected<void> has no operator
* and is not implicitly convertible
to other kinds of expected.
// define void specialization template <typename E> class expected<void, E> { bool no_error_; union { E error_; char dummy_; }; public: // Accessors explicit operator bool() const noexcept; E & err() & noexcept; const E & err() const & noexcept; E && err() && noexcept; // Default-construct in the "ok" / `true` state constexpr expected() noexcept; // Special member functions expected(const expected &); expected(expected &&) noexcept; expected & operator=(const expected &); expected & operator=(expected &&) noexcept; ~expected() noexcept; // Additional Constructors // Allow implicit conversion from `E`, so that we can return // `E` from functions that return `expected<void>`. expected(const E & e); expected(E && e) noexcept; };