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);
}
1 Answer 1
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 c++14, so I will assume you want implement it under C++14.
Here's some suggestions on the code.
Your code is missing a trailing
}
to end the namespacefub
.Consider wrapping your utility into a header. Add include guards.
The conversion operator to
bool
should be markedexplicit
. This still allows it to be contextually converted tobool
, but not otherwise implicitly converted in undesired situations.
-
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 even0
, etc. Anoptional<reference_wrapper>
, as I prefer to use, clearly states 'this is a non-owning observer, which might not exist'. \$\endgroup\$underscore_d– underscore_d2019年06月15日 14:12:59 +00:00Commented Jun 15, 2019 at 14:12 -
\$\begingroup\$ @underscore_d Agreed. Updated. \$\endgroup\$L. F.– L. F.2019年06月15日 14:29:03 +00:00Commented Jun 15, 2019 at 14:29
std::optional
is a C++17 thing. Are you implementingoptional_ref
under C++14? \$\endgroup\$