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 |