12
\$\begingroup\$

I omitted all free operators but the equality comparisons ones because of verbosity. I am glad about any comments and improvements.

Motivation

I know that optional references are equivalent to pointers and their implementation is just such a wrapper. But I believe they make sense in some circumstances and please correct me if I am wrong.

While implementing and using my interval map I needed to return an optional reference in one of its accessor methods, namely operator[](Key). The idea is to get a reference to a stored value or nothing. Thus I did it there with an std::optional<std::reference_wrapper<const T>>. Following this pattern of optional refs led to complications when I tried accessing an std::optional<std::reference_wrapper<T>> at some other point of time. I couldn't write something like this

int a = 0;
std::optional<std::reference_wrapper<int>> opt{a};
*opt = 42;
assert(a == 42);

but instead one has to explicitly unwrap the reference_wrapper like this

opt.value().get() = 42;
assert(a == 42);

or

opt->get() = 42
assert(a == 42);

And my motivation for optional_ref was born.

Source code

Here is the complete source code on wandbox.

optional_ref.hpp

#include <type_traits>
#include <cassert>
#include <stdexcept>

#include "range/v3/utility/concepts.hpp"

namespace fub
{

struct bad_optional_access : std::runtime_error {
  using runtime_error::runtime_error;
};

template <typename T>
  class optional_ref {
  public:
    using element_type = T;
    using reference = element_type&;
    using pointer = element_type*;

    // CONSTRUCTORS

    optional_ref() = default;

    template <typename S, 
        CONCEPT_REQUIRES_(ranges::ConvertibleTo<S*, T*>())>
      constexpr optional_ref(const optional_ref<S>& other)
      noexcept
      : m_pointer{other.get_pointer()} {}

    constexpr optional_ref(reference element)
    noexcept
    : m_pointer{std::addressof(element)} {}

    // ASSIGNMENT

    template <typename S, 
        CONCEPT_REQUIRES_(ranges::ConvertibleTo<S*, T*>())>
      constexpr optional_ref&
      operator=(const optional_ref<S>& other)
      noexcept
      {
        m_pointer = other.get_pointer();
      }

    // DESTRUCTOR

    ~optional_ref() = default;

    // SWAP

    void swap(optional_ref& other)
    noexcept
    {
      swap(m_pointer, other.m_pointer);
    }

    // OBSERVERS

    constexpr bool
    has_value() const
    noexcept
    { return m_pointer != nullptr; }

    constexpr pointer
    get_pointer() const
    noexcept
    { return m_pointer; }

    constexpr reference
    value() const
    {
      if (!has_value()) {
        throw bad_optional_access(
          "fub::optional_ref::value: optional_ref is empty."
        );
      }
      return *get_pointer();
    }

    template <typename U,
        CONCEPT_REQUIRES_(ranges::ConvertibleTo<U, reference>())>
      constexpr reference
      value_or(U&& alternative) const
      noexcept
      {
        if (has_value()) {
          return *get_pointer();
        }
        return alternative;
      }

    constexpr pointer
    operator->() const
    noexcept
    {
      assert(has_value());
      return get_pointer();
    }

    constexpr reference
    operator*() const
    noexcept
    {
      assert(has_value());
      return *get_pointer();
    }

    constexpr operator
    bool() const
    noexcept
    { return has_value(); }

    // MODIFIERS

    void reset()
    noexcept
    {
      m_pointer = nullptr;
    }

