Mostly complete and usable C++ version of Rust's Option<T> and Result<T,E> using only standard C++14.
// Mostly complete and usable C++ version of Rust's Option<T> and Result<T,E> using only standard C++14, for fun. | |
// This code is in the public domain. | |
/////////////////////////////////////////////////////////////////////////////// | |
// Option.hpp | |
/////////////////////////////////////////////////////////////////////////////// | |
#pragma once | |
// ---------------------------------------------------------------------------- | |
// Includes | |
// ---------------------------------------------------------------------------- | |
#include <cstdio> | |
#include <string> | |
#include <functional> | |
#include <type_traits> | |
#include <utility> | |
// ---------------------------------------------------------------------------- | |
// detail | |
// ---------------------------------------------------------------------------- | |
namespace detail { | |
[[noreturn]] | |
inline void option_error(const char* const error_msg) noexcept { | |
std::fprintf(stderr, "Option<T> error: %s\n", error_msg); | |
std::abort(); | |
} | |
struct NoneType final {}; | |
} // detail | |
// ---------------------------------------------------------------------------- | |
// 'None' constant | |
// ---------------------------------------------------------------------------- | |
constexpr detail::NoneType None; | |
// ---------------------------------------------------------------------------- | |
// Option<T> | |
// ---------------------------------------------------------------------------- | |
template<typename T> | |
class Option final { | |
public: | |
static constexpr bool IsHoldingReference = std::is_reference<T>::value; | |
using ValueReference = std::reference_wrapper<typename std::remove_reference<T>::type>; | |
using Value = typename std::conditional<IsHoldingReference, ValueReference, T>::type; | |
/*implicit*/ Option(detail::NoneType) noexcept | |
: m_state{State::IsNone} { | |
} | |
explicit Option(const Value& value) noexcept(std::is_nothrow_copy_constructible<Value>::value) | |
: m_state{State::IsSome} { | |
::new(&some()) Value(value); | |
} | |
explicit Option(Value&& value) noexcept(std::is_nothrow_move_constructible<Value>::value) | |
: m_state{State::IsSome} { | |
::new(&some()) Value(std::forward<Value>(value)); | |
} | |
/*implicit*/ Option(const Option& other) noexcept(std::is_nothrow_copy_constructible<Value>::value) | |
: m_state{other.m_state} { | |
if (other.is_some()) { | |
::new(&some()) Value(other.some()); | |
} | |
} | |
/*implicit*/ Option(Option&& other) noexcept(std::is_nothrow_move_constructible<Value>::value) | |
: m_state{other.m_state} { | |
if (other.is_some()) { | |
::new(&some()) Value(std::forward<Value>(other.some())); | |
} | |
} | |
Option& operator=(const Option& other) noexcept(std::is_nothrow_constructible<Value>::value && | |
std::is_nothrow_destructible<Value>::value) { | |
destroy_some(); | |
m_state = other.m_state; | |
if (other.is_some()) { | |
::new(&some()) Value(other.some()); | |
} | |
return *this; | |
} | |
Option& operator=(Option&& other) noexcept(std::is_nothrow_move_constructible<Value>::value && | |
std::is_nothrow_destructible<Value>::value) { | |
destroy_some(); | |
m_state = other.m_state; | |
if (other.is_some()) { | |
::new(&some()) Value(std::forward<Value>(other.some())); | |
} | |
return *this; | |
} | |
~Option() noexcept(std::is_nothrow_destructible<Value>::value) { | |
destroy_some(); | |
} | |
bool is_some() const noexcept { | |
return m_state == State::IsSome; | |
} | |
bool is_none() const noexcept { | |
return m_state == State::IsNone; | |
} | |
bool operator==(detail::NoneType) const noexcept { | |
return is_none(); | |
} | |
bool operator!=(detail::NoneType) const noexcept { | |
return !is_none(); | |
} | |
Option<const T&> as_ref() const noexcept { | |
if (is_some()) { | |
return Option<const T&>(some()); | |
} else { | |
return None; | |
} | |
} | |
Option<T&> as_mut() noexcept { | |
if (is_some()) { | |
return Option<T&>(some()); | |
} else { | |
return None; | |
} | |
} | |
Value& expect(const char* const error_msg) noexcept { | |
if (is_none()) { | |
detail::option_error(error_msg); | |
} | |
return some(); | |
} | |
const Value& expect(const char* const error_msg) const noexcept { | |
if (is_none()) { | |
detail::option_error(error_msg); | |
} | |
return some(); | |
} | |
Value& expect(const std::string& error_msg) noexcept { | |
return expect(error_msg.c_str()); | |
} | |
const Value& expect(const std::string& error_msg) const noexcept { | |
return expect(error_msg.c_str()); | |
} | |
Value& unwrap() noexcept { | |
return expect("Called Option::unwrap() on a None value"); | |
} | |
const Value& unwrap() const noexcept { | |
return expect("Called Option::unwrap() on a None value"); | |
} | |
Value unwrap_or(const Value& def) const { | |
if (is_some()) { | |
return some(); | |
} else { | |
return def; | |
} | |
} | |
template<typename F> | |
Value unwrap_or_else(F&& fn) const { | |
if (is_some()) { | |
return some(); | |
} else { | |
return fn(); | |
} | |
} | |
Value unwrap_or_default() const { | |
if (is_some()) { | |
return some(); | |
} else { | |
return Value{}; | |
} | |
} | |
template<typename F, typename U = std::result_of<F(const Value&)>::type> | |
Option<U> map(F&& fn) const { | |
if (is_some()) { | |
return Option<U>(fn(some())); | |
} else { | |
return None; | |
} | |
} | |
template<typename U, typename F> | |
U map_or(const U& def, F&& fn) const { | |
if (is_some()) { | |
return fn(some()); | |
} else { | |
return def; | |
} | |
} | |
template<typename FDefault, typename F> | |
auto map_or_else(FDefault&& def_fn, F&& fn) const { | |
if (is_some()) { | |
return fn(some()); | |
} else { | |
return def_fn(); | |
} | |
} | |
template<typename U> | |
Option<U> and(const Option<U>& optb) const { | |
if (is_some()) { | |
return optb; | |
} else { | |
return None; | |
} | |
} | |
template<typename F, typename U = std::result_of<F(const Value&)>::type> | |
Option<U> and_then(F&& fn) const { | |
if (is_some()) { | |
return Option<U>(fn(some())); | |
} else { | |
return None; | |
} | |
} | |
Option<Value> or(const Option<Value>& optb) const { | |
if (is_some()) { | |
return Option<Value>(some()); | |
} else { | |
return optb; | |
} | |
} | |
template<typename F> | |
Option<Value> or_else(F&& fn) const { | |
if (is_some()) { | |
return Option<Value>(some()); | |
} else { | |
return fn(); | |
} | |
} | |
Value& get_or_insert(const Value& value) { | |
if (is_none()) { | |
*this = Option(value); | |
} | |
return some(); | |
} | |
template<typename F> | |
Value& get_or_insert_with(F&& fn) { | |
if (is_none()) { | |
*this = Option(fn()); | |
} | |
return some(); | |
} | |
Option<Value> take() { | |
auto copy{*this}; | |
*this = None; | |
return copy; | |
} | |
private: | |
const Value& some() const noexcept { return m_some.m_value; } | |
Value& some() noexcept { return m_some.m_value; } | |
void destroy_some() noexcept(std::is_nothrow_destructible<Value>::value) { | |
if (is_some() && !std::is_trivially_destructible<Value>::value) { | |
m_some.m_value.~Value(); | |
} | |
} | |
union Storage { | |
Storage() {} | |
~Storage() {} | |
Value m_value; | |
}; | |
enum class State : char { | |
IsNone, | |
IsSome, | |
}; | |
Storage m_some; | |
State m_state; | |
}; | |
// ---------------------------------------------------------------------------- | |
// Some() | |
// ---------------------------------------------------------------------------- | |
template<typename T> | |
inline auto Some(T value) { | |
return Option<T>(std::move(value)); | |
} | |
/////////////////////////////////////////////////////////////////////////////// | |
// Result.hpp | |
/////////////////////////////////////////////////////////////////////////////// | |
#pragma once | |
// ---------------------------------------------------------------------------- | |
// Includes | |
// ---------------------------------------------------------------------------- | |
#include "Option.hpp" | |
#include <cstdio> | |
#include <string> | |
#include <functional> | |
#include <type_traits> | |
#include <utility> | |
// ---------------------------------------------------------------------------- | |
// detail | |
// ---------------------------------------------------------------------------- | |
#if (__cplusplus >= 201703L) | |
#define RESULT_WARN_UNUSED [[nodiscard]] | |
#else // Not C++17 | |
#if defined(_MSC_VER) | |
#include <sal.h> | |
#define RESULT_WARN_UNUSED _Check_return_ | |
#elif defined(__GNUC__) | |
#define RESULT_WARN_UNUSED __attribute__((warn_unused_result)) | |
#else | |
#define RESULT_WARN_UNUSED /*nothing*/ | |
#endif | |
#endif | |
namespace detail { | |
[[noreturn]] | |
inline void result_error(const char* const error_msg) noexcept { | |
std::fprintf(stderr, "Result<T,E> error: %s\n", error_msg); | |
std::abort(); | |
} | |
struct ResultValuePlaceholder final {}; | |
struct ResultErrorPlaceholder final {}; | |
} // detail | |
// ---------------------------------------------------------------------------- | |
// Result<T,E> | |
// ---------------------------------------------------------------------------- | |
template<typename T, typename E> | |
class RESULT_WARN_UNUSED Result final { | |
public: | |
static constexpr bool IsHoldingValueReference = std::is_reference<T>::value; | |
static constexpr bool IsHoldingErrorReference = std::is_reference<E>::value; | |
using ValueReference = std::reference_wrapper<typename std::remove_reference<T>::type>; | |
using ErrorReference = std::reference_wrapper<typename std::remove_reference<E>::type>; | |
using Value = typename std::conditional<IsHoldingValueReference, ValueReference, T>::type; | |
using Error = typename std::conditional<IsHoldingErrorReference, ErrorReference, E>::type; | |
template<typename V2> | |
/*implicit*/ Result(const Result<V2, detail::ResultErrorPlaceholder>& val_holder) | |
: Result(val_holder.unwrap()) { | |
} | |
template<typename E2> | |
/*implicit*/ Result(const Result<detail::ResultValuePlaceholder, E2>& err_holder) | |
: Result(err_holder.unwrap_err()) { | |
} | |
explicit Result(const Value& val) { | |
::new(&m_data.as_value) Value(val); | |
m_is_err = false; | |
} | |
explicit Result(Value&& val) { | |
::new(&m_data.as_value) Value(std::forward<Value>(val)); | |
m_is_err = false; | |
} | |
explicit Result(const Error& err) { | |
::new(&m_data.as_error) Error(err); | |
m_is_err = true; | |
} | |
explicit Result(Error&& err) { | |
::new(&m_data.as_error) Error(std::forward<Error>(err)); | |
m_is_err = true; | |
} | |
Result(const Result& other) { | |
copy_helper(other); | |
} | |
Result(Result&& other) { | |
move_helper(std::forward<Result>(other)); | |
} | |
Result& operator=(const Result& other) { | |
dtor_helper(); | |
copy_helper(other); | |
return *this; | |
} | |
Result& operator=(Result&& other) { | |
dtor_helper(); | |
move_helper(std::forward<Result>(other)); | |
return *this; | |
} | |
~Result() { | |
dtor_helper(); | |
} | |
bool is_ok() const noexcept { | |
return !is_err(); | |
} | |
bool is_err() const noexcept { | |
return m_is_err; | |
} | |
const Value& unwrap() const noexcept { | |
if (is_err()) { | |
detail::result_error("Called Result::unwrap() on an Error"); | |
} | |
return m_data.as_value; | |
} | |
Value& unwrap() noexcept { | |
if (is_err()) { | |
detail::result_error("Called Result::unwrap() on an Error"); | |
} | |
return m_data.as_value; | |
} | |
const Error& unwrap_err() const noexcept { | |
if (is_ok()) { | |
detail::result_error("Called Result::unwrap_err() on a Value"); | |
} | |
return m_data.as_error; | |
} | |
Error& unwrap_err() noexcept { | |
if (is_ok()) { | |
detail::result_error("Called Result::unwrap_err() on a Value"); | |
} | |
return m_data.as_error; | |
} | |
Option<Value> ok() const { | |
if (is_ok()) { | |
return Some(m_data.as_value); | |
} else { | |
return None; | |
} | |
} | |
Option<Error> err() const { | |
if (is_err()) { | |
return Some(m_data.as_error); | |
} else { | |
return None; | |
} | |
} | |
Result<const T&, const E&> as_ref() const noexcept { | |
if (is_ok()) { | |
return Result<const T&, const E&>(m_data.as_value); | |
} else { | |
return Result<const T&, const E&>(m_data.as_error); | |
} | |
} | |
Result<T&, E&> as_mut() noexcept { | |
if (is_ok()) { | |
return Result<T&, E&>(m_data.as_value); | |
} else { | |
return Result<T&, E&>(m_data.as_error); | |
} | |
} | |
private: | |
void dtor_helper() { | |
if (is_ok()) { | |
if (!std::is_trivially_destructible<Value>::value) { | |
m_data.as_value.~Value(); | |
} | |
} else { | |
if (!std::is_trivially_destructible<Error>::value) { | |
m_data.as_error.~Error(); | |
} | |
} | |
} | |
void copy_helper(const Result& other) { | |
if (other.is_ok()) { | |
::new(&m_data.as_value) Value(other.m_data.as_value); | |
} else { | |
::new(&m_data.as_error) Error(other.m_data.as_error); | |
} | |
m_is_err = other.m_is_err; | |
} | |
void move_helper(Result&& other) { | |
if (other.is_ok()) { | |
::new(&m_data.as_value) Value(std::move(other.m_data.as_value)); | |
} else { | |
::new(&m_data.as_error) Error(std::move(other.m_data.as_error)); | |
} | |
m_is_err = other.m_is_err; | |
} | |
private: | |
union Data { | |
Data() {} | |
~Data() {} | |
Value as_value; | |
Error as_error; | |
}; | |
Data m_data; | |
bool m_is_err; | |
}; | |
// ---------------------------------------------------------------------------- | |
// Ok(), Err() | |
// ---------------------------------------------------------------------------- | |
template<typename T> | |
inline auto Ok(T value) { | |
return Result<T, detail::ResultErrorPlaceholder>(std::move(value)); | |
} | |
template<typename E> | |
inline auto Err(E error) { | |
return Result<detail::ResultValuePlaceholder, E>(std::move(error)); | |
} | |
/////////////////////////////////////////////////////////////////////////////// | |
// Tests.cpp | |
/////////////////////////////////////////////////////////////////////////////// | |
#include "Option.hpp" | |
#include "Result.hpp" | |
#include <cassert> | |
#include <cstdint> | |
#include <cstdio> | |
#include <string> | |
using namespace std::string_literals; | |
// ---------------------------------------------------------------------------- | |
// Tests | |
// ---------------------------------------------------------------------------- | |
struct S1 { | |
S1() { std::printf("S1()\n"); } | |
~S1() { std::printf("~S1()\n"); } | |
S1(const S1&) { std::printf("S1(const S1&)\n"); } | |
S1(S1&&) { std::printf("S1(S1&&)\n"); } | |
S1& operator=(const S1&) { std::printf("operator=(const S1&)\n"); return *this; } | |
S1& operator=(S1&&) { std::printf("operator=(S1&&)\n"); return *this; } | |
}; | |
struct S2 { | |
S2() { std::printf("S2()\n"); } | |
~S2() { std::printf("~S2()\n"); } | |
S2(const S2&) { std::printf("S2(const S2&)\n"); } | |
S2(S2&&) { std::printf("S2(S2&&)\n"); } | |
S2& operator=(const S2&) { std::printf("operator=(const S2&)\n"); return *this; } | |
S2& operator=(S2&&) { std::printf("operator=(S2&&)\n"); return *this; } | |
}; | |
// ---------------------------------------------------------------------------- | |
namespace test_optional { | |
Option<S1> get_some() { | |
auto r = S1{}; | |
return Some(r); | |
} | |
Option<S2> get_none() { | |
return None; | |
} | |
Option<double> divide(double numerator, double denominator) { | |
if (denominator == 0.0) { | |
return None; | |
} else { | |
return Some(numerator / denominator); | |
} | |
} | |
void run() { | |
auto a = get_some(); | |
auto b = get_none(); | |
Option<S1> n = None; | |
assert(a.is_some()); | |
assert(b.is_none()); | |
assert(n.is_none()); | |
Option<const S1 &> ref_a = a.as_ref(); | |
Option<S2 &> m_ref_b = b.as_mut(); | |
assert(ref_a.is_some()); | |
assert(m_ref_b.is_none()); | |
Option<int> no_int = None; | |
Option<int> some_int = Some(42); | |
assert(no_int == None); | |
assert(no_int.is_none()); | |
assert(some_int != None); | |
assert(some_int.is_some()); | |
assert(some_int.unwrap() == 42); | |
auto air = Some("air"s); | |
assert(air.unwrap() == "air"s); | |
auto maybe_string = Some("value"s); | |
assert(maybe_string.expect("the world is ending"s) == "value"s); | |
assert(Some("car"s).unwrap_or("bike"s) == "car"s); | |
assert(Option<std::string>{None}.unwrap_or("bike"s) == "bike"s); | |
const int k1 = 10; | |
assert(Some(4).unwrap_or_else([k1]() { return 2 * k1; }) == 4); | |
assert(Option<int>{None}.unwrap_or_else([k1]() { return 2 * k1; }) == 20); | |
auto maybe_some_string = Some("Hello, World!"s); | |
auto maybe_some_len = maybe_some_string.map( | |
[](const std::string& s) { return unsigned(s.length()); }); | |
assert(maybe_some_len.unwrap() == 13u); | |
auto d0 = divide(3.0, 2.0); | |
assert(d0.unwrap() == 1.5); | |
auto d1 = divide(3.0, 0.0); | |
assert(d1.is_none()); | |
auto foo = Some("foo"s); | |
assert(foo.map_or(42u, | |
[](const std::string& s) { return unsigned(s.length()); }) == 3u); | |
Option<std::string> bar = None; | |
assert(bar.map_or(42u, | |
[](const std::string& s) { return unsigned(s.length()); }) == 42u); | |
const auto k2 = 21u; | |
auto x = Some("foo"); | |
assert(x.map_or_else( | |
[k2]() { return 2u * k2; }, | |
[](const std::string & s) { return unsigned(s.length()); }) == 3); | |
Option<std::string> y = None; | |
assert(y.map_or_else( | |
[k2]() { return 2u * k2; }, | |
[](const std::string & s) { return unsigned(s.length()); }) == 42); | |
auto i = Some(123); | |
assert(i.unwrap_or_default() == 123); | |
Option<int> j = None; | |
assert(j.unwrap_or_default() == 0); | |
Option<unsigned> xy = None; | |
xy = None; | |
{ | |
unsigned& zy = xy.get_or_insert(5u); | |
assert(zy == 5u); | |
zy = 7u; | |
} | |
assert(xy.unwrap() == 7u); | |
xy = None; | |
{ | |
unsigned& zy = xy.get_or_insert_with([]() { return 5u; }); | |
assert(zy == 5u); | |
zy = 7u; | |
} | |
assert(xy.unwrap() == 7u); | |
Option<int> xa = Some(2); | |
Option<int> old = xa.take(); | |
assert(xa == None); | |
assert(old.unwrap() == 2); | |
Option<int> xb = None; | |
old = xb.take(); | |
assert(xb == None); | |
assert(old.is_none()); | |
Option<int> xx = Some(2); | |
Option<std::string> yy = None; | |
assert(xx.and(yy) == None); | |
auto zero = [](int) { return 0; }; | |
auto square = [](int n) { return n*n; }; | |
assert(Some(2).and_then(square).and_then(square).unwrap() == 16); | |
assert(Some(2).and_then(square).and_then(zero).unwrap() == 0); | |
assert(Some(2).and_then(zero).and_then(square).unwrap() == 0); | |
assert(Option<int>{None}.and_then(square).and_then(square) == None); | |
auto nobody = []() -> Option<std::string> { return None; }; | |
auto vikings = []() -> Option<std::string> { return Some("vikings"s); }; | |
assert(Some("barbarians"s).or_else(vikings).unwrap() == "barbarians"s); | |
assert(Option<std::string>{None}.or_else(vikings).unwrap() == "vikings"s); | |
assert(Option<std::string>{None}.or_else(nobody) == None); | |
} | |
} // test_optional | |
// ---------------------------------------------------------------------------- | |
namespace test_result { | |
Result<S1, S2> get_result_ok() { | |
auto r = S1{}; | |
return Ok(r); | |
} | |
Result<S1, S2> get_result_err() { | |
auto r = S2{}; | |
return Err(r); | |
} | |
void run() { | |
auto o = get_result_ok(); | |
auto e = get_result_err(); | |
assert(o.is_ok()); | |
assert(e.is_err()); | |
Result<int, std::string> x = Ok(2); | |
assert(x.ok().unwrap() == 2); | |
Result<int, std::string> y = Err("Nothing here"s); | |
assert(y.ok() == None); | |
Result<int&, std::string&> xref = x.as_mut(); | |
Result<const int&, const std::string&> yref = y.as_ref(); | |
assert(xref.is_ok()); | |
assert(yref.is_err()); | |
} | |
} // test_result | |
// ---------------------------------------------------------------------------- | |
int main() { | |
test_optional::run(); | |
test_result::run(); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment