diff options
author | Alexey Rusakov <Kitsune-Ral@users.sf.net> | 2022-01-23 17:11:22 +0100 |
---|---|---|
committer | Alexey Rusakov <Kitsune-Ral@users.sf.net> | 2022-01-23 17:12:08 +0100 |
commit | abbab8d8f8c566bc2c9cdf766c6fbb11d978ca47 (patch) | |
tree | 07623282baa91122528a9b553370f8bb6195bfa0 /lib/omittable.h | |
parent | 63c953800330017ebb2afbabf41e5c4932c4d640 (diff) | |
download | libquotient-abbab8d8f8c566bc2c9cdf766c6fbb11d978ca47.tar.gz libquotient-abbab8d8f8c566bc2c9cdf766c6fbb11d978ca47.zip |
Omittable: split out from util.h and refresh
Improvements:
- Quotient::lift() - a way to invoke a function on an optional (including
Omittable) or a pointer if it's 'truthy'. Doesn't need enhanced
function_traits<>, only the standard library; works on any number
of arguments that can be dereferenced and casted to bool.
- then() - the version of lift() as a member function.
- edit() was renamed to ensure() (edit() might become a read-write
counterpart of then() at some point). It's not really used across
libQuotient codebase (or elsewhere) but is staying there just in case.
It can also accept an initializer, removing the requirement of
default-constructibility.
- Quotient::merge() is simplified, with one universal implementation
covering both Omittable/optional and plain values.
- All that now lives in its dedicated pair of files, further
decluttering util.h
Diffstat (limited to 'lib/omittable.h')
-rw-r--r-- | lib/omittable.h | 223 |
1 files changed, 223 insertions, 0 deletions
diff --git a/lib/omittable.h b/lib/omittable.h new file mode 100644 index 00000000..b5efecf5 --- /dev/null +++ b/lib/omittable.h @@ -0,0 +1,223 @@ +// 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... MaybeTs> +inline auto lift(FnT&& fn, MaybeTs&&... args) +{ + 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). + * - disabled non-const lvalue operator*() and operator->(), as it's too easy + * to inadvertently cause a value change through them. + * - ensure() to provide a safe and explicit lvalue accessor instead of + * those above. 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 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; + } + + // Hide non-const lvalue operator-> and operator* as these are + // a bit too surprising: value() & doesn't lazy-create an object; + // and it's too easy to inadvertently change the underlying value. + + const value_type* operator->() const& { return base_type::operator->(); } + value_type* operator->() && { return base_type::operator->(); } + const value_type& operator*() const& { return base_type::operator*(); } + value_type& operator*() && { return base_type::operator*(); } + + // 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 |