3
\$\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\$
  • 1
    \$\begingroup\$ A strange article indeed. \$\endgroup\$ – bipll Mar 25 at 15:40
  • \$\begingroup\$ Here's another article for you: thephd.github.io/to-bind-and-loose-a-reference-optional It seems like you've implemented assign-through? Is that what you meant to do? \$\endgroup\$ – Quuxplusone Mar 25 at 19:15
  • \$\begingroup\$ Why optional reference? Why not simply pass a pointer? If you are worried over raw pointer unintended usage - just use observer_ptr which is a trivial pointer wrapper and is added to the standart in C++20. \$\endgroup\$ – ALX23z Mar 27 at 0:14
  • \$\begingroup\$ @ALX23z I don't believe it has been added to the standard, do you have a reference for that? Also, I think opt_ref is a better name than observer_ptr, but yes, they would be very similar apart from the use of the -> operator, prevention of polymorphic use, and ability to store different types in a container, etc. \$\endgroup\$ – Blue7 Mar 30 at 15:15
  • \$\begingroup\$ @Blue7 sorry I looked it up and observer_ptr is still in experimental. Sry. opt_ref is not good because it doesn't provide interface one expects from a reference. The same issue is with std::reference_wrapper - for which reason they used only for forwarding references. Say you make/use a substitude for observer_ptr - it will simply be more intuitive to users and have better functionality than opt_ref has. \$\endgroup\$ – ALX23z Mar 30 at 15:38
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;

  • ...

|improve this answer|||||
\$\endgroup\$

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.