Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C++ Error Handling - downside of using std::pair or std::tuple for returning error codes and function returns [closed]

Without getting into the general exceptions vs error codes discussion, what do you think are the downsides of using std::pair or std:tuple for returning multiple values, namely the function's return value AND the error/success code, similar to how many Go developers apparently do error handling?

This approach obviously has the advantage of not having to use out parameters for the function return value or error code (depending on which way around you prefer it).

like image 848
CodeSalad Avatar asked Dec 08 '12 13:12

CodeSalad


2 Answers

This "idiom" is good because both the type and succes indicator are return values of the function. Failure might not be exceptional, so exceptions are inappropriate sometimes.

The downside however is that you have to split out the two return types. This can be ugly; using std::tie helps but you are unable to construct from multiple return.

bool success;
std::string value;
std::tie(success, value)=try_my_func();

This is quite verbose.

Second, if one of the types is "optional" depending on the value of another element in the tuple then it still has to be constructed which for some types is still very wasteful.

If you are using the idiom a lot, consider instead using something like the boost::optional type. This is close to the haskel maybe than go's multiple return.

Reference

http://www.boost.org/doc/libs/1_52_0/libs/optional/doc/html/index.html

like image 73
2 revs, 2 users 91% Avatar answered Sep 20 '22 02:09

2 revs, 2 users 91%


For this purpose, in most cases I use an own wrapper type which introduces some syntactic sugar. Let's see an example:

template <class T>
struct Result
{
public:
    enum Status {
        Success,
        Error
    };

    // Feel free to change the default behavior... I use implicit
    // constructors for type T for syntactic sugar in return statements.
    Result(T resultValue) : s(Success), v(resultValue) {}
    explicit Result(Status status, std::string errMsg = std::string()) : s(status), v(), errMsg(errMsg) {}
    Result() : s(Error), v() {} // Error without message

    // Explicit error with message
    static Result error(std::string errMsg) { return Result(Error, errMsg); }

    // Implicit conversion to type T
    operator T() const { return v; }
    // Explicit conversion to type T
    T value() const { return v; }

    Status status() const { return s; }
    bool isError() const { return s == Error; }
    bool isSuccessful() const { return s == Success; }
    std::string errorMessage() const { return errMsg; }

private:
    T v;
    Status s;

    // if you want to provide error messages:
    std::string errMsg;
};

Then, simply use this class as the return value in your methods which can return errors:

Result<int> fac(int n) {
    if(n < 0)
        return Result<int>::error("n has to be greater or equal zero!");
    if(n == 0)
        return 1;
    if(n > 0)
        return n * fac(n-1);  // gets automatically converted to int
}

Of course this implementation of the factorial function is horrible, but demonstrates the conversion without bothering about the error-extended return type we use.

Example usage:

int main() {
    for(int i = -3; i < 4; ++i)
    {
        Result<int> r = fac(i);
        std::cout << i << " | ";
        std::cout << (r.isSuccessful() ? "ok" : "error") << " | ";
        if(r.isSuccessful())
            std::cout << r.value();
        else
            std::cout << r.errorMessage();
        std::cout << std::endl;
    }
}

Output:

-3 | error | n has to be greater or equal zero!
-2 | error | n has to be greater or equal zero!
-1 | error | n has to be greater or equal zero!
0 | ok | 1
1 | ok | 1
2 | ok | 2
3 | ok | 6

One big advantage of the custom type is that you can insert some control ensuring that the client code always checks for errors before accessing the actual value and only accesses the value if it was successful respectively the error message if it wasn't. For this, we can extend the class by the following:

struct Result
{
public:
    // in all constructors, add:
    Result(...) : ..., checked(false) {...}

    // in the error checker methods, add: (and drop const-ness)
    bool is...() { checked = true; return ... }

    // rewrite the value conversion as follows:
    operator T() const { std::assert(checked && isSuccessful()); return v; }
    T value() const    { std::assert(checked && isSuccessful()); return v; }

    // rewrite the errorMessage-getter as follows:
    std::string errorMessage() const { std::assert(checked && isError()); return errMsg; }

private:
    ...
    bool checked;
};

You might want to make the class definition depending on the build mode (debug build / release build).

Note that the example has to be rewritten as follows:

Result<int> fac(int n) {
    if(n < 0)
        return Result<int>::error("n has to be greater or equal zero!");
    if(n == 0)
        return 1;
    if(n > 0) {
        Result<int> r = fac(n - 1);
        if(r.isError()) return r;  // propagate error (similar to exceptions)
        return n * r;              // r gets automatically converted to int
    }
}

The main-code from above is still valid, as it already did error-checking before accessing the value / the error message.

like image 31
leemes Avatar answered Sep 20 '22 02:09

leemes