5

Is there a common way in C++ to translate an error code to a string to display it?

I saw somewhere a err2msg function, with a big switch, but is that really the best way?

Karl von Moor
  • 8,484
  • 4
  • 40
  • 52

7 Answers7

12

Since C++ does not allow automatic 'translation' from enum values to enum names or similar, you need a function to do this. Since your error codes are not somehow defined in your O/S you need to translate it by yourself.

One approach is a big switch statement. Another is a table search or table lookup. What's best depends on error code set.

table search can be defined in this way:

struct {
    int value;
    const char* name;
} error_codes[] = {
    { ERR_OK, "ERR_OK" },
    { ERR_RT_OUT_OF_MEMORY, "ERR_RT_OUT_OF_MEMORY" },
    { 0, 0 }
};

const char* err2msg(int code)
{
    for (int i = 0; error_codes[i].name; ++i)
        if (error_codes[i].value == code)
            return error_codes[i].name;
    return "unknown";
}
harper
  • 13,345
  • 8
  • 56
  • 105
  • 3
    If your error codes are consecutive then you can use just array of strings instead of array of structs. So you can access to the error string by index. Keep the number of errors to check array bounds. So don't need to loop through the array each time. – gtikok Oct 20 '10 at 07:04
  • @gtikok: of course the real question would then be why bother with codes altogether ? I myself would say that the address of the error string is sufficient as an identifier, and all forms of look-up are superfluous. – Matthieu M. Oct 20 '10 at 07:18
  • @Matthieu M. You have better control over code values that way. I wouldn't rely on the string addresses. – ereOn Oct 20 '10 at 07:27
  • @harper: Nice solution, but there is a typo. You wrote `error_code` and defined `error_codes`. – ereOn Oct 20 '10 at 07:28
  • @ereOn: If you actually need the code value (for documentation ?), of course then it's a bit different (though a pointer is enough still, it just need to point a struct code/message). The `need` however isn't crystal clear, MSVC has error codes for its compilation messages and Clang doesn't, yet I find Clang's diagnostic better. – Matthieu M. Oct 20 '10 at 08:02
  • @Matthieu: One disadvantage of using (`const`) `char*` is that you are handing out pointers into your address space, which you might not want to do for security reasons. (Of course, the `err2msg()` function would have to write the error message into a user-supplied buffer for this to matter.) I once worked for a project where, for all APIs handed out to other software, this was a real concern. We ended up not handing out real pointers to objects in an OO C API, but handles (integer values) that had to be looked up internally. – sbi Oct 20 '10 at 08:15
  • @sbi: I must admit that I have very little knowledge on security of binaries, having only worked on server-code :) – Matthieu M. Oct 20 '10 at 08:27
  • @Matthieu: Yes, it's rather esoteric and only relevant if you have APIs to arbitrary 3rd-party software, but you know what they say: once bitten twice shy. `:)` – sbi Oct 20 '10 at 08:29
  • @ereOne: The code is not intended to become part of product code. The actual requirments will define the need for const char* or for another approach, i.e. passing a buffer or std::string& to the function. – harper Oct 20 '10 at 10:54
  • @ereOne: int is the compiler native integer type. It should be best supported (for speed and size) by the compiler. This should be sufficient as an array index. // Dunno where I need a std::thing for iterating a C-style array. – harper Oct 20 '10 at 10:57
  • What about replacing the for loop in `err2msg` with `for(const auto& e : error_codes) {...}`? This way you could avoid the artificial termination-element which is not very intuitive and may be removed. – MaciekS Apr 30 '19 at 11:26
3

In windows you can use FormatMessage(...) function either with error code return by GetLastError() function or directly to the suspected area.

Please see below links for examples.

http://msdn.microsoft.com/en-us/library/ms679351(v=VS.85).aspx

http://msdn.microsoft.com/en-us/library/ms680582(v=VS.85).aspx

I hope this will help you.

user001
  • 444
  • 5
  • 10
3

Similar to harper's idea, but a bit more generalized:

