In commit “util: Make Expected::value() throw” (fa0297540e29f077a76133d1b60d3fd35f03b4e1)
In the standard std::expected class there is a clear difference between the high-level value() method which throws an exception when the value is unset, and lower level pointer operators which are noexcept and can’t be called without a value. This makes std::expected consistent with std::optional, which also has noexcept pointer operators and a throwing value() method. It’s also similar to std::vector which has nonthrowing operator[] methods and throwing at() methods.
I don’t see a reason why util::Expected class shouldn’t follow the same pattern and have noexcept pointer operators, especially if it’s intended to be replaced with std::expected in the future. I also think it would clean up the implementation to have a separation between low level pointer operators and higher level accessor methods. Would suggest:
0template <class T, class E>
1class Expected
2{
3private:
4 std::variant<T, E> m_data;
5
6public:
7 // Value constructors.
8 constexpr Expected() = default;
9 constexpr Expected(T v) : m_data{std::in_place_index<0>, std::move(v)} {}
10
11 // Error constructor.
12 template <class G>
13 constexpr Expected(Unexpected<G> u) : m_data{std::in_place_index<1>, std::move(u).error()} {}
14
15 // Value accessors.
16 constexpr explicit operator bool() const noexcept { return m_data.index() == 0; }
17 constexpr const T& operator*() const& noexcept LIFETIMEBOUND { return *Assert(std::get_if<0>(&m_data)); }
18 constexpr T& operator*() & noexcept LIFETIMEBOUND { return *Assert(std::get_if<0>(&m_data)); }
19 constexpr T&& operator*() && noexcept LIFETIMEBOUND { return std::move(**this); }
20 constexpr const T* operator->() const noexcept LIFETIMEBOUND { return Assert(std::get_if<0>(&m_data)); }
21 constexpr T* operator->() noexcept LIFETIMEBOUND { return Assert(std::get_if<0>(&m_data)); }
22
23 // Error accessors.
24 constexpr const E& error() const& LIFETIMEBOUND { return *Assert(std::get_if<1>(&m_data)); }
25 constexpr E& error() & LIFETIMEBOUND { return *Assert(std::get_if<1>(&m_data)); }
26 constexpr E&& error() && LIFETIMEBOUND { return std::move(this->error()); }
27
28 // Higher level helpers.
29 constexpr bool has_value() const noexcept { return bool{*this}; }
30 constexpr const T& value() const& LIFETIMEBOUND { if (!*this) throw BadExpectedAccess{}; return **this; }
31 constexpr T& value() & LIFETIMEBOUND { if (!*this) throw BadExpectedAccess{}; return **this; }
32 constexpr T&& value() && LIFETIMEBOUND { return std::move(this->value()); }
33
34 template <class U>
35 T value_or(U&& default_value) const&
36 {
37 return *this ? **this : std::forward<U>(default_value);
38 }
39
40 template <class U>
41 T value_or(U&& default_value) &&
42 {
43 return *this ? std::move(**this) : std::forward<U>(default_value);
44 }
45};
This implementation would also make the value() methods throw in debug builds instead of aborting, which I think is good because std::expected does that, and there isn’t much reason to call value() if exceptions are unwanted. But no strong opinion about this aspect, and this could be easily tweaked to make value() abort in debug builds by replacing if (!*this) with if (!Assume(*this)) in the two value() methods.