4
\$\begingroup\$

Here is a good article on an optional reference type in C++. They discuss std::optional<T&>, but as that doesn't compile I have made my own.

One purpose of this type is to remove raw pointers from function signatures (where references alone cannot be used), as a raw pointer does not convey any indication of what it will be used for (become owned, deleted, iterated, dereferenced, etc).

#include <functional>
#include <optional>

template<typename T>
/** @brief An optional refference **/
class opt_ref {
    using std_opt_ref = std::optional<std::reference_wrapper<T>>;
    std_opt_ref data = std::nullopt;
public:
    using type = typename std::reference_wrapper<T>::type;

    /** public member functions **/

    T& get() { return data.value().get(); }
    const T& get() const { return data.value().get(); }
    bool has_value() const { return data.has_value(); }
    T& value_or(T&& other) const { return data.value_or(other); }

    /** constructors **/

    opt_ref() {}
    opt_ref(T& source) : data(source) {}
    opt_ref& operator = (T&& other) { data.value().get() = other; return *this; }

    /** comparisons **/

    bool operator == (const T& t) { return data.value() == t; }
    bool operator == (const std::nullopt_t&) {return !data.has_value(); }

    /** implicit conversion **/

    operator T&() { return data.value().get(); }
    operator const T&() const { return data.value().get(); }
    operator std::reference_wrapper<T>() { return data.value(); }
    operator const std::reference_wrapper<T>() const { return data.value(); }
};
\$\endgroup\$
3

1 Answer 1

2
\$\begingroup\$

Design

References are different from pointers in two ways:

  • they are designed to be aliases to the objects they refer to, so syntactically they are treated with special care;

  • they cannot be rebound.

You cannot always emulate the first bullet with an optional — for example, there's no general way to make opt.f() call opt.value().f(). You still have to resort to some other syntax like opt->value(). Therefore, my advice is to simply treat opt_ref<T> like an immutable nullable pointer that does not own the referred-to object — don't follow std::reference_wrapper.

Code Review

using type = typename std::reference_wrapper<T>::type;

typename std::reference_wrapper<T>::type is just T. Also, the standard terminology is value_type.

T& get() { return data.value().get(); }
const T& get() const { return data.value().get(); }
bool has_value() const { return data.has_value(); }
T& value_or(T&& other) const { return data.value_or(other); }

has_value is noexcept. Why does value_or take an rvalue reference? To introduce dangling references as in opt.value_or(1)? Take an lvalue reference instead.

bool operator == (const T& t) { return data.value() == t; }
bool operator == (const std::nullopt_t&) {return !data.has_value(); }

I'm not sure this is the right approach. The first == compares values (and throws an exception if there is no value), whereas the second == compares the references themselves. You can imitate the behavior of std::optional:

bool operator==(const opt_ref<T>& a, const opt_ref<T>& b)
{
    if (a.has_value() != b.has_value()) {
        return false;
    } else if (!a.has_value()) { // and !b.has_value()
        return true;
    } else {
        return a.get() == b.get();
    }
}
operator T&() { return data.value().get(); }
operator const T&() const { return data.value().get(); }
operator std::reference_wrapper<T>() { return data.value(); }
operator const std::reference_wrapper<T>() const { return data.value(); }

As I said before: are you sure you want this (especially the implicit conversions to reference_wrapper)?

Other functionalities

Consider:

  • operator* and operator->;

  • explicit operator bool;

  • has_value;

  • ...

\$\endgroup\$

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.