Boost logo

Boost :

Subject: [boost] [outcome] To variant, or not to variant?
From: Gavin Lambert (gavinl_at_[hidden])
Date: 2017-05-31 08:26:04


I probably should have thought of asking this earlier, but it occurs to
me now that my own mental model of how an "outcome"-ish type should act
is probably not suited to variant storage at all.

So just out of curiosity I thought I'd ask whether people prefer this
sort of interface:

template<typename T>
class outcome
{
public:
     bool has_value() const { return m_storage.has<T>(); }
     T& value() { return m_storage.get<T>(); }

     bool has_error() const { return m_storage.has<error_code>(); }
     error_code& error() { return m_storage.get<error_code>(); }

     bool has_exception() const { return m_storage.has<exception_ptr>(); }
     exception_ptr& exception() { return m_storage.get<exception_ptr>(); }

     void set(none_t) { m_storage = none; }
     void set(const T& val) { m_storage = val; }
     void set(error_code err) { m_storage = err; }
     void set(exception_ptr ep) { m_storage = ep; }

private:
     variant<none_t, T, error_code, exception_ptr> m_storage;
};

Or this sort of interface:

template<typename T>
class outcome
{
public:
     bool has_value() const { return !!m_value; }
     T& value() { ensure(); return m_value.value(); }

     //bool has_error() const { return !!m_error; }
     const error_code& error() const { return m_error; }

     //bool has_exception() const { return !!m_exception; }
     const exception_ptr& exception() const { return m_exception; }

     void ensure() const
     {
         if (m_exception) { rethrow_exception(m_exception); }
         if (m_error) { throw system_error(m_error); }
     }

     void set(none_t)
     {
         m_value = none;
         m_error = error_code();
         m_exception = nullptr;
     }
     void set(const T& val)
     {
         m_value = val;
         m_error = error_code();
         m_exception = nullptr;
     }
     void set(error_code err)
     {
         m_value = none;
         m_error = err;
         m_exception = nullptr;
     }
     void set(exception_ptr ep)
     {
         m_value = none;
         m_error = ep ? error_code(errc::has_exception) : error_code();
         m_exception = ep;
     }

private:
     optional<T> m_value;
     error_code m_error;
     exception_ptr m_exception;
};

(Don't get too hung up on the specifics. This is a sketch, not a real
implementation. I've obviously omitted things that a real
implementation would need such as more const methods and move support,
and construction and assignment rather than using set methods. The
focus is on variant vs. non-variant.)

Anyway, the point is that this could actually transport multiple things;
in particular as above both an error_code and an exception_ptr; perhaps
set(error_code) could construct an exception as well, although I've
explained elsewhere why I don't like that option.

Perhaps this could even be exposed even more so that user code could
explicitly provide both a value and an error -- think "here's a value,
but it was truncated" or "here's the 5 values you asked for, but there's
more", both fairly common in system APIs. Or for things like ambiguous
matches where you still return the most likely candidate. Or dictionary
insertion (duplicate key, but here's the value that's already there).
Or many more such examples. (Though obviously in this case value()
couldn't call ensure(). But that should make some people happy.)

Also note has_error() and has_exception() don't even need to be provided
in this version since you can always call error() and exception() in a
boolean context anyway, with the same result. (Or actually a slightly
better result than the implementation shown here.)

The obvious downside is that it now uses more storage, which might
hinder some inlining cases.


Boost list run by bdawes at acm.org, gregod at cs.rpi.edu, cpdaniel at pacbell.net, john at johnmaddock.co.uk