PrevUpHomeNext

class expected

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.

Example
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());
}
Synopsis
// 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);
    }
  }
};
Specialization: references

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));
    }
  }
};
Specialization: void

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;
};


[12] See also a talk by Andrei Alexandrescu


PrevUpHomeNext