  private:
    pointer m_pointer{nullptr};
  };

// EQUALITY COMPARISON

template <typename T, typename S,
    CONCEPT_REQUIRES_(ranges::EqualityComparable<T, S>())>
  bool operator==(optional_ref<T> left, optional_ref<S> right)
  noexcept(noexcept(*left == *right))
  {
    return (!left && !right) || (left && right && *left == *right);
  }

template <typename T, typename S,
    CONCEPT_REQUIRES_(ranges::EqualityComparable<T, S>())>
  bool operator!=(optional_ref<T> left, optional_ref<S> right)
  noexcept(noexcept(left == right))
  {
    return !(left == right);
  }

// EQUALITY COMPARISON WITH T

template <typename T, typename S,
    CONCEPT_REQUIRES_(ranges::EqualityComparable<T, S>())>
  bool operator==(optional_ref<T> left, const S& right)
  noexcept(noexcept(*left == right))
  {
    return left && *left == right;
  }

template <typename T, typename S,
    CONCEPT_REQUIRES_(ranges::EqualityComparable<T, S>())>
  bool operator==(const T& left, optional_ref<S> right)
  noexcept(noexcept(left == *right))
  {
    return right && left == *right;
  }


template <typename T, typename S,
    CONCEPT_REQUIRES_(ranges::EqualityComparable<T, S>())>
  bool operator!=(optional_ref<T> left, const S& right)
  noexcept(noexcept(left == right))
  {
    return !(left == right);
  }

template <typename T, typename S,
    CONCEPT_REQUIRES_(ranges::EqualityComparable<T, S>())>
  bool operator!=(const T& left, optional_ref<S> right)
  noexcept(noexcept(left == right))
  {
    return !(left == right);
  }

Some tests (see also on wandbox)

TEST_CASE("optional refs are regular types")
{
  REQUIRE(ranges::Regular<optional_ref<int>>());
  REQUIRE(ranges::Regular<optional_ref<std::unique_ptr<int>>>());
  REQUIRE(ranges::Regular<optional_ref<char[30]>>());
  REQUIRE(ranges::Regular<optional_ref<double[]>>());
}

TEST_CASE("compare const int refs with int refs")
{
  int a = 42;
  int b = 24;
  const int c = 42;

  optional_ref<int> ref_a = a;
  optional_ref<const int> ref_x;
  REQUIRE(!ref_x);
  REQUIRE(!ref_x.has_value());
  REQUIRE(ref_a != ref_x);
  ref_x = c;
  REQUIRE(ref_a == ref_x);
  REQUIRE(ref_a.get_pointer() != ref_x.get_pointer());
  ref_x = b;
  REQUIRE(ref_a != ref_x);
  ref_x = a;
  REQUIRE(ref_a.get_pointer() == ref_x.get_pointer());
}

TEST_CASE("Arrow operator works with objects")
{
  struct A {
    int foo;
    char bar;
  };

  A a{4, '2'};

  optional_ref<A> ref{a};
  REQUIRE(ref->foo == 4);
  REQUIRE(ref->bar == '2');
}

void access_throws(optional_ref<const int> ref)
{
  REQUIRE_THROWS_AS(ref.value(), bad_optional_access);
}

TEST_CASE("throw error on using empty optional_ref")
{
  auto ref = optional_ref<int>{};
  access_throws(ref);
}

TEST_CASE("assign reference")
{
  int a = 0;
  optional_ref<int> ref{a};
  REQUIRE(ref == 0);
  *ref = 42;
  REQUIRE(ref == 42);
}
\$\endgroup\$
1
  • \$\begingroup\$ std::optional is a C++17 thing. Are you implementing optional_ref under C++14? \$\endgroup\$
    – L. F.
    Jun 13, 2019 at 9:41

1 Answer 1

2
\$\begingroup\$

Your code seems really nice to me. It is well structured, and the use of ranges and concepts increase its diagnosing ability.

As I mentioned in my comment, std::optional is really a C++17 thing. You tagged your question , so I will assume you want implement it under C++14.

Here's some suggestions on the code.

  1. Your code is missing a trailing } to end the namespace fub.

  2. Consider wrapping your utility into a header. Add include guards.

  3. The conversion operator to bool should be marked explicit. This still allows it to be contextually converted to bool, but not otherwise implicitly converted in undesired situations.

\$\endgroup\$
2
  • 1
    \$\begingroup\$ "why not simply use a pointer?" Because it may be seen by many readers as not clearly enough conveying its (lack of) ownership semantics, may inadvertently have pointer arithmetic applied on it, can't be overloaded separately from other pointers and might be accidentally created by conversion from other pointers or nullptr or worse even 0, etc. An optional<reference_wrapper>, as I prefer to use, clearly states 'this is a non-owning observer, which might not exist'. \$\endgroup\$ Jun 15, 2019 at 14:12
  • \$\begingroup\$ @underscore_d Agreed. Updated. \$\endgroup\$
    – L. F.
    Jun 15, 2019 at 14:29

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.