I am building an application with a lot of integer values which need to be mapped to strings for user CLI and logging.
Using array LUT does not provide boundary checks so I want to generate conversion functions to make sure invalid values will be handled properly.
The idea is to use a function like this:
void *enumStr(void *e, bool isToStr)
{
if (isToStr && e == NUM_ZERO) return "zero"; if (!isToStr && strcmp(e, "zero") == 0) return 0;
if (isToStr && e == NUM_ONE) return "one"; if (!isToStr && strcmp(e, "one") == 0) return 1;
if (isToStr && e == NUM_TWO) return "two"; if (!isToStr && strcmp(e, "two") == 0) return 2;
if (isToStr) return "ENUM_N/A"; else return UINT_MAX;
}
which converts between a number and a string and to add two helper functions which hide the boolean argument.
#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>
#include <string.h>
#include <limits.h>
#define ENUMSTR_START(enumName) \
void * _ ## enumName ## _(void *e, bool isToStr) {
#define ENUMSTR_ITEM(enumItem, enumStr) \
if (isToStr && e == enumItem) return enumStr; if (!isToStr && strcmp(e, enumStr) == 0) return enumItem
#define ENUMSTR_END(enumName) \
if (isToStr) return "STR_N/A"; else return UINT_MAX; } \
const char * enumName ## _toStr(uint32_t enumItem) { return (const char * ) _ ## enumName ## _ (enumItem, true); } \
uint32_t enumName ## _fromStr(const char *enumStr) { return _ ## enumName ## _ (enumStr, false); }
typedef enum
{
NUM_ZERO,
NUM_ONE,
NUM_TWO
} NUM;
ENUMSTR_START(NUM)
ENUMSTR_ITEM(NUM_ZERO, "zero");
ENUMSTR_ITEM(NUM_ONE, "one");
ENUMSTR_ITEM(NUM_TWO, "two");
ENUMSTR_END(NUM)
int main(void) {
printf("test 0 %s\n", NUM_toStr(NUM_ZERO));
printf("test 1 %s\n", NUM_toStr(NUM_ONE));
printf("test 2 %s\n", NUM_toStr(NUM_TWO));
printf("test 32 %s\n", NUM_toStr(32));
printf("test zero %u\n", NUM_fromStr("zero"));
printf("test one %u\n", NUM_fromStr("one"));
printf("test two %u\n", NUM_fromStr("two"));
printf("test three %u\n", NUM_fromStr("three"));
return 0;
}
Output:
test 0 zero
test 1 one
test 2 two
test 32 STR_N/A
test zero 0
test one 1
test two 2
test three 4294967295
1 Answer 1
Macro names are all-caps - that's conventional and good.
Enum values are also all-caps - that makes them look like macros, which is not good.
#define ENUMSTR_START(enumName) \ void * _ ## enumName ## _(void *e, bool isToStr) {
This appears to create a function whose name begins with _
- that's not good, as all such names are reserved for the implementation. Even worse when enumName
begins with a capital letter, as this could collide with macros!
I don't like the single function switching between conversions. Is there really a use-case where we decide which direction to convert at run-time?
Much better to define a pair of functions, one for each direction of conversion - enumName str_to_enumName(const char*)
and const char* enumName_to_str(enumName)
.
This is a standard use case for an X-macro, where we define a macro that can apply another macro to each of the members of the enum:
#include <string.h>
#include <limits.h>
#define X_ENUM_VALUE(v, s) v,
#define X_TO_STR(v, s) case v: return s;
#define X_FROM_STR(v, s) if (!strcmp(x, s)) { return v; }
#define MAKE_ENUM(X, name) \
\
typedef enum { \
X(X_ENUM_VALUE) \
} name; \
\
const char *name##_to_str(name x) \
{ \
switch(x) { \
X(X_TO_STR) \
default: return "ENUM_N/A"; \
} \
} \
\
name str_to_##name(const char* x) \
{ \
X(X_FROM_STR) \
return UINT_MAX; \
}
Then for each enum, we just need something like
#define ENUM_NUM(DO) \
DO(num_zero, "Zero") \
DO(num_one, "One") \
DO(num_two, "Two") \
MAKE_ENUM(ENUM_NUM, num)
Simple demo of the above:
#include <stdio.h>
int main()
{
printf("%s\n", num_to_str(num_one));
printf("%d\n", str_to_num("Two"));
}
1
from the functionvoid *enumStr(void *e, bool isToStr)
is not valid C as arbitrary integer values cannot certainly convert to avoid *
. You need a new approach. Please explain more about your needs and usage limitations. \$\endgroup\$