2

When delivering an API to you often have to declare error codes (most often as int) and later you often want to provide a function converting a int error code to std::string to be able to report the error to the user in a smart way.

I found some posts on how to maintain the int/std::string mapping programmatically, like this one: Mapping error codes to string in C++

Now, I was wondering, why not simply return a std::string rather than a int? Empty string would mean no error, anything else would mean error + provide a human readable message.

Let's obviously assume you don't care about memory usage and performances (your API has functions not called that often and time of execution is not critical).

If you need the client to be able to do some specific operations programmatically, error codes could be declared as constants. But you don't need any int to std::string mapping anymore. As an example, it would be:

Declaration:

static const std::string successMessage;
static const std::string fileDoesNotExistMessage;
static const std::string internalErrorMessage;

std::string openFile( const std::string& fileName );

Implementation:

static const std::string successMessage = "";
static const std::string fileDoesNotExistMessage = "File does not exist";
static const std::string internalErrorMessage = "Internal error";

std::string openFile( const std::string& fileName )
{
    if ( ... ) // test file existance
    {
        if ( ... ) // internal tests
            return internalErrorMessage;
        else
            return successMessage;
    }
    else
    {
        return fileDoesNotExistMessage ;
    }
}

API user can then do:

int main()
{
    std::string error = openFile( "file.txt" );
    if ( error.empty() )
    {
        std::cout << "File was successfully opened" << std::endl;
    }
    else if ( error == fileDoesNotExistMessage )
    {
        // specific error handling
    }
    else
    {
        std::cout << "Unable to open file, error reported is " << error << std::endl;
    }
}

Advantages:

  • No risk to have a int/std::string broken mapping
  • Easy to add error codes on the fly
  • Easy to maintain

There must be disavantages, because I know no open source library using this approach...any idea why?

Community
  • 1
  • 1
jpo38
  • 20,821
  • 10
  • 70
  • 151
  • 1
    Disadvantages: Allocates memory for and copies the string every time you return. Users of the API who don't show the strings to the end-user will have to pay in terms of performance for a feature they don't use. – Emil Laine Dec 24 '15 at 10:55
  • 1
    Disadvantage - string comparison is expensive compared to equality of two integers – Ed Heal Dec 24 '15 at 10:57
  • User may be tempted to use raw-string instead of your global, on then you cannot change message (like typo, ...) – Jarod42 Dec 24 '15 at 11:01
  • @zenith: As mentioned, performance and memory usage is not a big deal here. – jpo38 Dec 24 '15 at 11:09
  • @EdHeal: As mentioned, performance and memory usage is not a big deal here. – jpo38 Dec 24 '15 at 11:09
  • 3
    Harder to internationalize. – Galik Dec 24 '15 at 11:16
  • @Galik: You may post this as a real answer. It's absolutely true and it's definitely a disadventage I did not identify – jpo38 Dec 24 '15 at 15:49

2 Answers2

5

Why not go one stage further and do the Right Thing(tm)?

Report errors with exceptions. It's what they're for.

example:

struct file_does_not_exist : std::runtime_error
{
  using std::runtime_error::runtime_error;
};

struct internal_error : std::runtime_error
{
  using std::runtime_error::runtime_error;
};

void openFile( const std::string& fileName )
{
    if (!fileExists())
      throw file_does_not_exist(fileName + " does not exist");

    if (!internal_tests(fileName))
      throw internal_error("internal tests failed");

    doLogic();
}

user code now becomes:

   try {
     openFile("xyz.txt");
     do_other_logic();
     ...
   }
   catch(const std::exception& e)
   {
      std::cerr << "failed because: " << e.what() << std::endl;
   }

A complete example that demonstrates why exceptions are the Right Thing

#include <iostream>
#include <stdexcept>
#include <exception>
#include <string>
#include <fstream>

// define our program's exceptions, deriving from standard types
struct failed_to_open : std::runtime_error
{
    using std::runtime_error::runtime_error;
};

struct database_op_failed : std::runtime_error
{
    using std::runtime_error::runtime_error;
};

struct user_update_failed : std::runtime_error
{
    using std::runtime_error::runtime_error;
};

// copied from cppreference.com
void print_exception(const std::exception& e, int level =  0)
{
    std::cerr << std::string(level, ' ') << "exception: " << e.what() << '\n';
    try {
        std::rethrow_if_nested(e);
    } catch(const std::exception& e) {
        print_exception(e, level+1);
    } catch(...) {}
}


void open_file(std::fstream& f, const std::string& filename)
{
    throw failed_to_open(std::string("failed to open file " + filename));
}


struct database_connection
{
    database_connection()
    try {
        open_file(_file, "database.txt");
    }
    catch(...) {
        std::throw_with_nested(database_op_failed("database failed to create"));
    }


    std::fstream _file;
};

// note the use of function try blocks coupled with
// throw_with_nested to make exception handling *clean and easy*
void update_user_age(const std::string& name, int newage)
try {
    database_connection d;
    // d.update_record(...)
    // ... RAII
}
catch(...) {
    std::throw_with_nested(user_update_failed("failed to update user " + name
                                                + " to age " + std::to_string(newage)));
}


// only one try/catch in the whole program...
int main()
{
    try {
        update_user_age("bob", 30);

        // ... lots more log here with no need to check errors

    }
    catch(const std::exception& e) {
        // ...which completely explains the error
        print_exception(e);
        return(100);
    }
    return 0;
}

expected output:

exception: failed to update user bob to age 30
 exception: database failed to create
  exception: failed to open file database.txt
Richard Hodges
  • 68,278
  • 7
  • 90
  • 142
  • Exceptions are nice for the library itself (`openFile`) but is a real pain for the library user, having to always `try`/`catch` every function call.... – jpo38 Dec 24 '15 at 15:48
  • @jpo38 the whole basis of exceptions and RAII is that you don't need to catch every exception. You catch them in the stack frame you want to handle them. If that's in main() with a general catch-all of `std::exception&` then so be it. What is a pain is libraries that return status codes, because this means that user code becomes littered with error checking, hiding logic and creating un-necessary resource cleanup logic. – Richard Hodges Dec 24 '15 at 15:53
  • @jpo38 from the C++ standards body and creator of the language: https://isocpp.org/wiki/faq/exceptions – Richard Hodges Dec 24 '15 at 15:57
  • This convonced me: https://isocpp.org/wiki/faq/exceptions#exceptions-avoid-spreading-out-error-logic (it's true it does not require more code to be written by caller...) – jpo38 Dec 24 '15 at 16:01
  • @jpo38 fantastic :) one more developer producing excellent, maintainable code. Welcome to the bright new world! (updated answer with a complete compilable example of correct exception handling) – Richard Hodges Dec 24 '15 at 16:17
1

This could be useful to a user, but not to a dev.

Given any error, I'd like to easily find the spot responsible for it. Can you guarantee the message will remain unchanged, so a future search will point to the right spot? It's much easier to get that with enums - the message can be changed with no real harm.

You should not underestimate the performance issue. Comparison, copying, allocation - it costs. If you really, really don't care, simply create a struct with both error code and string message. Or throw an exception.

hauron
  • 4,550
  • 5
  • 35
  • 52