11
\$\begingroup\$

I have researched ways to implement a simpler alternative for C++ exceptions. The problem I had with exceptions was the complex and not-portable stack unwinding process which makes it hard to implement for a multiple systems.

However, the alternatives didn't look clean enough to indicate about an error case. In other words, a programmer could easily ignore an error. For example, when using a status code and a result, it can be ignored/mistaken for a result (output). Other alternatives (like, for example exception stack) wasn't modular enough.

My solution is simple: I used a union in order to create an error/value variant. In other words, my class could contain either a return value or an error value.

Example of usage:

Expected<int, ErrorCode>
doSomething(bool var) {
    if (val) // Success.
        return {0x30};
    else
        return {ErrorCode::InvalidVar};
}

void callDoSomething() {
    auto maybeRetval = doSomething(false);

    if (maybeRetval) {
        // Success, print result;
        std::cout << maybeRetval.get();
    } else {
        // Failure, print error code:
        std::cout << maybeRetval.getError();
    }
}

The same program with status codes:

ErrorCode 
doSomething(bool var, int *retval) {
    if (var) {
        *retval = 0x30;
        return ErrorCode::OK;
    } else {
        return ErrorCode::InvalidVar;
    }
}

void callDoSomething() {
    int retval;
    ErrorCode errorCode = doSomething(false, &retval);

    if (errorCode != ErrorCode::OK) {
        // Success, print result;
        std::cout << retval;
    } else {
        // Failure, print error code:
        std::cout << errorCode;
    }
}

Expected code:

#include <cassert>

/**
  @brief A dynamic variant container for a value, or an error value.
 */
template<class T, class ErrorType>
class Expected
{
public:
    /**
      @brief Forward arguments to T's constructor (not error).
     */
    template<class... Args>
    Expected(Args&&...valueArgs)
        : mValue{std::forward<Args>(valueArgs)...}, mIsError{false}
    {
    }

    ~Expected ()
    {
        destruct ();
    }

    /**
       @brief Construct the error from to an error rvalue (error).
       @param error The error.
     */
    Expected(ErrorType &&error)
        : mError{std::move (error)}, mIsError{true}
    {
    }

    // Disallow copy.
    Expected &operator = (const Expected &expected) = delete;

    Expected(Expected &&expected)
        : mIsError{expected.mIsError}
    {
        if (mIsError)
            mError = std::move (expected.mError);
        else
            mValue = std::move (expected.mValue);
    }

    Expected &operator = (Expected &&expected) {
        destruct ();

        if (mIsError)
            mError = std::move (expected.mError);
        else
            mValue = std::move (expected.mValue);

        return *this;
    }

    operator bool () const
    {
        return !mIsError;
    }

    bool isError() const
    {
        return mIsError;
    }

    const ErrorType &getError() const{
        assert(mIsError);
        return mError;
    }

    const T &get() const{
        assert (!mIsError);
        return mValue;
    }

private:
    /**
      @brief According to the current status (error|not error),
             descturct the current active value.
      */
    void destruct() {
        if (mIsError)
            mError.~ErrorType();
        else
            mValue.~T();
    }

    /**
       @brief An union, holds the value, or the error.
     */
    union {
         T mValue;
         ErrorType mError;
    };

    /**
       @brief mIsError Specify whether we're holding a error
              or T.
     */
    bool mIsError;
};

This implementation has a few problems:

  • Sometimes it can be hard to spot whether a return {X} segment is an error or not. Also, there would be a problem if ErrorType and T would be the same.
  • A little bit of overhead compared to exceptions on success cases: we need to check every time the Expected object for error (even on success). When with exceptions, there would be an overhead only on failure.
\$\endgroup\$
3
  • 1
    \$\begingroup\$ Your hosted C++ implementation does not already provide support for exceptions? The compiler should be responsible for dealing with all of the messiness and ugliness. It doesn't matter that stack unwinding isn't portable. You don't need to know how the stack is unwound. \$\endgroup\$
    – Cody Gray
    Commented Dec 31, 2016 at 12:39
  • 1
    \$\begingroup\$ @CodyGray It is, in theory. However, When developing a portable API, you can't always trust it. For example, I can't really use the STL for that reason (I use a smaller set of utilities from the STL). \$\endgroup\$
    – Shmuel H.
    Commented Dec 31, 2016 at 14:55
  • \$\begingroup\$ A programmer can easily ignore exceptions too. I've seen plenty of catch everything blocks in my career \$\endgroup\$ Commented Dec 31, 2016 at 16:51

1 Answer 1

2
\$\begingroup\$

I like the idea of an "opt-in" exception handling (as in, per function opt-in, rather than globally).

Just a small thing: Your move operator might be faulty. For one, the the argument to the if should likely read expected.mIsError rather than just mIsError. Furthermore, after the move, the contents of expected have been moved out. I think it will be destructed immediately after the move, at which point expected.destruct will still try to destruct one of the two union contents, though none of them is still in a constructed state.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.