typedef std::map<int, const char*> error_code_tbl_t;
typedef error_code_tbl_t::value_type error_code_entry_t;
const error_code_entry_t error_code_tbl_[] = {
    { ERR_OK              , "ERR_OK" },
    { ERR_RT_OUT_OF_MEMORY, "ERR_RT_OUT_OF_MEMORY" }, 
    // ...
};
const error_code_tbl_t error_code_tbl( begin(error_code_tbl_)
                                     , end  (error_code_tbl_) );

const char* err2msg(int code)
{
    const error_code_tbl_t::const_iterator it = error_code_tbl.find(code);
    if(it == error_code_tbl.end())
      return "unknown";
    return it->second;
}

(Those begin() and end() functions can be found here.)

Community
  • 1
  • 1
sbi
  • 219,715
  • 46
  • 258
  • 445
2

The big switch is not that bad for this. To get a string for an error code is almost always not performance critical.

You should keep in mind that these error strings are probably not what you want to show your users. The messeges for the user should be kept in resources for easier translation.

strings for error codes are for logs or diagnostics and need no translation.

You can use this trick to define your error codes and the strings in parrallel:

#if defined(ERROR_BUILD_ARRAY)

#define ERROR_START \
        static const err_defn error_table[] = { \
        { WARNING, "Warning" },
#define ERRDEF(num, offset, str) { num, str },
#define ERROR_END { 0, NULL } };

#elif !defined(ERROR_ENUM_DEFINED)

#define ERROR_START \
        typedef enum svn_errno_t { \
        WARNING = OS_START_USERERR + 1,
#define ERRDEF(num, offset, str) /** str */ num = offset,
#define ERROR_END ERR_LAST } svn_errno_t;

#define ERROR_ENUM_DEFINED

ERROR_START

ERRDEF(ERR_BAD_BAD,
            ERR_BAD_CATEGORY_START + 0,
            "Bad error")

ERRDEF(ERR_BAD_FILENAME,
            ERR_BAD_CATEGORY_START + 1,
            "Bogus filename")

ERROR_END

(Copied from subversion sources)

frast
  • 2,700
  • 1
  • 25
  • 34
  • It's for logging. I could also think e.g. about a array of strings `const char* errorMessages[ERR_CORE_LAST]` and then: lot's of `errorMessages[ERR_RT_OUT_OF_MEMORY] = "Runtime - out of memory";` -- so what's better? How do you think about the array version? – Karl von Moor Oct 20 '10 at 07:18
  • The array is ok. There is also a special define trick to define the error codes and the strings in parrallel. This is easier because you can not mess up the order of the error codes and the strings. You can find this trick for example in the source code of the subversion project. – frast Oct 20 '10 at 07:26
  • http://svn.apache.org/repos/asf/subversion/trunk/subversion/include/svn_error_codes.h – frast Oct 20 '10 at 07:28
2

As far as I am concerned, error codes are just a subset of enums. Since we are not blessed in C++ with pretty enums (which makes logs somehow quite hard to parse), error codes are no more easier.

The solution is pretty simple for error codes though:

class ErrorCode
{
public:
  ErrorCode(): message(0) {}
  explicit ErrorCode(char const* m): message(m) {}

  char const* c_str() const { return message; }
  std::string toString() const
  {
    return message ? std::string(message) : std::string();
  }

private:
  char const* message;
};

std::ostream& operator<<(std::ostream& out, ErrorCode const& ec)
{
  return out << ec.c_str();
}

Of course you can supply the traditional ==, !=, <, etc...

  • It's simple!
  • It's fast (the code IS the string, no look-up involved)
  • It's type safe (you cannot accidentally mix it up with another type)

The idea is to return pointers to the text instead of error codes (though wrapped in a class for type safety).

Usage:

// someErrors.h
extern ErrorCode const ErrorOutOfMemory;

// someErrors.cpp
ErrorCode const ErrorOutOfMemory = ErrorCode("OUT OF MEMORY");
Matthieu M.
  • 287,565
  • 48
  • 449
  • 722
  • The drawback is that you can't define error classes. I use sometimes codes where the higher bits define the severity of the error. This allows to check if an error must be handled at a specific stage. – harper Oct 20 '10 at 11:12
  • @harper: if you content yourself with an error string yes, admittedly one could had a severity enum as well as plenty of other fancy tricks. As for packing data, it would work as well, and even be guaranteed to be a nicely encapsulated behavior, possibly done differently on 32 bits and 64 bits platform :p – Matthieu M. Oct 20 '10 at 11:23
1

I tend to avoid the switch since it's usually a big piece of code. I prefer a table lookup along the lines of:

In btree.h:
    enum btreeErrors {
        ZZZ_ERR_MIN = -1,        
        OKAY,
        NO_MEM,
        DUPLICATE_KEY,
        NO_SUCH_KEY,
        ZZZ_ERR_MAX };

In btree.c:
    static const char *btreeErrText[] = {
        "Okay",
        "Ran out of memory",
        "Tried to insert duplicate key",
        "No key found",
        "Coding error - invalid error code, find and destroy developer!"
    };
    const char *btreeGetErrText (enum btreeErrors err) {
        if ((err <= ZZZ_ERR_MIN) || (err >= ZZZ_ERR_MAX))
            err = ZZZ_ERR_MAX;
        return btreeErrText[err];
    }

Not that it usually matters since errors should be the exception rather than the rule, but table lookups are generally faster than running big switch statements (unless they get heavily optimised).

paxdiablo
  • 854,327
  • 234
  • 1,573
  • 1,953
  • My experience is that switch statements often are implemented with jump tables, especially if the cases are consecutive or near by. // One disadvantage of your approach is that you have to maintain two arrays. That can be error-prone. While your compiled code survives silently if you _insert_ a enum member your additional message array tends to become out of sync. – harper Oct 20 '10 at 11:05
  • @harper, that's probably something your unit tests should pick up, ensuring that `sizeof(btreeErrText)/sizeof(*btreeErrText) == ZZZ_ERR_MAX + 1`. – paxdiablo Oct 20 '10 at 12:58
  • It won't find it, if you _insert_ code and message text, but not at the same index. Imaging you have 100+ (error) codes. It's easier to maintain, if you have code and message side by side. – harper Oct 20 '10 at 13:10
0

I wanted a way to have error code (int) and string description (any string) be declared in one and only one single place and none of the examples above allows that (ERR_OK has to be declared somewhere and then "ERR_OK" is mapped to it somewhere else).

So I declared a simple class storing both int and string and maintaining a static map for int->string conversion. I also added an "auto-cast to" int function:

class Error
{
public:
    Error( int _value, const std::string& _str )
    {
        value = _value;
        message = _str;
#ifdef _DEBUG
        ErrorMap::iterator found = GetErrorMap().find( value );
        if ( found != GetErrorMap().end() )
            assert( found->second == message );
#endif
        GetErrorMap()[value] = message;
    }

    // auto-cast Error to integer error code
    operator int() { return value; }

private:
    int value;
    std::string message;

    typedef std::map<int,std::string> ErrorMap;
    static ErrorMap& GetErrorMap()
    {
        static ErrorMap errMap;
        return errMap;
    }

public:

    static std::string GetErrorString( int value )
    {
        ErrorMap::iterator found = GetErrorMap().find( value );
        if ( found == GetErrorMap().end() )
        {
            assert( false );
            return "";
        }
        else
        {
            return found->second;
        }
    }
};

Then, you simply declare your error codes as below:

static Error ERROR_SUCCESS(                 0, "The operation succeeded" );
static Error ERROR_SYSTEM_NOT_INITIALIZED(  1, "System is not initialised yet" );
static Error ERROR_INTERNAL(                2, "Internal error" );
static Error ERROR_NOT_IMPLEMENTED(         3, "Function not implemented yet" );

Then, any function returning int can do to return 1

return ERROR_SYSTEM_NOT_INITIALIZED;

And, client programs of your library will get "System is not initialised yet" when calling

Error::GetErrorString( 1 );

The only limitation I see is that static Error objects are created many times if .h file declaring them is included by many .cpp (that's why I do a _DEBUG test in constructor to check consistency of the map). If you don't have thousands of error code, it should be a problem (and there may be a workaround...)

Jean

jpo38
  • 20,821
  • 10
  • 70
  • 151