Czy istnieje możliwość konwersji nazw modułu wyliczającego na ciąg w C?
Jeden sposób, zmuszając preprocesor do wykonania pracy. Zapewnia również synchronizację wyliczeń i ciągów.
#define FOREACH_FRUIT(FRUIT) \
FRUIT(apple) \
FRUIT(orange) \
FRUIT(grape) \
FRUIT(banana) \
#define GENERATE_ENUM(ENUM) ENUM,
#define GENERATE_STRING(STRING) #STRING,
enum FRUIT_ENUM {
FOREACH_FRUIT(GENERATE_ENUM)
};
static const char *FRUIT_STRING[] = {
FOREACH_FRUIT(GENERATE_STRING)
};
Po zakończeniu preprocesora będziesz mieć:
enum FRUIT_ENUM {
apple, orange, grape, banana,
};
static const char *FRUIT_STRING[] = {
"apple", "orange", "grape", "banana",
};
Wtedy możesz zrobić coś takiego:
printf("enum apple as a string: %s\n",FRUIT_STRING[apple]);
Jeśli przypadek użycia polega dosłownie na wypisywaniu nazwy wyliczenia, dodaj następujące makra:
#define str(x) #x
#define xstr(x) str(x)
Następnie wykonaj:
printf("enum apple as a string: %s\n", xstr(apple));
W tym przypadku może się wydawać, że dwupoziomowe makro jest zbędne, jednak ze względu na to, jak działa stringifikacja w C, w niektórych przypadkach jest konieczne. Na przykład, powiedzmy, że chcemy użyć #define z wyliczeniem:
#define foo apple
int main() {
printf("%s\n", str(foo));
printf("%s\n", xstr(foo));
}
Wynik byłby następujący:
foo
apple
Dzieje się tak, ponieważ str będzie traktować dane wejściowe foo zamiast rozszerzać je na apple. Używając xstr, rozwijanie makr jest wykonywane najpierw, a następnie wynik jest określany jako łańcuch.
Aby uzyskać więcej informacji, zobacz Stringification .
#define GENERATE_ENUM(ENUM) PREFIX##ENUM,
W sytuacji, gdy masz to:
enum fruit { apple, orange, grape, banana, // etc. };
Lubię to umieścić w pliku nagłówkowym, w którym zdefiniowano wyliczenie:
static inline char *stringFromFruit(enum fruit f) { static const char *strings[] = { "apple", "orange", "grape", "banana", /* continue for rest of values */ }; return strings[f]; }
źródło
enumToString(apple)
niż pisać"apple"
? To nie jest tak, że nigdzie nie ma żadnego zabezpieczenia typu. Chyba że brakuje mi czegoś, co sugerujesz tutaj, jest bezcelowe i po prostu udaje się zaciemnić kod.Nie ma prostego sposobu na osiągnięcie tego bezpośrednio. Ale P99 ma makra, które pozwalają na automatyczne tworzenie tego typu funkcji:
w pliku nagłówkowym i
w jednej jednostce kompilacji (plik .c) powinno załatwić sprawę, w tym przykładzie funkcja zostanie wywołana
color_getname
.źródło
Znalazłem sztuczkę preprocesora C, która wykonuje tę samą pracę bez deklarowania dedykowanego ciągu tablicy (źródło: http://userpage.fu-berlin.de/~ram/pub/pub_jf47ht81Ht/c_preprocessor_applications_en ).
Wyliczenia sekwencyjne
Zgodnie z wynalazkiem Stefana Rama, sekwencyjne wyliczenia (bez wyraźnego podawania indeksu, np.
enum {foo=-1, foo1 = 1}
) Mogą być realizowane w następujący sposób:#include <stdio.h> #define NAMES C(RED)C(GREEN)C(BLUE) #define C(x) x, enum color { NAMES TOP }; #undef C #define C(x) #x, const char * const color_name[] = { NAMES };
Daje to następujący wynik:
int main( void ) { printf( "The color is %s.\n", color_name[ RED ]); printf( "There are %d colors.\n", TOP ); }
Wyliczenia niesekwencyjne
Ponieważ chciałem zamapować definicje kodów błędów na ciąg tablicowy, aby móc dołączyć surową definicję błędu do kodu błędu (np.
"The error is 3 (LC_FT_DEVICE_NOT_OPENED)."
), Rozszerzyłem kod w ten sposób, aby można było łatwo określić wymagany indeks dla odpowiednich wartości wyliczenia :#define LOOPN(n,a) LOOP##n(a) #define LOOPF , #define LOOP2(a) a LOOPF a LOOPF #define LOOP3(a) a LOOPF a LOOPF a LOOPF #define LOOP4(a) a LOOPF a LOOPF a LOOPF a LOOPF #define LOOP5(a) a LOOPF a LOOPF a LOOPF a LOOPF a LOOPF #define LOOP6(a) a LOOPF a LOOPF a LOOPF a LOOPF a LOOPF a LOOPF #define LOOP7(a) a LOOPF a LOOPF a LOOPF a LOOPF a LOOPF a LOOPF a LOOPF #define LOOP8(a) a LOOPF a LOOPF a LOOPF a LOOPF a LOOPF a LOOPF a LOOPF a LOOPF #define LOOP9(a) a LOOPF a LOOPF a LOOPF a LOOPF a LOOPF a LOOPF a LOOPF a LOOPF a LOOPF #define LC_ERRORS_NAMES \ Cn(LC_RESPONSE_PLUGIN_OK, -10) \ Cw(8) \ Cn(LC_RESPONSE_GENERIC_ERROR, -1) \ Cn(LC_FT_OK, 0) \ Ci(LC_FT_INVALID_HANDLE) \ Ci(LC_FT_DEVICE_NOT_FOUND) \ Ci(LC_FT_DEVICE_NOT_OPENED) \ Ci(LC_FT_IO_ERROR) \ Ci(LC_FT_INSUFFICIENT_RESOURCES) \ Ci(LC_FT_INVALID_PARAMETER) \ Ci(LC_FT_INVALID_BAUD_RATE) \ Ci(LC_FT_DEVICE_NOT_OPENED_FOR_ERASE) \ Ci(LC_FT_DEVICE_NOT_OPENED_FOR_WRITE) \ Ci(LC_FT_FAILED_TO_WRITE_DEVICE) \ Ci(LC_FT_EEPROM_READ_FAILED) \ Ci(LC_FT_EEPROM_WRITE_FAILED) \ Ci(LC_FT_EEPROM_ERASE_FAILED) \ Ci(LC_FT_EEPROM_NOT_PRESENT) \ Ci(LC_FT_EEPROM_NOT_PROGRAMMED) \ Ci(LC_FT_INVALID_ARGS) \ Ci(LC_FT_NOT_SUPPORTED) \ Ci(LC_FT_OTHER_ERROR) \ Ci(LC_FT_DEVICE_LIST_NOT_READY) #define Cn(x,y) x=y, #define Ci(x) x, #define Cw(x) enum LC_errors { LC_ERRORS_NAMES TOP }; #undef Cn #undef Ci #undef Cw #define Cn(x,y) #x, #define Ci(x) #x, #define Cw(x) LOOPN(x,"") static const char* __LC_errors__strings[] = { LC_ERRORS_NAMES }; static const char** LC_errors__strings = &__LC_errors__strings[10];
W tym przykładzie preprocesor C wygeneruje następujący kod :
enum LC_errors { LC_RESPONSE_PLUGIN_OK=-10, LC_RESPONSE_GENERIC_ERROR=-1, LC_FT_OK=0, LC_FT_INVALID_HANDLE, LC_FT_DEVICE_NOT_FOUND, LC_FT_DEVICE_NOT_OPENED, LC_FT_IO_ERROR, LC_FT_INSUFFICIENT_RESOURCES, LC_FT_INVALID_PARAMETER, LC_FT_INVALID_BAUD_RATE, LC_FT_DEVICE_NOT_OPENED_FOR_ERASE, LC_FT_DEVICE_NOT_OPENED_FOR_WRITE, LC_FT_FAILED_TO_WRITE_DEVICE, LC_FT_EEPROM_READ_FAILED, LC_FT_EEPROM_WRITE_FAILED, LC_FT_EEPROM_ERASE_FAILED, LC_FT_EEPROM_NOT_PRESENT, LC_FT_EEPROM_NOT_PROGRAMMED, LC_FT_INVALID_ARGS, LC_FT_NOT_SUPPORTED, LC_FT_OTHER_ERROR, LC_FT_DEVICE_LIST_NOT_READY, TOP }; static const char* __LC_errors__strings[] = { "LC_RESPONSE_PLUGIN_OK", "" , "" , "" , "" , "" , "" , "" , "" "LC_RESPONSE_GENERIC_ERROR", "LC_FT_OK", "LC_FT_INVALID_HANDLE", "LC_FT_DEVICE_NOT_FOUND", "LC_FT_DEVICE_NOT_OPENED", "LC_FT_IO_ERROR", "LC_FT_INSUFFICIENT_RESOURCES", "LC_FT_INVALID_PARAMETER", "LC_FT_INVALID_BAUD_RATE", "LC_FT_DEVICE_NOT_OPENED_FOR_ERASE", "LC_FT_DEVICE_NOT_OPENED_FOR_WRITE", "LC_FT_FAILED_TO_WRITE_DEVICE", "LC_FT_EEPROM_READ_FAILED", "LC_FT_EEPROM_WRITE_FAILED", "LC_FT_EEPROM_ERASE_FAILED", "LC_FT_EEPROM_NOT_PRESENT", "LC_FT_EEPROM_NOT_PROGRAMMED", "LC_FT_INVALID_ARGS", "LC_FT_NOT_SUPPORTED", "LC_FT_OTHER_ERROR", "LC_FT_DEVICE_LIST_NOT_READY", };
Wynika to z następujących możliwości implementacji:
źródło
Nie musisz polegać na preprocesorze, aby zapewnić synchronizację wyliczeń i ciągów. Według mnie używanie makr sprawia, że kod jest trudniejszy do odczytania.
Używanie wyliczenia i tablicy ciągów
enum fruit { APPLE = 0, ORANGE, GRAPE, BANANA, /* etc. */ FRUIT_MAX }; const char * const fruit_str[] = { [BANANA] = "banana", [ORANGE] = "orange", [GRAPE] = "grape", [APPLE] = "apple", /* etc. */ };
Uwaga: ciągi w
fruit_str
tablicy nie muszą być deklarowane w tej samej kolejności co elementy wyliczenia.Jak tego użyć
printf("enum apple as a string: %s\n", fruit_str[APPLE]);
Dodawanie kontroli czasu kompilacji
Jeśli boisz się zapomnieć o jednym sznurku, możesz dodać następujące sprawdzenie:
#define ASSERT_ENUM_TO_STR(sarray, max) \ typedef char assert_sizeof_##max[(sizeof(sarray)/sizeof(sarray[0]) == (max)) ? 1 : -1] ASSERT_ENUM_TO_STR(fruit_str, FRUIT_MAX);
Błąd byłby zgłaszany w czasie kompilacji, gdyby ilość elementów wyliczeniowych nie odpowiadała ilości ciągów w tablicy.
źródło
Taka funkcja bez walidacji wyliczenia jest trochę niebezpieczna. Sugeruję użycie instrukcji przełącznika. Kolejną zaletą jest to, że można tego użyć dla wyliczeń, które mają zdefiniowane wartości, na przykład dla flag, w których wartości to 1,2,4,8,16 itd.
Umieść również wszystkie ciągi wyliczeniowe w jednej tablicy: -
static const char * allEnums[] = { "Undefined", "apple", "orange" /* etc */ };
zdefiniuj indeksy w pliku nagłówkowym: -
#define ID_undefined 0 #define ID_fruit_apple 1 #define ID_fruit_orange 2 /* etc */
Ułatwia to tworzenie różnych wersji, na przykład jeśli chcesz stworzyć międzynarodowe wersje swojego programu w innych językach.
Używając makra, również w pliku nagłówkowym: -
#define CASE(type,val) case val: index = ID_##type##_##val; break;
Stwórz funkcję za pomocą instrukcji switch, powinna ona zwrócić a,
const char *
ponieważ ciągi znaków static consts: -const char * FruitString(enum fruit e){ unsigned int index; switch(e){ CASE(fruit, apple) CASE(fruit, orange) CASE(fruit, banana) /* etc */ default: index = ID_undefined; } return allEnums[index]; }
W przypadku programowania w systemie Windows wartości ID_ mogą być wartościami zasobów.
(Jeśli używasz C ++, wszystkie funkcje mogą mieć tę samą nazwę.
string EnumToString(fruit e);
)
źródło
Prostsza alternatywa dla odpowiedzi „Non-Sequential enums” Hokyo, oparta na wykorzystaniu desygnatorów do utworzenia wystąpienia tablicy ciągów:
#define NAMES C(RED, 10)C(GREEN, 20)C(BLUE, 30) #define C(k, v) k = v, enum color { NAMES }; #undef C #define C(k, v) [v] = #k, const char * const color_name[] = { NAMES };
źródło
Zwykle robię to:
#define COLOR_STR(color) \ (RED == color ? "red" : \ (BLUE == color ? "blue" : \ (GREEN == color ? "green" : \ (YELLOW == color ? "yellow" : "unknown"))))
źródło