This is about decorating callables by making their argument and return value to be a std::optional. Therefore I created the template function make_skippable that takes a callable and returns a wrapping lambda having an optional argument and an optional return value.
Here a simple example of how to use this function:
auto callable = [](auto arg) { return arg; };
auto skippable_callable = make_skippable(callable);
std::optional<int> res1 = skippable_callable(static_cast<int>(1));
std::optional<int> res2 = skippable_callable(std::optional<int>{2});
std::optional<int> res3 = skippable_callable(std::optional<int>{});
The purpose of this functionality is to allow callables to be composed as a chain, whereby in the event that a callable doesn’t produce a result (by returning a std::nullopt), the chain breaks by skipping the processing of the subsequent callables (unless the callable is able to handle a std::nullopt). I will follow up on the compose implementation later in a separate review request.
Below you will find the detailed requirements that the make_skippable implementation should fulfil:
- wrap a callable regardless of whether it already has an optional argument or an optional return value
- in case a nullopt argument occurs, only skip a callable that has a value argument, instead execute a callable that has an optional argument by forwarding the nullopt
- support all kinds callables having a single argument, including generic lambdas
- support perfect forwarding regarding argument and callable object
- c++17
Here the implementation itself:
template <typename>
struct IsOptional : std::false_type {};
template <typename T>
struct IsOptional<std::optional<T>> : std::true_type {};
template <typename TArg>
auto ensure_optional(TArg&& arg) {
if constexpr (IsOptional<std::decay_t<TArg>>::value) {
return std::forward<TArg>(arg);
} else {
return std::make_optional(std::forward<TArg>(arg));
}
}
template <typename TFn>
auto make_skippable(TFn&& fn) {
return [fn = std::forward<TFn>(fn)](auto&& optional_or_value) {
auto&& optional_arg = ensure_optional(std::forward<decltype(optional_or_value)>(optional_or_value));
using OptionalArg = decltype(optional_arg);
constexpr bool invoke_with_optional = std::is_invocable_v<TFn, OptionalArg>;
if constexpr (invoke_with_optional) {
auto&& res = fn(std::forward<OptionalArg>(optional_arg));
return ensure_optional(std::forward<decltype(res)>(res));
}
const bool invoke_with_value = optional_arg.has_value();
if (invoke_with_value) {
auto&& value = std::forward<OptionalArg>(optional_arg).value();
auto&& res = fn(std::forward<decltype(value)>(value));
return ensure_optional(std::forward<decltype(res)>(res));
}
// skip fn
using ValueArg = typename std::decay_t<OptionalArg>::value_type;
using Res = typename std::invoke_result<TFn, ValueArg>::type;
if constexpr (IsOptional<Res>::value) {
return Res{};
} else {
return std::optional<Res>{};
}
};
}
Kudos to Jonathan Boccara who inspired me with his blog post The Optional Monad In C++, Without the Ugly Stuff - Fluent C++.
Happy to receive any kind of feedback :-)
This godbolt link refers to the latest version of the implementation and does also include tests.