A proposal to add convenience functions to std::optional
Anders Schau Knatten (anders@knatten.org)
This proposal adds three convenience functions to std::optional. They all aim to eliminate a lot of if (x.has_value()) conditionals in user code, in the same way that value_or() does. The interface is based on Rust's Option type, which is a core and very popular type in Rust.
The proposal requires no changes to core language, and breaks no existing code. The target is programmers at all levels, from novices to experts.
WARNING: This is a work in progress. In particular, I'm not happy with the name transform_optional.
A reference implementation with unit tests and demonstration code can be found at https://github.com/knatten/optional_proposal. That is also the current home of this proposal.
Note: If this proposal gathers support, similar functions should be added to std::expected, proposed in P0323R3. I'll defer that work until this proposal has been discussed more widely, to narrow the scope a bit.
Calling a function on an optional object currently requires the user to first check whether the object has a value. This proposal extends std::optional with three functions that eliminates this need.
Imagine user code integrating with Twitter. It has a types tweet and author, and the following functions:
optional<tweet> find_first(const string& search_string);
author lookup_author(const tweet& t);
If we want to search for a tweet, and get the author if a tweet was found, we currently have to do:
auto foo = find_first("foo");
auto foo_author(foo.has_value() ? lookup_author(*foo) : optional<author>());
The first function added by this proposal is std::optional::transform() (corresponding to Rust's map()), which takes a unary operation T->U and returns an new optional<U> that:
- If the original
optional<T>was empty, is empty - If the original
optional<T>object had a value, calls the unary operation on that value, and returns anoptional<U>containing the resulting value
It's easier to demonstrate in code:
auto foo_author = find_first("foo").transform(lookup_author);
These chain nicely, so you can do this:
auto result = some_optional
.transform(function1)
.transform(function2)
.transform([](const T& t){/**/});
The proposal also contains two more functions, which are discussed in more detail below
transform_optional(), which is liketransformbut takes an operationT->optional<U>instead.call(), which takes an operationT->Uwhich, if there is a current value, calls the operation with that value. The return valueUis ignored, andUcan bevoid.
This does not depend on anything else than C++17 std::optional, and can be implemented in the current standard.
Following is a list of things I either considered doing, or that were suggested to me, which were discarded.
- Someone suggested merging
transformandtransform_optionalinto one functiontransform, that wraps the result ofopinto anoptionalonly ifopdoesn't already return an optional. While this could be convenient, it results intransformnot having a consistent type. It would make the type oftransformbe(T->U)->optional<U>in most cases, but(T->U)->UwhenUis itselfoptional.
I investigated the possibility of marking any of the proposed functions noexcept. I decided to not mark any of them as such.
-
transformcan not benoexcept, sinceoptional(U&& v)is notnoexcept. -
transform_optionalcould be conditionallynoexceptfor the rvalue overloads, asoptional's move constructor isnoexceptwhenT's move constructor isnoexcept. However, note that fortransform_optional,opis itself expected to construct anoptional, meaning thatopwould rarely benoexcept. -
callcould be conditionallynoexcept.
Due to the guidelines in N3279 discouraging the use of conditional noexcept outside swap/move, I decided against adding any noexcept specifier.
template <class UnaryOperation>
constexpr optional<U> transform(UnaryOperation op) &
template <class UnaryOperation>
constexpr optional<U> transform(UnaryOperation op) const&
op is a function T->U
Returns: optional<U>(op(*val)) if *this has a value, otherwise optional<U>()
Remarks: If is_trivially_copy_constructible_v<T> is true and op(*val) is constexpr, this function shall be constexpr.
template <class UnaryOperation>
constexpr optional<U> transform(UnaryOperation op) &&
template <class UnaryOperation>
constexpr optional<U> transform(UnaryOperation op) const&&
op is a function T->U
Returns: optional<U>(op(std::move(*val))) if *this has a value, otherwise optional<U>()
Remarks: If is_trivially_move_constructible_v<T> is true and op(std::move(*val)) is constexpr, this function shall be constexpr.
template <class UnaryOperation>
constexpr optional<U> transform_optional(UnaryOperation op) &
template <class UnaryOperation>
constexpr optional<U> transform_optional(UnaryOperation op) const&
op is a function T->optional<U>
Returns: op(*val) if *this has a value, otherwise optional<U>()
Remarks: If is_trivially_copy_constructible_v<T> is true and op(*val) is constexpr, this function shall be constexpr.
template <class UnaryOperation>
constexpr optional<U> transform_optional(UnaryOperation op) &&
template <class UnaryOperation>
constexpr optional<U> transform_optional(UnaryOperation op) const&&
op is a function T->optional<U>
Returns: op(std::move(*val)) if *this has a value, otherwise optional<U>()
Remarks: If is_trivially_move_constructible_v<T> is true and op(std::move(*val)) is constexpr, this function shall be constexpr.
template <class UnaryOperation>
constexpr void call(UnaryOperation op) &
template <class UnaryOperation>
constexpr void call(UnaryOperation op) const&
op is a function T->U
Calls op(*val) if *this has a value.
Remarks: If op(*val) is constexpr, this function shall be constexpr.
template <class UnaryOperation>
constexpr void call(UnaryOperation op) &&
template <class UnaryOperation>
constexpr void call(UnaryOperation op) const&&
op is a function T->U
Calls op(std::move(*val)) if *this has a value.
Remarks: If op(std::move(*val)) is constexpr, this function shall be constexpr.