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 | |