2

Let's say I have a bunch of error codes in my application header.h like:

#define SOMETHING_WENT_WRONG   -1
#define SOLAR_FLARE_DECTECTED  -2
#define ANTS_IN_YOUR_CPU       -3

I return these as int from various API functions. I'd like a clean way to offer a function that user can call to map these to more descriptive error messages, while keeping the header short.

That is, while I could always do something like:

const char *getErrorMessage(int errCode) {
  switch (errCode) {
  case SOMETHING_WENT_WRONG:
    return "Something bad happened, but I don't know more than that";
  case SOLAR_FLARE_DECTECTED:
    return "A solar flare, rather than programmer error, has caused a malfunction";
  case ANTS_IN_YOUR_CPU:
    return "Ants have nested in your CPU, causing all ALU functions to fail";
  default:
    return "I heard you like errors in your error handling. So ...";
  }
}

I really want to avoid the duplication of mentioning the error macro twice (DRY), and having the definition and error string in two different places (making it non-obvious that those adding an error code also need to update the error function).

So I'm looking for an approach that only uses 1 line(ish) per error code - the macro name, int error code and message all together.

Bonus points if I can leave the error string off and have the error string just be the stringified macro name (e.g., "SOMETHING_WENT_WRONG" for the first error).

I'm not too worried about lookup performance, so it's fine for example, to build an array and look through it in the lookup function.

8
  • 5
    Xmacros can do the trick, albeit they need some (not too) deep C know how to understand. Commented Jan 19, 2017 at 21:14
  • Why don't you return the string literals, in macro format, themselves? NULL indicates no error. Performance is the same.
    – 2501
    Commented Jan 19, 2017 at 21:26
  • @2501 - interesting, but it's just not a typical error handling idiom in C. It has issues, for e.g., if the caller wants to check the error code (they'd have to use strcmp rather than ==, and I depending on how the strings were used, I may not be able to change the string text in the future. Also, if I'm still using #define SOME_ERROR "some error" pattern, the linker may or may not combine the identical strings, which can cause linker-dependent behavior if people do compare the pointers, an so on.
    – SODIMM
    Commented Jan 19, 2017 at 21:29
  • ... and furthermore, I can't easily change my exposed API at this point (at least not for some internal reason like error declaration elegance!). Cool idea though.
    – SODIMM
    Commented Jan 19, 2017 at 21:30
  • Actually if you use external variables, you can use ==. There won't be an unexpected behavior.
    – 2501
    Commented Jan 19, 2017 at 21:31

4 Answers 4

6

I think there's a better way to do this using something called the X-macro. This technique allows you to define all of your codes and messages in one place and extract the relevant pieces from your table at compile-time. Attached below is a sample of (working) C code that does what you are expecting.

#include <stdio.h>

#define ERR_TABLE(V)                                                           \
  V(SOMETHING_WENT_WRONG,                                                      \
    "Something bad happened, but I don't know more than that")                 \
  V(SOLAR_FLARE_DECTECTED,                                                     \
    "A solar flare, rather than programmer error, has caused a malfunction")   \
  V(ANTS_IN_YOUR_CPU,                                                          \
    "Ants have nested in your CPU, causing all ALU functions to fail")

#define ERR_ID(id, text) id,
enum ErrId {
  ERR_TABLE(ERR_ID)
  kNumErrs,
};
#undef ERR_ID

const char* err_to_string(enum ErrId id) {
#define ERR_TEXT(id, text) text,
  static const char* table[] = {
    ERR_TABLE(ERR_TEXT)
  };
#undef ERR_TEXT
  if (id < 0 || id >= kNumErrs) {
    return "I heard you like errors in your error handling. So ...";
  }
  return table[id];
}

int main() {
  printf("%s\n", err_to_string(SOMETHING_WENT_WRONG));
  printf("%s\n", err_to_string(SOLAR_FLARE_DECTECTED));
  printf("%s\n", err_to_string(ANTS_IN_YOUR_CPU));
  printf("%s\n", err_to_string(100));
  printf("%s\n", err_to_string(-1));
  return 0;
}

And the output:

maple% gcc err.c -o err
maple% ./err 
Something bad happened, but I don't know more than that
A solar flare, rather than programmer error, has caused a malfunction
Ants have nested in your CPU, causing all ALU functions to fail
I heard you like errors in your error handling. So ...
I heard you like errors in your error handling. So ...
maple% 

