Background
First I recommend watching the wonderful talk given by Andrei Alexandrescu about error handling in modern c++ and also viewing the accompanying slides.
Thanks to this talk, I was inspired to try my hand at implementing one possible version of the expected<T> that he presents in his talk. However, I was more inclined to agree with some of the criticisms that Herb Sutter made; namely, I wanted to try an implementation using SFINAE and the restriction that the stored exception type must be derived from std::exception. Moreover, I wanted to add one or two other convenience functions modeled loosely on what the stl provides. Also, as I do most of my casual development in MSVC, I wanted my implementation to compile with the 2012 CTP version of the compiler.
Final usage
// from an int expected<int> expected_int1(10); // true bool valid = expected_int1.is_valid(); // 10 auto a = expected_int1.get(); // nullptr auto ptr = expected_int1.get_exception<std::runtime_error>(); // false bool has_exception = expected_int1.with_exception([](std::runtime_error& e) { // won't be called }); // from an exception expected<int> expected_int2(std::runtime_error("oops")); // false valid = expected_int2.is_valid(); // std::exception_ptr ptr = expected_int2.get_exception<std::runtime_error>(); // true has_exception = expected_int2.with_exception([](std::runtime_error& e) { std::clog << e.what() << std::endl; // "oops" }); try { a = expected_int2.get(); // throws } catch (std::runtime_error& e) { std::clog << e.what() << std::endl; // "oops" } // from code expected<std::ifstream> expected_file([] { std::ifstream in("no file"); in.exceptions(in.failbit); return in; }); // prints the message and then rethrows expected_file.with_exception<true>([](std::ios_base::failure& e) { std::clog << e.code().message() << std::endl; }); struct foo { foo(int a, bool b, double c, char const* d) {} }; auto expected_foo = make_expected<foo>(10, false, 11.0, "bar");
Complications
The implementation that is presented in the talk unabashedly makes use of any appropriate features offered by C++11 — critically, unrestricted unions. Unfortunately these are not yet supported by visual studio, hence the need to work around it. The work around isn’t hard per say, but it makes the implementation a little messier. We need two things:
Properly aligned storage
Nothing too hard, but note the use of two new standard library types: std::aligned_storage and std::alignment_of.
//! compile time max(A, B) template <unsigned A, unsigned B> struct max_of { static unsigned const value = B < A ? A : B; }; //! workaround for the lack of C++11 unions in MSVC typedef typename std::aligned_storage< max_of<sizeof(T), sizeof(std::exception_ptr)>::value, max_of<std::alignment_of<T>::value, std::alignment_of<std::exception_ptr>::value>::value >::type storage_type_; //! workaround for the lack of C++11 unions in MSVC storage_type_ storage_;
Access to the two mutually exclusive values
Again, nothing particularly burdensome; just a little casting.
//! workaround for the lack of C++11 unions in MSVC T& as_value_() { return *reinterpret_cast<T*>(&storage_); } //! workaround for the lack of C++11 unions in MSVC T const& as_value_() const { return *reinterpret_cast<T*>(&storage_); } //! workaround for the lack of C++11 unions in MSVC std::exception_ptr& as_exception_() { return *reinterpret_cast<std::exception_ptr*>(&storage_); } //! workaround for the lack of C++11 unions in MSVC std::exception_ptr const& as_exception_() const { return *reinterpret_cast<std::exception_ptr*>(&storage_); }
Differences and Refinements
The original talk provides static member functions for construction from an exception or from “code”. I decided to restrict my implementation to exceptions derived from std::exception, and as such could use regular member constructors instead.
Construction from an exception
Again, as in the talk above, the compile time and run-time typeids must match, or else the exception object will have been sliced: Throw by value, catch by reference.
//////////////////////////////////////////////////////////////////////////// //! Construct from an exception derived from std::exception //////////////////////////////////////////////////////////////////////////// template <typename E> expected(E const& exception, //the exception type must be derived from std::exception typename std::enable_if< std::is_base_of<std::exception, E>::value >::type* = nullptr ) : is_expected_(false) { //! the dynamic and static types must match or the object will have //! sliced. if (typeid(E) != typeid(exception)) { assert(false && "sliced!"); } new(&storage_) std::exception_ptr(std::make_exception_ptr(exception)); }
Construction from a callable type
Here the constructor is constrained to nullary callable types by the second unnamed type: if F doesn’t define a nullary operator() SFINAE will exclude this overload.
//////////////////////////////////////////////////////////////////////////// //! Construct from a nullary callable object //////////////////////////////////////////////////////////////////////////// template <typename F, typename = decltype(std::declval<F>()())> expected(F function) : is_expected_(true) { try { new(&storage_) T(function()); } catch(...) { is_expected_ = false; new(&storage_) std::exception_ptr(std::current_exception()); } }
Handling, modifying, and rethrowing stored exceptions
A final convenience function I call with_exception makes use of the callable_traits template I introduced in my last post to avoid having to explicitly specify the exception type twice when using it.
//! Decide at compile time whether to rethrow the active exception template <bool B> static inline void rethrow_exception(); template <> __declspec(noreturn) static inline void rethrow_exception<true>() { throw; } template <> static inline void rethrow_exception<false>() { } //////////////////////////////////////////////////////////////////////////// //! If the stored exception_ptr is convertible to E, call function(E&), and //! for Rethrow = true, rethrow. Otherwise return true. If the exception_ptr //! is not convertible to to E, return false. //////////////////////////////////////////////////////////////////////////// template <bool Rethrow = false, typename F> bool with_exception(F function) { typedef traits::callable_traits<F>::arg<0>::type exception_type; typedef typename std::remove_reference<exception_type>::type non_ref_exception_type; static_assert(std::is_reference<exception_type>::value, "function must take a reference"); static_assert( std::is_base_of< std::exception, non_ref_exception_type >::value, "function must take a type derived from std::exception" ); if (!is_valid()) { try { std::rethrow_exception(as_exception_()); } catch (exception_type e) { function(e); rethrow_exception<Rethrow>(); return true; } catch (...) { } } return false; }
Making expected objects
One final difference, or addition, is an stl like make_expected helper function, which also happened to reveal an interesting compiler bug in MSVC related to catch(…) and variadic templates. It merely constructs the object with a T or any exception thrown during its construction. Another rather severe compiler bug prevents this from being as useful as it could be, but it will work for simple construction.
namespace detail { //! Workaround for a compiler bug in MSVC template <typename T, typename F> expected<T> make_expected_helper(F function) { try { return expected<T>(function()); } catch (...) { return expected<T>(std::current_exception()); } } } //namespace detail template <typename T, typename... Args> expected<T> make_expected(Args&&... args) { //! workaround for a compiler bug in MSVC return detail::make_expected_helper<T>([&] { return T(std::forward<Args>(args)...); }); }
Final Implementation
//! compile time max(A, B) template <unsigned A, unsigned B> struct max_of { static unsigned const value = B < A ? A : B; }; //! Decide at compile time whether to rethrow the active exception template <bool B> static inline void rethrow_exception(); template <> __declspec(noreturn) static inline void rethrow_exception<true>() { throw; } template <> static inline void rethrow_exception<false>() { } //////////////////////////////////////////////////////////////////////////////// //! Contains a T or an exception derived from std::exception describing the //! reason a T failed to be created. //////////////////////////////////////////////////////////////////////////////// template <typename T> class expected { public: //////////////////////////////////////////////////////////////////////////// //! Explicitly destroy T or the exception_ptr //////////////////////////////////////////////////////////////////////////// ~expected() { if (is_valid()) { as_value_().~T(); } else { using std::exception_ptr; //only as a workaround for a parsing quirk as_exception_().~exception_ptr(); } } //////////////////////////////////////////////////////////////////////////// //! Construct from another expected //////////////////////////////////////////////////////////////////////////// expected(expected const& other) : is_expected_(other.is_expected_) { if (other.is_valid()) { new(&storage_) T(other.as_value_()); } else { new(&storage_) std::exception_ptr(other.as_exception_); } } expected(expected&& other) : is_expected_(other.is_expected_) { if (other.is_valid()) { new(&storage_) T(std::move(other.as_value_())); } else { new(&storage_) std::exception_ptr(std::move(other.as_exception_())); } } expected& operator=(expected const& rhs) { ~expected(); // clean up the current value; is_expected_ = rhs.is_expected_; if (is_valid()) { new(&storage_) T(rhs.as_value_()); } else { new(&storage_) std::exception_ptr(rhs.as_exception_); } return *this; } expected& operator=(expected&& rhs) { swap(rhs); return *this; } //////////////////////////////////////////////////////////////////////////// //! Construct from a T //////////////////////////////////////////////////////////////////////////// expected(T const& value) : is_expected_(true) { new(&storage_) T(value); } expected(T&& value) : is_expected_(true) { new(&storage_) T(std::move(value)); } //////////////////////////////////////////////////////////////////////////// //! Construct from an exception derived from std::exception //////////////////////////////////////////////////////////////////////////// template <typename E> expected(E const& exception, //the exception type must be derived from std::exception typename std::enable_if< std::is_base_of<std::exception, E>::value >::type* = nullptr ) : is_expected_(false) { if (typeid(E) != typeid(exception)) { assert(false && "sliced!"); } new(&storage_) std::exception_ptr(std::make_exception_ptr(exception)); } //////////////////////////////////////////////////////////////////////////// //! Construct from an exception_ptr //////////////////////////////////////////////////////////////////////////// expected(std::exception_ptr exception) : is_expected_(false) { new(&storage_) std::exception_ptr(std::move(exception)); } //////////////////////////////////////////////////////////////////////////// //! Construct from a nullary callable object //////////////////////////////////////////////////////////////////////////// template <typename F, typename = decltype(std::declval<F>()())> expected(F function) : is_expected_(true) { try { new(&storage_) T(function()); } catch(...) { is_expected_ = false; new(&storage_) std::exception_ptr(std::current_exception()); } } bool is_valid() const { return is_expected_; } T& get() { if (!is_valid()) std::rethrow_exception(as_exception_()); return as_value_(); } T const& get() const { if (!is_valid()) std::rethrow_exception(as_exception_()); return as_value_(); } //! The std::exception_ptr if the stored exception is an E, otherwise std::exception_ptr(nullptr) template <typename E> std::exception_ptr get_exception() { static_assert(std::is_base_of<std::exception, E>::value, "E must be derived from std::exception."); if (!is_valid()) { try { std::rethrow_exception(as_exception_()); } catch (E const&) { return as_exception_(); } catch (...) { } } return std::exception_ptr(nullptr); } //////////////////////////////////////////////////////////////////////////// //! If the stored exception_ptr is convertible to E, call function(E&), and //! for Rethrow = true, rethrow. Otherwise return true. If the exception_ptr //! is not convertible to to E, return false. //////////////////////////////////////////////////////////////////////////// template <bool Rethrow = false, typename F> bool with_exception(F function) { typedef traits::callable_traits<F>::arg<0>::type exception_type; typedef typename std::remove_reference<exception_type>::type non_ref_exception_type; static_assert(std::is_reference<exception_type>::value, "function must take a reference"); static_assert( std::is_base_of< std::exception, non_ref_exception_type >::value, "function must take a type derived from std::exception" ); if (!is_valid()) { try { std::rethrow_exception(as_exception_()); } catch (exception_type e) { function(e); rethrow_exception<Rethrow>(); return true; } catch (...) { } } return false; } void swap(expected& rhs) { if (is_valid()) { //(1) good good if (rhs.is_valid()) { using std::swap; swap(as_value_(), rhs.as_value_()); //(2) good bad } else { auto temp = std::move(rhs.as_exception_()); new(&rhs.storage_) T(std::move(as_value_())); new(&storage_) std::exception_ptr(std::move(temp)); std::swap(is_expected_, rhs.is_expected_); } } else { //(3) good bad if (rhs.is_valid()) { rhs.swap(*this); //complementary case to (2) //(4) bad bad } else { std::swap(as_exception_(), rhs.as_exception_()); } } } private: //! workaround for the lack of C++11 unions in MSVC typedef typename std::aligned_storage< max_of<sizeof(T), sizeof(std::exception_ptr)>::value, max_of<std::alignment_of<T>::value, std::alignment_of<std::exception_ptr>::value>::value >::type storage_type_; //! workaround for the lack of C++11 unions in MSVC storage_type_ storage_; bool is_expected_; T& as_value_() { return *reinterpret_cast<T*>(&storage_); } T const& as_value_() const { return *reinterpret_cast<T*>(&storage_); } //! workaround for the lack of C++11 unions in MSVC std::exception_ptr& as_exception_() { return *reinterpret_cast<std::exception_ptr*>(&storage_); } //! workaround for the lack of C++11 unions in MSVC std::exception_ptr const& as_exception_() const { return *reinterpret_cast<std::exception_ptr*>(&storage_); } }; namespace detail { //! Workaround for a compiler bug in MSVC template <typename T, typename F> expected<T> make_expected_helper(F function) { try { return expected<T>(function()); } catch (...) { return expected<T>(std::current_exception()); } } } //namespace detail template <typename T, typename... Args> expected<T> make_expected(Args&&... args) { //! workaround for a compiler bug in MSVC return detail::make_expected_helper<T>([&] { return T(std::forward<Args>(args)...); }); } template <typename T> void swap(expected<T>& lhs, expected<T>& rhs) { lhs.swap(rhs); }