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);
}
std::optional
is a C++17 thing. Are you implementingoptional_ref
under C++14? \$\endgroup\$