Czy istnieje możliwość konwersji nazw modułu wyliczającego na ciąg w C?
Odpowiedzi:
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];
}
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:
P99_DECLARE_ENUM(color, red, green, blue);
w pliku nagłówkowym i
P99_DEFINE_ENUM(color);
w jednej jednostce kompilacji (plik .c) powinno załatwić sprawę, w tym przykładzie funkcja zostanie wywołana color_getname
.
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 ).
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 );
}
Kolor jest CZERWONY.
Dostępne są 3 kolory.
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:
LC_errors__strings [-1] ==> LC_errors__strings [LC_RESPONSE_GENERIC_ERROR] ==> "LC_RESPONSE_GENERIC_ERROR"
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.
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.
printf("enum apple as a string: %s\n", fruit_str[APPLE]);
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.
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);
)
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 };
Zwykle robię to:
#define COLOR_STR(color) \
(RED == color ? "red" : \
(BLUE == color ? "blue" : \
(GREEN == color ? "green" : \
(YELLOW == color ? "yellow" : "unknown"))))