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