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
9namespace Quotient {
10
11template <typename T>
12class Omittable;
13
14constexpr 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.
29template <typename FnT>
30inline 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 */
61template <typename T>
62class Omittable : public std::optional<T> {
63public:
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
175template <typename T>
176Omittable(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
184template <typename T1, typename T2>
185inline 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