Browse Source

feat: implement out<T>/inout<T>

out<T>   - an optional out-parameter with fluent handling
inout<T> - a required out-parameter that is rvalue-friendly
Sam Jaffe 2 months ago
parent
commit
d071f0b987
1 changed files with 138 additions and 0 deletions
  1. 138 0
      include/pointers/out.hpp

+ 138 - 0
include/pointers/out.hpp

@@ -0,0 +1,138 @@
+//
+//  out.hpp
+//  pointer
+//
+//  Created by Sam Jaffe on 11/4/23.
+//
+//
+
+#pragma once
+
+#include <type_traits>
+#include <utility>
+#include <variant>
+
+namespace pointers {
+  constexpr struct discard_out_t {
+  } discard_out;
+
+  /**
+   * @brief An optional out-parameter to a function, similar to a function
+   * that takes `T* out_param = nullptr`. Unfortunately, std::optional does not
+   * support storing references - so if we want the syntactic sugar of that, we
+   * need a custom class.
+   *
+   * In addition to acting like an optional value - we have the special behavior
+   * that we wanted - namely that "if there is a contained value provided by the
+   * caller, we will update that reference", and "if there is no value, then
+   * assigning a value will have no effect" as if we performed the idiomatic:
+   * @code
+   * if (out_param) {
+   *   *out_param = ...;
+   * }
+   * @endcode
+   *
+   * @tparam T The type being returned
+   */
+  template <typename T> class out {
+  private:
+    static_assert(std::is_same_v<T, std::decay_t<T>>,
+                  "out<T> can only be value-types");
+    T * ref_ = nullptr;
+
+  public:
+    // Explicitly construct an empty out parameter, that has no side-effects
+    out(discard_out_t) {} // NOLINT NOSONAR
+    // Construct an out parameter pointing to the given concrete value.
+    out(T & value) : impl_(&value) {} // NOLINT NOSONAR
+
+    /**
+     * @breif On rare occasions, we still need to perform checks
+     * that an out-param holds a value.
+     */
+    explicit operator bool() const { return impl_; }
+
+    /**
+     * @brief Update the value of this out parameter, if it holds a value. By
+     * convention, we assume that this function will only be called once, but
+     * there is no requirement for that.
+     *
+     * @tparam U Any type that can be used to construct the held type T
+     *
+     * @param val The new value being passed up to the caller
+     *
+     * @returns Nothing - this object does not behave like a normal object where
+     * you can do things like `if ((A = B).foo())` - since this object
+     * represents exclusively a way to pass an optional value back to the caller
+     * without returning a tuple.
+     */
+    template <typename U,
+              typename = std::enable_if_t<std::is_constructible_v<T, U>>>
+    void operator=(U && value) {
+      if (ref_) { *ref_ = T(std::forward<U>(value)); }
+      return *this;
+    }
+  };
+
+  /**
+   * @brief A non-optional out-parameter to a function, similar to a function
+   * that takes a `T& out_param` argument. Unlike the standard form, this type
+   * allows passing an rvalue (temporary) or an lvalue (persistant) element, and
+   * will properly handle the assigment and updating of the object as
+   * appropriate.
+   *
+   * @tparam T The type being returned
+   */
+  template <typename T> class inout {
+  private:
+    static_assert(std::is_same_v<T, std::decay_t<T>>,
+                  "inout<T> can only be value-types");
+    std::variant<T, T *> ref_;
+
+  public:
+    // Constructs an inout parameter from an rvalue type - whose modification
+    // will not effect the calling scope.
+    inout(T && value) : ref_(std::move(value)) {}
+    // Constructs an inout parameter from an lvalue type - whose modification
+    // will propogate upwards to the caller.
+    inout(T & ref) : ref_(&ref) {}
+
+    /**
+     * @brief Convert this object back into its underlying type for use.
+     *
+     * @returns A reference to the contained value/reference, to avoid the cost
+     * of performing a copy-operation if the contained object is non-trivial.
+     */
+    operator T const &() const {
+      struct {
+        T const & operator()(T const & in) const { return in; }
+        T const & operator()(T * in) const { return *in; }
+      } visitor;
+      return std::visit(visitor, ref_);
+    }
+
+    /**
+     * @brief Update the value of this out parameter. Depending on the variation
+     * contained in this type, this will either propogate up to the caller's
+     * level or will update future uses of this object.
+     *
+     * @tparam U Any type that can be used to construct the held type T
+     *
+     * @param val The new value being set
+     *
+     * @returns The updated value
+     */
+    template <typename U>
+      requires std::is_constructible_v<T, U>
+    T const & operator=(U && val) {
+      struct {
+        U && val;
+        T const & operator()(T & in) const { return in = std::forward<U>(val); }
+        T const & operator()(T * in) const {
+          return *in = std::forward<U>(val);
+        }
+      } visitor{std::forward<U>(val)};
+      return std::visit(visitor, ref_);
+    }
+  };
+}