cpp-option is a generic library based on C++23, implementing a type-safe optional value container similar to Rust's Option type. The library aims to provide safer and more concise handling of nullable values in C++, supporting rich operations and modern C++ features.
opt::option<T> represents an optional value: it either contains a value of type T (Some), or is empty (None). This type is useful for:
- Initial values for variables
- Return values for functions that may fail (e.g., lookup, parse)
- Simple error reporting (None means error)
- Optional struct fields
- Optional function parameters
- Safe wrapper for nullable pointers
- Value swapping and ownership transfer in complex scenarios
With option, you can explicitly handle the presence or absence of a value, avoiding common errors like null pointer dereference or uninitialized variables.
#include "option.hpp"
constexpr opt::option<double> divide(double numerator, double denominator) {
if (denominator == 0.0) {
return opt::none;
}
return opt::some(numerator / denominator);
}
constexpr auto result_1 = divide(2.0, 3.0);
static_assert(result_1 == opt::some(0.6666666666666666));
constexpr auto result_2 = divide(2.0, 0.0);
static_assert(result_2 == opt::none);opt::option is fully compatible with the C++26 draft std::optional, but some member functions may change the returned std::optional to opt::option. You can completely replace std::optional with opt::option and enjoy richer features and possibly better performance.
opt::option passes all STL tests for std::optional.
C++ raw pointers can be null, which may lead to undefined behavior. option can safely wrap raw pointers, but it's recommended to use smart pointers (std::unique_ptr, std::shared_ptr) when possible.
#include "option.hpp"
constexpr auto whatever = 1;
static_assert(opt::some(&whatever).unwrap() == &whatever);
static_assert(opt::some(&whatever).is_some());
static_assert(opt::some(nullptr).unwrap() == nullptr);
void check_optional(const opt::option<int*>& optional) {
if (optional.is_some()) {
std::println("Has value {}", *optional.unwrap());
} else {
std::println("No value");
}
}
int main() {
auto optional = opt::none;
check_optional(optional);
int value = 9000;
auto optional2 = opt::some(&value);
check_optional(optional2);
}When multiple operations may return option, you can use method chaining to simplify nested checks:
#include "option.hpp"
#include <unordered_map>
#include <vector>
std::unordered_map<int, std::string> bt = {
{ 20, "foo" },
{ 42, "bar" }
};
auto checked_sub = [](int x, int y) -> opt::option<int> {
if (x < y)
return opt::none;
return opt::some(x - y);
};
auto checked_mul = [](int x, int y) -> opt::option<int> {
if (x > INT_MAX / y)
return opt::none;
return opt::some(x * y);
};
auto lookup = [](int x) -> opt::option<std::string> {
auto it = bt.find(x);
if (it != bt.end())
return opt::some(it->second);
return opt::none;
};
std::vector<int> values = { 0, 1, 11, 200, 22 };
std::vector<std::string> results;
for (int x : values) {
auto result = checked_sub(x, 1)
.and_then([](int x) { return checked_mul(x, 2); })
.and_then([](int x) { return lookup(x); })
.or_else([]() { return opt::some(std::string("error!")); });
results.push_back(result.unwrap());
}
// results: ["error!", "error!", "foo", "error!", "bar"]This library uses a union-based storage mechanism, supporting value types, reference types (implemented with pointers), and void (presence only), combining Rust Option semantics with C++26 features:
- Complete move/copy semantics and perfect forwarding
- Extensive
constexprsupport - Explicit
thisparameter (deducing this) - Integration with standard types like
std::expected,std::pair, etc.
Besides basic checks, option provides a rich set of member methods for state queries, value extraction, transformation, combination, in-place modification, and type conversion.
option provides several state query methods for easy branching:
is_some(): whether value is presentis_none(): whether value is absentis_some_and(pred): true if value is present and predicate holdsis_none_or(pred): true if value is absent or predicate holds
// is_some
constexpr auto x = opt::some(2);
static_assert(x.is_some());
// is_none
constexpr opt::option<int> y = opt::none;
// constexpr auto y = opt::none_opt<int>();
static_assert(y.is_none());
// is_some_and
constexpr auto z = opt::some(2);
static_assert(z.is_some_and([](int x) { return x > 1; }));
// is_none_or
constexpr auto w = opt::option<int>(opt::none);
static_assert(w.is_none_or([](int x) { return x == 2; }));option supports reference adapters for safe access:
as_ref(): tooption<const T&>as_mut(): tooption<T&>as_deref(): dereference tooption<const U&>as_deref_mut(): dereference tooption<U&>
// as_ref
constexpr auto as_ref_test() {
auto s = opt::some("abc"s);
return s.as_ref().map([](const auto &v) {
return v.size();
});
}
static_assert(as_ref_test() == opt::some(3uz));
// as_mut
constexpr auto as_mut_test() {
auto v = opt::some(1);
*v.as_mut() += 1;
return v;
}
static_assert(as_mut_test() == opt::some(2));
// as_deref
constexpr auto as_deref_test() {
auto v = 42;
auto opt = opt::some(std::addressof(v));
return opt.as_deref().unwrap();
}
static_assert(as_deref_test() == opt::some(42));
// as_deref_mut
constexpr auto as_deref_mut_test() {
auto v = 1;
auto opt = opt::some(std::addressof(v));
*opt.as_deref_mut() += 1;
return *opt.unwrap();
}
static_assert(as_deref_mut_test() == opt::some(2));option provides several ways to extract values:
unwrap(): extract value, throws if noneexpect(msg): likeunwrap()but with custom messageunwrap_or(default): return default if noneunwrap_or_default(): return default-constructed value if noneunwrap_or_else(func): call function if noneunwrap_unchecked(): unchecked extraction (undefined if none)
// unwrap
constexpr auto x = opt::some(std::string("value"));
static_assert(x.unwrap() == "value");
// expect
static_assert(x.expect("should have value") == "value");
// unwrap_or
constexpr opt::option<std::string> y = opt::none;
constexpr auto default_string = std::string("default");
static_assert(y.unwrap_or(default_string) == "default");
// unwrap_or_default
static_assert(y.unwrap_or_default() == "");
// unwrap_or_else
static_assert(y.unwrap_or_else([] { return std::string("computed"); }) == "computed");
// Note: unwrap_unchecked() is only safe if value is present
auto z = opt::some(42);
int v = z.unwrap_unchecked(); // safe
// auto w = opt::option<int>(opt::none).unwrap_unchecked(); // undefined behavioroption can interoperate with std::expected:
ok_or(error): tostd::expected<T, E>, or error if noneok_or_else(func): tostd::expected<T, E>, or function result if nonetranspose():option<std::expected<T, E>>tostd::expected<option<T>, E>
// ok_or
constexpr auto x = opt::some("foo"s);
constexpr auto y = x.ok_or("error"sv);
static_assert(y.has_value() && y.value() == "foo");
constexpr auto z = opt::none_opt<std::string>();
constexpr auto w = z.ok_or("error info"sv);
static_assert(!w.has_value() && w.error() == "error info");
// ok_or_else
constexpr auto w2 = z.ok_or_else([] {
return std::string("whatever error");
});
static_assert(!w2.has_value() && w2.error() == "whatever error");
// transpose
constexpr auto opt_exp = opt::some(std::expected<int, const char *>{ 42 });
constexpr auto exp_opt = opt_exp.transpose();
static_assert(exp_opt.has_value() && exp_opt.value() == opt::some(42));
constexpr auto opt_exp2 = opt::some(std::expected<int, const char *>{ std::unexpected{ "fail" } });
constexpr auto exp_opt2 = opt_exp2.transpose();
static_assert(!exp_opt2.has_value() && exp_opt2.error() == "fail"sv);option supports various transformation and mapping operations:
map(func): apply function if value presentmap_or(default, func): apply function if present, else return defaultmap_or_default(func): apply function if present, else return default-constructed value of return type offuncmap_or_else(default_func, func): apply function if present, else call default_funcfilter(predicate): keep value if predicate holdsflatten(): flatten nestedoption<option<T>>inspect(func): run side-effect if value present
// map
constexpr auto x = opt::some(4);
constexpr auto y = x.map([](int v) { return v * 2; });
static_assert(y == opt::some(8));
constexpr auto z = opt::option<int>(opt::none);
constexpr auto w = z.map([](int v) { return v * 2; });
static_assert(w == opt::none);
// map_or
constexpr auto f = opt::some(10);
constexpr auto r1 = f.map_or(0, [](int v) { return v + 1; });
static_assert(r1 == 11);
constexpr auto r2 = opt::option<int>(opt::none).map_or(0, [](int v) { return v + 1; });
static_assert(r2 == 0);
// map_or_default
constexpr opt::option<int> v = opt::none;
constexpr auto r5 = v.map_or_default([](auto &&v) { return v + 100; });
static_assert(r5 == 0);
// map_or_else
constexpr auto r3 = f.map_or_else([] { return 100; }, [](int v) { return v * 3; });
static_assert(r3 == 30);
constexpr auto r4 = opt::option<int>(opt::none).map_or_else([] { return 100; }, [](int v) { return v * 3; });
static_assert(r4 == 100);
// filter
constexpr auto filtered = opt::some(5).filter([](int v) { return v > 3; });
static_assert(filtered == opt::some(5));
constexpr auto filtered2 = opt::some(2).filter([](int v) { return v > 3; });
static_assert(filtered2 == opt::none);
// flatten
constexpr auto nested = opt::some(opt::some(42));
constexpr auto flat = nested.flatten();
static_assert(flat == opt::some(42));
// inspect
auto log_fn = [](int v) { std::println("got value: {}", v); };
opt::some(123).inspect(log_fn); // prints if value presentoption supports combining and unpacking:
zip(other): if both present, returns option of pairzip_with(other, func): if both present, combine with funcunzip(): option of pair to pair of options
// zip
constexpr auto a = opt::some(1);
constexpr auto b = opt::some(2);
constexpr auto zipped = a.zip(b);
static_assert(zipped == opt::some(std::pair{ 1, 2 }));
constexpr auto none_a = opt::none_opt<int>();
constexpr auto zipped2 = none_a.zip(b);
static_assert(zipped2 == opt::none);
constexpr auto s1 = opt::some("foo"sv);
constexpr auto s2 = opt::some("bar"sv);
constexpr auto zipped3 = s1.zip_with(s2, [](auto x, auto y) { return x.size() + y.size(); });
static_assert(zipped3 == opt::some(6zu));
// unzip
constexpr auto pair_opt = opt::some(std::pair(42, "hi"sv));
constexpr auto unzipped = pair_opt.unzip();
static_assert(unzipped.first == opt::some(42));
static_assert(unzipped.second == opt::some("hi"sv));
constexpr auto none_pair = opt::none_opt<std::pair<int, std::string_view>>();
constexpr auto unzipped2 = none_pair.unzip();
static_assert(unzipped2.first == opt::none);
static_assert(unzipped2.second == opt::none);option provides boolean-like logic operations:
and_(other): if present, return other; else noneor_(other): if present, return self; else otherxor_(other): only one present, return it; else noneand_then(func): if present, call funcor_else(func): if none, call func
constexpr auto a = opt::some(1);
constexpr auto b = opt::some(2);
constexpr auto n = opt::none_opt<int>();
// and_
static_assert(a.and_(b) == b);
static_assert(n.and_(b) == opt::none);
// or_
static_assert(a.or_(b) == a);
static_assert(n.or_(b) == b);
// xor_
static_assert(a.xor_(n) == a);
static_assert(n.xor_(b) == b);
static_assert(a.xor_(b) == opt::none);
static_assert(n.xor_(n) == opt::none);
// and_then
constexpr auto f = [](int x) { return opt::some(x * 10); };
static_assert(a.and_then(f) == opt::some(10));
static_assert(n.and_then(f) == opt::none);
// or_else
constexpr auto g = [] { return opt::some(99); };
static_assert(a.or_else(g) == a);
static_assert(n.or_else(g) == opt::some(99));If T supports comparison, option<T> also supports all standard comparison operations. The rules are as follows:
- An empty option (
none) is always less than an option with a value (some). - When both are
some, comparison is done by value. - Supports
<,<=,>,>=,==,!=, as well as three-way comparison (<=>). - Supports
lt,le,gt,ge,eq,ne, andcmp.
static_assert(opt::none < opt::some(0));
static_assert(opt::none <= opt::none);
static_assert(opt::none.le(opt::none));
static_assert(opt::some(0) < opt::some(1));
static_assert(opt::some(0) <= opt::some(1));
static_assert(opt::some(1) > opt::none);
static_assert(opt::some(1) >= opt::some(0));
static_assert(opt::some(1).ge(opt::some(0)));
static_assert(opt::some(1) == opt::some(1));
static_assert(opt::none == opt::none);
static_assert(opt::some(1) != opt::some(0));
static_assert(opt::some(1).ne(opt::some(0)));
static_assert((opt::some(1) <=> opt::none) == std::strong_ordering::greater);
static_assert((opt::some(1).cmp(opt::some(1))) == std::strong_ordering::equal);option supports in-place modification and lazy initialization:
insert(value): insert new valueget_or_insert(value): get or insert valueget_or_insert_default(): insert default if noneget_or_insert_with(func): insert value from function if none
// insert
constexpr auto ins() {
opt::option<int> x = opt::none;
x.insert(123);
return x;
}
static_assert(ins() == opt::some(123));
// get_or_insert
constexpr auto bar() {
opt::option<int> n = opt::none;
int &ref = n.get_or_insert(42);
return std::pair{ n, ref };
}
static_assert(bar() == std::pair{ opt::some(42), 42 });
// get_or_insert_default
constexpr auto baz() {
opt::option<int> n = opt::none;
int &ref = n.get_or_insert_default();
return std::pair{ n, ref };
}
static_assert(baz() == std::pair{ opt::some(0), 0 });
// get_or_insert_with
constexpr auto with() {
opt::option<int> x = opt::none;
int &ref = x.get_or_insert_with([] { return 77; });
return std::pair{ x, ref };
}
static_assert(with() == std::pair{ opt::some(77), 77 });option supports safe ownership transfer:
take(): take value and set to none, return old valuetake_if(pred): take value and set to none if pred is satisfied, return old valuereplace(value): replace with new value, return old value
// take
constexpr auto foo() {
auto x = opt::some(2);
auto y = x.take();
return std::pair{ x, y };
}
static_assert(foo() == std::pair{ opt::none, opt::some(2) });
// take_if
constexpr auto bar() {
auto x = opt::some(3);
auto y = x.take_if([](int v) { return v > 5; });
return std::pair{ x, y };
}
static_assert(bar() == std::pair{ opt::some(3), opt::none });
// replace
constexpr auto baz() {
auto s = opt::some("abc"s);
auto old = s.replace("xyz");
return std::pair{ s, old };
}
static_assert(baz() == std::pair{ opt::some("xyz"s), opt::some("abc"s) });option that stores references supports explicit clone and copy operations on the referenced object, making it convenient to perform deep or shallow copies when needed:
cloned():Returns a new object containing a deep copy of the current referenced value. Since it is impossible to determine whether a custom type's copy constructor performs a deep copy, the object needs to have a semantically correctclone()member function or be trivially copyable.copied():Returns a new object containing a shallow copy of the current referenced value.
// cloned
static constexpr auto v_1 = 1;
constexpr auto ref = opt::some(std::ref(v_1));
static_assert(ref.cloned() == opt::some(1));
struct x {
int v;
constexpr x clone() const noexcept {
return *this;
}
constexpr bool operator==(const x &) const = default;
};
constexpr auto v_2 = x{ .v = 1 };
static_assert(opt::some(std::ref(v_2)).cloned() == opt::some(x{ .v = 1 }));
// copied
struct y {
constexpr bool operator==(const y &) const = default;
};
constexpr auto v_3 = y{};
static_assert(opt::some(std::ref(v_3)).copied() == opt::some(y{}));enum class Kingdom { Plant, Animal };
struct BigThing {
Kingdom kind;
int size;
std::string_view name;
};
constexpr std::array<BigThing, 6> all_the_big_things = {
{
{ Kingdom::Plant, 250, std::string_view("redwood") },
{ Kingdom::Plant, 230, std::string_view("noble fir") },
{ Kingdom::Plant, 229, std::string_view("sugar pine") },
{ Kingdom::Animal, 25, std::string_view("blue whale") },
{ Kingdom::Animal, 19, std::string_view("fin whale") },
{ Kingdom::Animal, 15, std::string_view("north pacific right whale") },
}
};
constexpr opt::option<std::string_view> find_biggest_animal_name() {
int max_size = 0;
opt::option<std::string_view> max_name = opt::none;
for (const auto &thing : all_the_big_things) {
if (thing.kind == Kingdom::Animal && thing.size > max_size) {
max_size = thing.size;
max_name = opt::some(thing.name);
}
}
return max_name;
}
constexpr auto name_of_biggest_animal = find_biggest_animal_name();
static_assert(name_of_biggest_animal.is_some(), "there are no animals :(");
static_assert(name_of_biggest_animal.unwrap() == std::string_view("blue whale"),
"the biggest animal should be blue whale");#include "option.hpp"
#include <vector>
#include <algorithm>
std::vector<opt::option<int>> options = {
opt::some(1),
opt::none,
opt::some(3)
};
// Collect all present values
std::vector<int> values;
for (const auto& opt : options) {
if (opt.is_some()) {
values.push_back(opt.unwrap());
}
}
// [1, 3]
// Pipeline transformation and filtering
constexpr auto process = [](int x) -> opt::option<int> {
return opt::some(x)
.filter([](int y) { return y > 0; })
.map([](int y) { return y * 2; });
};
static_assert(process(5) == opt::some(10));This library is header-only / module-based, supporting multiple integration methods:
- Header: Copy
src/include/option.hppto your project and#include "option.hpp". - Module: Copy
src/option.cppmto your project and useimport option;. Requires compiler support for modules and standard library modules.
Unit tests are based on GoogleTest, with main file at src/test_unit.cpp.
Supported on three major compilers (GCC, Clang, MSVC), with corresponding targets:
- GCC:
test_unit_gcc - Clang:
test_unit_clang - MSVC:
test_unit_msvc
Example command (GCC):
xmake run test_unit_gcc --file=xmake.ci.luaTo customize or extend tests, refer to src/test_unit.cpp and ensure gtest is installed.
Benchmarks are based on Google Benchmark, with main file at src/bench.cpp.
Also supported on three major compilers, with corresponding targets:
- GCC:
bench_gcc - Clang:
bench_clang - MSVC:
bench_msvc
Example command (Clang):
xmake run bench_clang --file=xmake.ci.luaTo add new benchmarks, refer to src/bench.cpp and ensure benchmark is installed.
This project uses GitHub Actions for automated build, test, and benchmark. See .github/workflows/ci.yml for details. Main steps:
- Install GCC/Clang/MSVC and xmake
- Build all targets (including tests and benchmarks)
- Run unit tests and benchmarks automatically
To simulate CI locally:
xmake --yes --file=xmake.ci.lua
xmake run test_unit_gcc --file=xmake.ci.lua
xmake run bench_gcc --file=xmake.ci.lua- C++23 standard support
- Explicit
thisparameter (__cpp_explicit_this_parameter >= 202110L) std::expected(__cpp_lib_expected >= 202202L)
opt::option<int> value = opt::some(42);
opt::option<int> empty = opt::none;
if (value.is_some()) {
int x = value.unwrap();
std::println("Value: {}", x);
}
int result = empty.unwrap_or(0);
std::println("Result: {}", result);opt::option<int> value = opt::some(42);
opt::option<int> empty = opt::none;
// map operation
auto doubled = value.map([](int x) { return x * 2; });
std::println("Doubled: {}", doubled);
// or_else operation
auto fallback = empty.or_else([]() { return opt::some(99); });
std::println("Fallback: {}", fallback);
// Chaining
opt::option<int> combined = value.and_then([](int x) {
return opt::some(x + 1);
});
std::println("Combined: {}", combined);opt::some(value)— Create an option with valueopt::none— Empty optionopt::none_opt<T>()— Create an empty option of type T
// Lookup function
opt::option<std::string> find_user_name(int user_id) {
if (user_id == 42) {
return opt::some(std::string("Alice"));
}
return opt::none;
}
// Parse function
opt::option<int> parse_int(const std::string& str) {
try {
return opt::some(std::stoi(str));
} catch (...) {
return opt::none;
}
}
// Safe array access
template<typename T>
opt::option<T> safe_get(const std::vector<T>& vec, size_t index) {
if (index < vec.size()) {
return opt::some(vec[index]);
}
return opt::none;
}See LICENSE file for details.