| 1 | // SPDX-FileCopyrightText: 2018 Kitsune Ral <kitsune-ral@users.sf.net> |
| 2 | // SPDX-License-Identifier: LGPL-2.1-or-later |
| 3 | |
| 4 | #pragma once |
| 5 | |
| 6 | #include <optional> |
| 7 | #include <functional> |
| 8 | |
| 9 | namespace Quotient { |
| 10 | |
| 11 | template <typename T> |
| 12 | class Omittable; |
| 13 | |
| 14 | constexpr auto none = std::nullopt; |
| 15 | |
| 16 | //! \brief Lift an operation into dereferenceable types (Omittables or pointers) |
| 17 | //! |
| 18 | //! This is a more generic version of Omittable::then() that extends to |
| 19 | //! an arbitrary number of arguments of any type that is dereferenceable (unary |
| 20 | //! operator*() can be applied to it) and (explicitly or implicitly) convertible |
| 21 | //! to bool. This allows to streamline checking for nullptr/none before applying |
| 22 | //! the operation on the underlying types. \p fn is only invoked if all \p args |
| 23 | //! are "truthy" (i.e. <tt>(... && bool(args)) == true</tt>). |
| 24 | //! \param fn A callable that should accept the types stored inside |
| 25 | //! Omittables/pointers passed in \p args |
| 26 | //! \return Always an Omittable: if \p fn returns another type, lift() wraps |
| 27 | //! it in an Omittable; if \p fn returns an Omittable, that return value |
| 28 | //! (or none) is returned as is. |
| 29 | template <typename FnT> |
| 30 | inline auto lift(FnT&& fn, auto&&... args) |
| 31 | { |
| 32 | if constexpr (std::is_void_v<std::invoke_result_t<FnT, decltype(*args)...>>) { |
| 33 | if ((... && bool(args))) |
| 34 | std::invoke(std::forward<FnT>(fn), *args...); |
| 35 | } else |
| 36 | return (... && bool(args)) |
| 37 | ? Omittable(std::invoke(std::forward<FnT>(fn), *args...)) |
| 38 | : none; |
| 39 | } |
| 40 | |
| 41 | /** `std::optional` with tweaks |
| 42 | * |
| 43 | * The tweaks are: |
| 44 | * - streamlined assignment (operator=)/emplace()ment of values that can be |
| 45 | * used to implicitly construct the underlying type, including |
| 46 | * direct-list-initialisation, e.g.: |
| 47 | * \code |
| 48 | * struct S { int a; char b; } |
| 49 | * Omittable<S> o; |
| 50 | * o = { 1, 'a' }; // std::optional would require o = S { 1, 'a' } |
| 51 | * \endcode |
| 52 | * - entirely deleted value(). The technical reason is that Xcode 10 doesn't |
| 53 | * have it; but besides that, value_or() or (after explicit checking) |
| 54 | * `operator*()`/`operator->()` are better alternatives within Quotient |
| 55 | * that doesn't practice throwing exceptions (as doesn't most of Qt). |
| 56 | * - merge() - a soft version of operator= that only overwrites its first |
| 57 | * operand with the second one if the second one is not empty. |
| 58 | * - then() and then_or() to streamline read-only interrogation in a "monadic" |
| 59 | * interface. |
| 60 | */ |
| 61 | template <typename T> |
| 62 | class Omittable : public std::optional<T> { |
| 63 | public: |
| 64 | using base_type = std::optional<T>; |
| 65 | using value_type = std::decay_t<T>; |
| 66 | |
| 67 | using std::optional<T>::optional; |
| 68 | |
| 69 | // Overload emplace() and operator=() to allow passing braced-init-lists |
| 70 | // (the standard emplace() does direct-initialisation but |
| 71 | // not direct-list-initialisation). |
| 72 | using base_type::operator=; |
| 73 | Omittable& operator=(const value_type& v) |
| 74 | { |
| 75 | base_type::operator=(v); |
| 76 | return *this; |
| 77 | } |
| 78 | Omittable& operator=(value_type&& v) |
| 79 | { |
| 80 | base_type::operator=(std::move(v)); |
| 81 | return *this; |
| 82 | } |
| 83 | |
| 84 | using base_type::emplace; |
| 85 | T& emplace(const T& val) { return base_type::emplace(val); } |
| 86 | T& emplace(T&& val) { return base_type::emplace(std::move(val)); } |
| 87 | |
| 88 | // Use value_or() or check (with operator! or has_value) before accessing |
| 89 | // with operator-> or operator* |
| 90 | // The technical reason is that Xcode 10 has incomplete std::optional |
| 91 | // that has no value(); but using value() may also mean that you rely |
| 92 | // on the optional throwing an exception (which is not an assumed practice |
| 93 | // throughout Quotient) or that you spend unnecessary CPU cycles on |
| 94 | // an extraneous has_value() check. |
| 95 | auto& value() = delete; |
| 96 | const auto& value() const = delete; |
| 97 | |
| 98 | //! Merge the value from another Omittable |
| 99 | //! \return true if \p other is not omitted and the value of |
| 100 | //! the current Omittable was different (or omitted), |
| 101 | //! in other words, if the current Omittable has changed; |
| 102 | //! false otherwise |
| 103 | template <typename T1> |
| 104 | auto merge(const std::optional<T1>& other) |
| 105 | -> std::enable_if_t<std::is_convertible_v<T1, T>, bool> |
| 106 | { |
| 107 | if (!other || (this->has_value() && **this == *other)) |
| 108 | return false; |
| 109 | this->emplace(*other); |
| 110 | return true; |
| 111 | } |
| 112 | |
| 113 | // The below is inspired by the proposed std::optional monadic operations |
| 114 | // (http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p0798r6.html). |
| 115 | |
| 116 | //! \brief Lift a callable into the Omittable |
| 117 | //! |
| 118 | //! 'Lifting', as used in functional programming, means here invoking |
| 119 | //! a callable (e.g., a function) on the contents of the Omittable if it has |
| 120 | //! any and wrapping the returned value (that may be of a different type T2) |
| 121 | //! into a new Omittable\<T2>. If the current Omittable is empty, |
| 122 | //! the invocation is skipped altogether and Omittable\<T2>{none} is |
| 123 | //! returned instead. |
| 124 | //! \note if \p fn already returns an Omittable (i.e., it is a 'functor', |
| 125 | //! in functional programming terms), then() will not wrap another |
| 126 | //! Omittable around but will just return what \p fn returns. The |
| 127 | //! same doesn't hold for the parameter: if \p fn accepts an Omittable |
| 128 | //! you have to wrap it in another Omittable before calling then(). |
| 129 | //! \return `none` if the current Omittable has `none`; |
| 130 | //! otherwise, the Omittable returned from a call to \p fn |
| 131 | //! \tparam FnT a callable with \p T (or <tt>const T&</tt>) |
| 132 | //! returning Omittable<T2>, T2 is any supported type |
| 133 | //! \sa then_or |
| 134 | template <typename FnT> |
| 135 | auto then(FnT&& fn) const |
| 136 | { |
| 137 | return lift(std::forward<FnT>(fn), *this); |
| 138 | } |
| 139 | |
| 140 | //! \brief Lift a callable into the rvalue Omittable |
| 141 | //! |
| 142 | //! This is an rvalue overload for then(). |
| 143 | template <typename FnT> |
| 144 | auto then(FnT&& fn) |
| 145 | { |
| 146 | return lift(std::forward<FnT>(fn), *this); |
| 147 | } |
| 148 | |
| 149 | //! \brief Lift a callable into the const lvalue Omittable, with a fallback |
| 150 | //! |
| 151 | //! This effectively does the same what then() does, except that it returns |
| 152 | //! a value of type returned by the callable (unwrapped from the Omittable), |
| 153 | //! or the provided fallback value if the resulting (or the current - then |
| 154 | //! the callable is not even touched) Omittable is empty. This is a typesafe |
| 155 | //! version to apply an operation on an Omittable without having to deal |
| 156 | //! with another Omittable afterwards. |
| 157 | template <typename FnT, typename FallbackT> |
| 158 | auto then_or(FnT&& fn, FallbackT&& fallback) const |
| 159 | { |
| 160 | return then(std::forward<FnT>(fn)) |
| 161 | .value_or(std::forward<FallbackT>(fallback)); |
| 162 | } |
| 163 | |
| 164 | //! \brief Lift a callable into the rvalue Omittable, with a fallback |
| 165 | //! |
| 166 | //! This is an overload for functions that accept rvalue |
| 167 | template <typename FnT, typename FallbackT> |
| 168 | auto then_or(FnT&& fn, FallbackT&& fallback) |
| 169 | { |
| 170 | return then(std::forward<FnT>(fn)) |
| 171 | .value_or(std::forward<FallbackT>(fallback)); |
| 172 | } |
| 173 | }; |
| 174 | |
| 175 | template <typename T> |
| 176 | Omittable(T&&) -> Omittable<T>; |
| 177 | |
| 178 | //! \brief Merge the value from an optional |
| 179 | //! This is an adaptation of Omittable::merge() to the case when the value |
| 180 | //! on the left hand side is not an Omittable. |
| 181 | //! \return true if \p rhs is not omitted and the \p lhs value was different, |
| 182 | //! in other words, if \p lhs has changed; |
| 183 | //! false otherwise |
| 184 | template <typename T1, typename T2> |
| 185 | inline auto merge(T1& lhs, const std::optional<T2>& rhs) |
| 186 | -> std::enable_if_t<std::is_assignable_v<T1&, const T2&>, bool> |
| 187 | { |
| 188 | if (!rhs || lhs == *rhs) |
| 189 | return false; |
| 190 | lhs = *rhs; |
| 191 | return true; |
| 192 | } |
| 193 | |
| 194 | } // namespace Quotient |
| 195 | |