std::expected, references, and std::reference_wrapper

473 Views Asked by At

This question is related to std::expected and reference return type

I try to get my head around std::expected (respectively https://github.com/TartanLlama/expected) as an alternative error handling methodology, and I like the concept. However, references seem to be forbidden for the expected type. In my project, some functions should return values, some others references, and I want a common error handling strategy which works for both.

My idea/expectation would be that an std::expected for references should work kind of like a pointer upon success, e.g. like this (ill formed, but what I "would like"):

std::expected<Foo&,string> ret = someFunction(); // undefined behavior
if(ret)
   ret->bar();
else
   doErrorHandling(ret.error());

In std::expected and reference return type, it was proposed to use a std::reference_wrapper as expected type in such cases. However, it seems that this would mean my example above would need some (ugly) indirection:

std::expected<std::reference_wrapper<Foo>,string> ret = someFunction();
if(ret)
{
   Foo& temp{*ret}; // ugly indirection to trigger implicit conversion
   temp.bar();
}
else
   doErrorHandling(ret.error());

Is there a way to avoid this extra step? I know, it seems small, but, for me, it somewhat contradicts the whole point of std::expected...

1

There are 1 best solutions below

2
On

For a little less typing, you might consider something like gsl::not_null<Foo *> in place of std::reference_wrapper<Foo> (note the addition of the pointer to the templated type in gsl::not_null vs std::reference_wrapper). Then you could do something like (*ret)->bar() or (**ret).bar(). There is more information about the comparison of those two wrapper types in another question.

Note that most compilers implement references as pointers anyway, so the "extra" indirection still happens "under the hood", it's just done for you by the compiler. Thus, this should be primarily a question of typing and clarity rather than performance. In particular, since reference_wrapper<Foo> and gsl::not_null<Foo *> should both be trivially copyable and trivially destructible, you shouldn't be prevented from passing them in registers the way that you might with unique_ptr.

If you're unable or disinclined to use gsl specifically, you could write a similar wrapper class yourself:

  1. Use the constructors to enforce the invariant that a private member raw pointer "T *" is never null. To support polymorphism you probably want to allow constructing a "not_null<T>" with a "U &" where std::is_convertible<U *, T *>::value is true. Copy constructor and destructor should be trivial.
  2. Add some method(s) -- probably at least inline dereference operator(s), but up to you -- to dereference the private member pointer
  3. Optionally add implicit or explicit conversion operators to reference and/or raw pointer

Example:

#include <type_traits>
// for std::addressof, unless you don't want to support
// classes with overloaded operator&
#include <memory> 

namespace stack_overflow_answer {
     template<typename T>
     class non_null
     {
          T * raw;

     public:
          using pointer = T *;
          using element_type = T;
          using reference = T &;

          // defining our own constructor 
          // already deletes the default constructor, 
          // but this makes it obvious that we did so on purpose
          non_null() = delete;

          // std::addressof is only constexpr since c++17,
          // so for older compilers, either get rid of constexpr
          // or give up supporting classes that overload operator&
          // and replace std::addressof with &
          constexpr non_null(reference t_ref) noexcept
          : raw(std::addressof(t_ref))
          {}

          // construct from other types if pointer converts
          template<
               typename U, 
               typename Enable = typename std::enable_if<
                    std::is_convertible<U *, T *>::value
                    && !std::is_same<U *, T *>::value
               >::type
          > constexpr non_null(U & convertible_ref) noexcept
          : raw(std::addressof(convertible_ref))
          {}

          non_null(non_null const &) = default;
          non_null & operator=(non_null const &) = default;

          // also allow assignment from non-null convertible pointers
          template<
               typename U, 
               typename Enable = typename std::enable_if<
                    std::is_convertible<U *, T *>::value
                    && !std::is_same<U *, T *>::value
               >::type
          > constexpr non_null & operator=(non_null<U> const & convertible) noexcept
          {
               // other than constexpr std::addressof,
               // the rest of this should all work in c++11,
               // so we'll meet c++11's restrictions on constexpr
               // by using the comma operator to do the assignment
               return ((raw = static_cast<U *>(convertible)), *this);
          }
          
          constexpr reference operator*() const noexcept
          {return *raw;}

          constexpr pointer operator->() const noexcept
          {return raw;}

          // I have chosen implicit conversions in this example
          // but you could make them explicit if you prefer
          constexpr operator reference () const noexcept
          { return *raw;}

          constexpr operator pointer () const noexcept
          {return raw;} 
     };

     // you could verify with whatever type you want
     static_assert(
          std::is_trivially_copyable<non_null<int> >::value
          && std::is_trivially_destructible<non_null<int> >::value,
         "inappropriate overhead"
     );
}