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 ifErrorType
andT
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.