The Option type, and the
some and none
type-deduction constructor functions.
The Option type represents an optional value:
every Option is either Some and contains a
value, or None, and does not. It is similar to
std::optional but
with some differences:
- Extensive vocabulary for combining
Options together. - Safe defined behaviour (a panic) when unwrapping an empty
Option, with an explicit unsafe backdoor (unwrap_unchecked) for when it is needed. - Avoid accidental expensive copies. Supports
Copyif the inner type isCopyandCloneif the inner type isClone. - Provides
take()to move a value out of an lvalueOption, which mark the lvalue as empty instead of leaving a moved-from value behind as withstd::move(optional).value(). - A custom message can be printed when trying to unwrap an empty
Option. - Subspace Iterator integration.
Optioncan be iterated over, acting like a single-element collection, which allows it to be chained together with other iterators, filtered, etc.
Option types are very common, as they have a
number of uses:
- Initial values
- Return values for functions that are not defined over their entire input range (partial functions)
- Return value for otherwise reporting simple errors, where
Noneis returned on error. - Optional struct fields Struct fields that can be loaned or "taken"
- Optional function arguments
- Returning an optional reference to a member
Quick start
When the type is known to the compiler, you can construct an
Option from a value without writing the full
type again, by using
sus::some(x) to make an
Option holding x or
sus::none() to make an empty
Option. If returning an
Option from a lambda, be sure to specify the
return type on the lambda to allow successful type deduction.
// Returns Some("power!") if the input is over 9000, or None otherwise.
auto is_power = [](i32 i) -> sus::Option<std::string> {
if (i > 9000) return sus::some("power!");
return sus::none();
};
Use is_some and
is_none to see if the
Option is holding a value.
To immediately pull the inner value out of an
Option an an rvalue, use
unwrap. If the
Option is an lvalue, use
as_value and
as_value_mut to access the
inner value. Like
std::optional,
operator* and
operator-> are also
available if preferred. However if doing this many times, consider doing
unwrap a single time up front.
sus_check(is_power(9001).unwrap() == "power!");
if (Option<std::string> lvalue = is_power(9001); lvalue.is_some())
sus_check(lvalue.as_value() == "power!");
sus_check(is_power(9000).unwrap_or("unlucky") == "unlucky");
Option<const T> for non-reference-type T
is disallowed, as the Option
owns the T in that case and it ensures the
Option and the T are both
accessed with the same constness.
Representation
If a type T is a reference or satisties
NeverValueField, then
Option<T> will have the same size as T and
will be internally represented as just a T (or T* in the case of
a reference T&).
The following types T, when stored in an
Option<T>, will have the same
size as the original type T:
const T&orT&(have the same size asconst T*orT*)ptr::NonNull<U>Box<T>Choice
This is called the "NeverValueField optimization", but is also called the "null pointer optimization" or NPO in Rust.
Reference parameters
As mentioned above Option type can hold a
reference, which allows code to use Option<const T&> or Option<T&>
as a function parameter in place of const Option<T>& or Option<T>&.
This can have a positive impact on compiler optimizations (and codegen
size) as the function is receiving the Option
by value and thus the compiler can reason locally about the
Option's state. Otherwise
it needs to assume any function call can change the const Option<T>& to
become empty/non-empty. This is a common optimization pitfall with
std::optional.
As an example, this code is optimized poorly,
keeping a runtime check on
the Option. Global analysis could perhaps show it
not required, but it is beyond the view of the compiler.
void Foo(const Option<T>& t) {
if (t.is_some()) {
A(); // Compiler assumes `t` may be changed.
B(t->thingy); // Compiler has to keep the check if `t` is Some.
}
}
Whereas here the compiler can elide runtime checks, and the parameter's size
is still the same as a pointer.
Use the as_ref method to convert to
Option<const T&> in the caller.
void Foo(Option<const T&> t) {
if (t.is_some()) {
A(); // Compiler knows `t` is not changed.
B(t->thingy); // Compiler drops the redundant check if `t` is Some.
}
}
Querying the variant
The is_some and
is_none methods return
true if the Option is
holding a value or not, respectively.
Adapters for working with lvalues
The following methods allow you to create an
Option that refers to the value
held in an lvalue, without copying or moving from the lvalue:
as_refconverts from a const lvalueOption<T>to an rvalueOption<const T&>`.as_mutconverts from a mutable lvalueOption<T>to an rvalueOption<T&>.takemoves the element out of the lvalueOption<T>into an rvalueOption<T>, leaving the lvalue empty.
Extracting the contained value
These methods extract the contained value in an Option<T> when it is
holding a value.
For working with the option as an lvalue:
as_valuereturns const reference access to the inner value. It will panic with a generic message when empty.as_value_mutreturns mutable reference access to the inner value. It will panic with a generic message when empty.operator*returns mutable reference access to the inner value. It will panic with a generic message when empty.operator->returns mutable pointer access to the inner value. It will panic with a generic message when empty.
For working with the option as an rvalue (when it returned from a function call):
expectmoves and returns the inner value. It will panic with a provided custom message when empty.unwrapmoves and returns the inner value. It will panic with a generic message with empty.unwrap_ormoves and returns the inner value. It will returns the provided default value instead when empty.unwrap_or_defaultmoves and returns the inner value. It will return the default value of the typeT(which must satisfyDefault) when empty.unwrap_or_elsemoves and returns the inner value. It will return the result of evaluating the provided function when empty.
Copying
Most methods of Option act on an rvalue and consume the Option to
transform it into a new Option with a new value. This ensures that the
value inside an Option is moved while transforming it.
However, if Option is
Copy, then the majority of methods offer an
overload to be called as an lvalue, in which case the
Option will copy
itself, and its contained value, and perform the intended method on the copy
instead. This can have performance implications!
The unwrapping methods are excluded from this, and are only available on an
rvalue Option to avoid copying just to access
the inner value. To do that, access the inner value as a reference through
as_value and
as_value_mut or through
operator* and
operator->.
Transforming contained values
These methods transform Option to
Result:
ok_ortransformsSome(v)toOk(v), andNonetoErr(err)using the provided default err value.ok_or_elsetransformsSome(v)toOk(v), andNoneto a value ofErrusing the provided function.transposetransposes anOptionof aResultinto aResultof anOption.
These methods transform an option holding a value:
filtercalls the provided predicate function on the contained valuetif theOptionisSome(t), and returnsSome(t)if the function returnstrue; otherwise, returnsNone.flattenremoves one level of nesting from anOption<Option<T>>.maptransformsOption<T>toOption<U>by applying the provided function to the contained value ofSomeand leavingNonevalues unchanged.
These methods transform Option<T> to a value
of a possibly different type U:
map_orapplies the provided function to the contained value ofSome, or returns the provided default value if theOptionisNone.map_or_elseapplies the provided function to the contained value ofSome, or returns the result of evaluating the provided fallback function if theOptionisNone.
These methods combine the Some variants of two Option values:
zipreturnsSome(Tuple<S, O>(s, o)))if theOptionisSome(s)and the method is called with anOptionvalue ofSome(o); otherwise, returnsNone- TODO:
zip_withcalls the provided functionfand returnsSome(f(s, o))if theOptionisSome(s)and the method is called with anOptionvalue ofSome(o); otherwise, returnsNone.
Boolean operators
These methods treat the Option as a boolean value,
where Some acts like true and None acts like false. There are two
categories of these methods: ones that take an
Option as input, and ones
that take a function as input (to be lazily evaluated).
The and_that,
or_that,
and xor_that methods take
another Option as input, and produce an
Option as output.
Only the and_that
method can produce an Option<U> value having a
different inner type U than Option<T>.
| method | self | input | output |
|---|---|---|---|
and_that |
None | (ignored) | None |
and_that |
Some(x) | None | None |
and_that |
Some(x) | Some(y) | Some(y) |
or_that |
None | None | None |
or_that |
None | Some(y) | Some(y) |
or_that |
Some(x) | (ignored) | Some(x) |
xor_that |
None | None | None |
xor_that |
None | Some(y) | Some(y) |
xor_that |
Some(x) | None | Some(x) |
xor_that |
Some(x) | Some(y) | None |
The and_then and
or_else methods take a function
as input, and only evaluate the function when they need to produce a new
value. Only the and_then method
can produce an Option<U> value having a
different inner type U than Option<T>.
| method | self | function input | function result | output |
|---|---|---|---|---|
and_then |
None | (not provided) | (not evaluated) | None |
and_then |
Some(x) | x | None | None |
and_then |
Some(x) | x | Some(y) | Some(y) |
or_else |
None | (not provided) | None | None |
or_else |
None | (not provided) | Some(y) | Some(y) |
or_else |
Some(x) | (not provided) | (not evaluated) | Some(x) |
This is an example of using methods like
and_then and
or_that in a pipeline of
method calls. Early stages of the pipeline pass failure values (None)
through unchanged, and continue processing on success values (Some).
Toward the end, or substitutes an error message if it receives None.
auto to_string = [](u8 u) -> sus::Option<std::string> {
switch (uint8_t{u}) { // switch requires a primitive.
case 20u: return sus::some("foo");
case 42u: return sus::some("bar");
default: return sus::none();
}
};
auto res =
sus::Vec<u8>(0_u8, 1_u8, 11_u8, 200_u8, 22_u8)
.into_iter()
.map([&](auto x) {
// `checked_sub()` returns `None` on error.
return x.checked_sub(1_u8)
// same with `checked_mul()`.
.and_then([](u8 x) { return x.checked_mul(2_u8); })
// `to_string` returns `None` on error.
.and_then([&](u8 x) { return to_string(x); })
// Substitute an error message if we have `None` so far.
.or_that(sus::some(std::string("error!")))
// Won't panic because we unconditionally used `Some` above.
.unwrap();
})
.collect<Vec<std::string>>();
sus_check(res == sus::vec("error!", "error!", "foo", "error!", "bar"));
Restrictions on returning references
Methods that return references are only callable on an rvalue
Option if the
Option is holding a reference. If the
Option is holding a non-reference
type, returning a reference from an rvalue
Option would be giving a reference to a
short-lived object which is a bugprone pattern in C++ leading to
memory-safety bugs.
Functions
-
Used to construct an option with a None value.
-
Used to construct an option with a Some(t) value.
Operators
-
Compares two options. This function requires that
Tis ordered. An empty option always compares less than a non-empty option. -
Satisfies the
Eq<Option<U>>concept.
Function Aliases
-
Implicit for-ranged loop iteration via
Option::iter. -
Implicit for-ranged loop iteration via
Option::iter.