Monday, May 27, 2013

Extending expected<T> to deal with references.

On Dec 10, 2012 Andrei Alexandrescu presented the concept of expected class template which purpose is to hold a value returned by function, or an exception object in case of function failure (similar to Boost.Optional, but latter holds only a value, no error information). The main idea is that expected<T> contains an anonymous union with members of types T and std::exception_ptr. Unions with class object data members are available in C++11.

However, unions still cannot contain reference variables as the members [ibid.], so expected is useless for functions returning references (like at() method of std::vector, et c.). A possible solution is to partially specialize expected<T&> and to use std::reference_wrapper<T> instead of T as union member. Also, such specialized class should use different constructors set since it's senseless to initialize reference with anything but other reference. Same is for expected<const T&> with addition that it's get() method should always return constant reference.

Below is corresponding realization of expected class template. It some differs from original one (e.g. pointer-like operators is used instead of is_valid() and get() methods), but substantially they are the same.

#include <functional>
#include <stdexcept>

template<typename>
class expected;

template<typename>
class expected_common;

template<typename T>
class expected_traits {
    friend class expected_common<T>;

    typedef T  storage;
    typedef T  value;
    typedef T* pointer;
    typedef T& reference;
}; //expected_traits

template<typename T>
class expected_traits<T&> {
    friend class expected_common<T&>;

    typedef std::reference_wrapper<T> storage;
    typedef T                         value;
    typedef T*                        pointer;
    typedef T&                        reference;
}; //expected_traits<T&>

template<typename T>
class expected_traits<const T&> {
    friend class expected_common<const T&>;

    typedef std::reference_wrapper<const T> storage;
    typedef T                               value;
    typedef const T*                        pointer;
    typedef const T&                        reference;
}; //expected_traits<const T&>

template<typename T>
class expected_common {
    friend class expected<T>;

    typedef typename expected_traits<T>::value     value;
    typedef typename expected_traits<T>::storage   storage;
    typedef typename expected_traits<T>::pointer   pointer;
    typedef typename expected_traits<T>::reference reference;

    expected_common() {};

    expected_common(const value& value) :
        m_valid  (true),
        m_storage(value)
    {};

    expected_common(value& value) :
        m_valid  (true),
        m_storage(value)
    {};

    expected_common(value&& value) :
        m_valid  (true),
        m_storage(std::move(value))
    {};

    template<typename... AA>
    explicit expected_common(AA&&... arguments) :
        m_valid  (true),
        m_storage(std::forward<AA>(arguments)...)
    {};

    expected_common(const expected_common& other) :
        m_valid(other.m_valid)
    {
        if(m_valid) new(&m_storage) storage(other.m_storage);
        else new(&m_exception) std::exception_ptr(other.m_exception);
    };

    expected_common(expected_common&& other) :
        m_valid(other.m_valid)
    {
        if(m_valid) new(&m_storage) storage(std::move(other.m_storage));
        else new(&m_exception) std::exception_ptr(std::move(other.m_exception));
    };

    ~expected_common() {
        if(m_valid) m_storage.~storage();
        else m_exception.~exception_ptr();
    };

    void swap(expected_common& other) {
        if(m_valid) {
            if(other.m_valid) std::swap(m_storage, other.m_storage);
            else {
                auto exception = std::move(other.m_exception);
                new(&other.m_storage) storage(std::move(m_storage));
                new(&m_exception) std::exception_ptr(exception);
            }
        } else
            if(other.m_valid) other.swap(*this);
            else m_exception.swap(other.m_exception);
    };

public:
    template<class E>
    static expected<T> from_exception(const E& exception) {
        if (typeid (exception) != typeid (E))
        throw std::invalid_argument("slicing detected");

        return from_exception(std::make_exception_ptr(exception));
    }

    static expected<T> from_exception(std::exception_ptr exception) {
        expected<T> result;
        result.m_valid = false;
        new(&result.m_exception) std::exception_ptr(exception);
        return result;
    };

    static expected<T> from_exception() {
        return from_exception(std::current_exception());
    };

    template<class F, typename... AA>
    static expected<T> from_code(F function, AA&&... arguments) {
        try {
            return expected<T>(function(std::forward<AA>(arguments)...));
        }
        catch (...) {
            return from_exception();
        };
    };

