Created
January 7, 2018 20:03
-
-
Save glampert/ce6fde15a742d467f0a61a6831153954 to your computer and use it in GitHub Desktop.
Mostly complete and usable C++ version of Rust's Option<T> and Result<T,E> using only standard C++14.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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