If you want to leave the message off, just have the ERR_TEXT macro stringify the id (#id). If you want to define custom codes, you can add those as a separate field in the macro.

5
  • Ah, I noticed @StoryTeller posted a comment mentioning this.
    – tekknolagi
    Commented May 15, 2019 at 4:10
  • The most complete solution even though is not looking minimal. And some nice preprocess tricks here.
    – MaxC
    Commented Nov 18, 2020 at 22:00
  • I wanna do something like that in a very minimal microcontroller, so both program size and run time are very important. Ideally I would like something like: GET_CODE(MY_CONSTANT) which would return and int and then GET_STRING(MY_CONSTANT), that would return the associated string (GET_CODE is a little redundant, MY_CONSTANT could be used directly). Afaik something like that would be solved by the pre-processor, so no extra memory or run time would be needed. Could somebody comment on how this solution compares to that? Does err_to_string take up CPU time? do the tables require actual memory?
    – viterbi
    Commented Mar 31, 2021 at 23:20
  • @viterbi err_to_string is 1-2 comparisons for validity checking followed by 1 load. The tables require memory, yes.
    – tekknolagi
    Commented Apr 2, 2021 at 2:07
  • so I guess my question is, is there any way to have codes linked to strings which is compact, elegant, and totally handled by the preprocessor?
    – viterbi
    Commented Apr 2, 2021 at 5:00
5

When I first came in this issue, I made it so that for every macro, I added a _STR macro next to it:

#define SOMETHING_WENT_WRONG     -1
#define SOMETHING_WENT_WRONG_STR "Something went wrong"

#define SOLAR_FLARE_DECTECTED  -2
#define SOLAR_FLARE_DECTECTED_STR "Solar flare detected"

#define ANTS_IN_YOUR_CPU       -3
#define ANTS_IN_YOUR_CPU_STR "Ants in your CPU"

const char *errorstr;
int errorno;
#define SET_ERROR(e) errorno = e; errorstr = e##_STR;

That way I could simply call SET_ERROR(ANTS_IN_YOUR_CPU) and the errorno and string would be taken care of.

3
  • You cannot switch on a non-integer value and cases must be also constants.
    – 2501
    Commented Jan 19, 2017 at 21:57
  • On a 64-bit machine, these pointer values would be of no use as return values from main().
    – r3mainer
    Commented Jan 19, 2017 at 21:59
  • @2501 You're absolutely right, I just took that part out all together.
    – Dellowar
    Commented Jan 19, 2017 at 22:29
4
enum errs {
    __ERR_NONE,
    SOMETHING_WENT_WRONG,
    SOLAR_FLARE_DECTECTED,
    ANTS_IN_YOUR_CPU,
    __ERR_MAX,
};
static const char * const errorstrings[__ERR_MAX] = {
    "__ERR_NONE",
    "Something bad happened, but I don't know more than that",
    "A solar flare, rather than programmer error, has caused a malfunction",
    "Ants have nested in your CPU, causing all ALU functions to fail",
};

//index the error strings by the error value
printf("%s\n", errorstrings[SOMETHING_WENT_WRONG]); // Something bad happened, but I don't know more than that

You can do some pretty clean conversion using the integers as indexes for the error strings.

1
  • This approach seems reasonable, although it has the downside of not putting the error code and message side-by-side and needing to keep the two structures in sync by "eyeballing it" - i.e., if you insert an error in the list of 100 you need to update the array in exactly the right place. I guess you can add a compile-time assert that at least the errorstrings array and the errs have the same size, to catch missed additions.
    – SODIMM
    Commented Jan 19, 2017 at 21:37
-2

Use a hash table where error codes as keys and string pointers as values. Bad news is that you need to fill it manually at runtime and good one is that you can use any error codes. Signed or even float numbers.

3
  • C does not have hash tables, and you don't need one for integer indexes. An array of strings will suffice.
    – DYZ
    Commented Jan 20, 2017 at 3:28
  • Someone does not allow you to define your own hash table? And how do you use negative numbers as indexes? And how do you will synchronise all in the case you remove one code from the array?
    – Ariel
    Commented Jan 20, 2017 at 11:46
  • (1) What's the point? (2) array[-code]; (3) Just keep the slots unused.
    – DYZ
    Commented Jan 20, 2017 at 17:47

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