    operator bool() const {
        return m_valid;
    };

    template<class E>
    bool exception_is() const {
        try {
            if(!m_valid) std::rethrow_exception(m_exception);
        } catch(const E&) {
            return true;
        } catch(...) {};

        return false;
    };

private:
    reference get() {
        if(!m_valid) std::rethrow_exception(m_exception);
        return m_storage;
    };

    const reference get() const {
        if(!m_valid) std::rethrow_exception(m_exception);
        return m_storage;
    };

public:
    reference operator * () {
        return get();
    };

    const reference operator * () const {
        return get();
    };

    pointer operator -> () {
        return &get();
    };

    const pointer operator -> () const {
        return &get();
    };

private:
    bool                   m_valid;
    union {
        storage            m_storage;
        std::exception_ptr m_exception;
    };
}; //expected_common

template<typename T>
class expected :
    public expected_common<T>
{
    friend class expected_common<T>;
    typedef expected_common<T> common;

    expected() {};

public:
    template<class... AA>
    expected(AA&&... arguments) :
        common(std::forward<AA>(arguments)...)
    {};

    expected(const T& value) :
        common(value)
    {};

    expected(T&& value) :
        common(value)
    {};

    expected(const expected& other) :
        common(static_cast<const common&>(other))
    {};

    expected(expected&& other) :
        common(static_cast<common&&>(other))
    {};

    void swap(expected& other) {
        common::swap(static_cast<common&>(other));
    };
}; //expected

template<typename T>
class expected<T&> :
    public expected_common<T&>
{
    friend class expected_common<T&>;
    typedef expected_common<T&> common;

    expected() {};

public:
    expected(T& value) :
        common(value)
    {};

    expected(const expected& other) :
        common(static_cast<const common&>(other))
    {};

    expected(expected&& other) :
        common(static_cast<common&&>(other))
    {};

    void swap(expected& other) {
        common::swap(static_cast<common&>(other));
    };
}; //expected<T&>

template<typename T>
class expected<const T&> :
    public expected_common<const T&>
{
    friend class expected_common<const T&>;
    typedef expected_common<const T&> common;

    expected() {};

public:
    expected(const T& value) :
        common(value)
    {};

    expected(const expected& other) :
        common(static_cast<const common&>(other))
    {};

    expected(expected&& other) :
        common(static_cast<common&&>(other))
    {};

    void swap(expected& other) {
        common::swap(static_cast<common&>(other));
    };
}; //expected<const T&>

10 comments:

  1. Nice work!
    I'd probably make the operator bool in expected_common explicit... and fix the from_code method :)

    ReplyDelete
    Replies
    1. Thank you! Fixed. There was also a syntax error at swap methods. Seemingly, I've hastened to post cleaned code even without compiling it :)

      As regards the operator bool, the idea was to make validity check look close to pointer case once expected uses pointer notation. Latter by-turn was chosen to avoid excess type conversions in use case with auto like following:

      expected bar() {
      //...
      };

      if(auto foo = bar())
      foo->baz();

      I don't like this idea much, but with lack of smart references it looks reasonable.

      Delete
    2. I've wanted to write:

      expected<Foo> bar() {
          //...
      };

      if(auto foo = bar())
          foo->baz();

      Delete
  2. There is a bug in swap() function. You need to destroy other.m_exception and this.m_storage before you are allowed to construct new objects in their place. Moving from them is not enough. You have destroy the objects after move. Otherwise you have undefined behaviour AFAIK.

    ReplyDelete
    Replies
    1. More importantly, it looks like swap doesn't swap m_valid? I'm looking for a lightweight, correct expected, and I don't particularly want to install the full ptal proposal. Any others around, preferably single-header?

      Delete
    2. In 2013 I've started spike-expected. I'm considering to work on a single header expected-lite for C++03 and later with alignment as used in optional-lite.

      Delete
    3. .. Perhaps starting with a C++11 and later version...

      Delete
  3. This comment has been removed by the author.

    ReplyDelete
  4. Should this line has a rvalue cast?

    expected(T&& value) :
    common(value)
    {};

    --

    expected(T&& value) :
    common(std::move(value))
    {};

    ReplyDelete