Jak przekonwertować nazwy wyliczeń na ciąg w c

92

Czy istnieje możliwość konwersji nazw modułu wyliczającego na ciąg w C?

Misha
źródło

Odpowiedzi:

187

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 .

Terrence M.
źródło
1
To jest idealne, ale nie jestem w stanie zrozumieć, co się naprawdę dzieje. : O
p0lAris
Jak w powyższym przypadku przekonwertować ciąg na wyliczenie?
p0lAris
Można to zrobić na kilka sposobów, w zależności od tego, co próbujesz osiągnąć?
Terrence M
5
Jeśli nie chcesz zanieczyszczać przestrzeni nazw jabłkami i pomarańczami ... możesz poprzedzić ją prefiksem#define GENERATE_ENUM(ENUM) PREFIX##ENUM,
jsaak
1
Dla tych, którzy natkną się na ten post, ta metoda użycia listy makr do wyliczenia różnych elementów w programie jest nieformalnie nazywana „makrami X”.
Lundin
27

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];
}
Richard J. Ross III
źródło
4
Na całe życie nie rozumiem, jak to pomaga. Czy mógłbyś trochę rozszerzyć, aby było to bardziej oczywiste.
David Heffernan,
2
OK, jak to pomaga? Czy twierdzisz, że łatwiej jest pisać 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.
David Heffernan,
2
OK, teraz rozumiem. Moim zdaniem makro jest fałszywe i sugeruję, abyś je usunął.
David Heffernan,
2
komentarze mówią o makro. Gdzie to jest?
mk ..
2
Jest to również niewygodne w utrzymaniu. Jeśli wstawię nowe wyliczenie, muszę pamiętać, aby zduplikować to również w tablicy, we właściwej pozycji.
Fabio
14

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.

Jens Gustedt
źródło
Jak mogę pobrać tę bibliotekę?
JohnyTex
14

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 ); 
}

Kolor jest CZERWONY.
Dostępne są 3 kolory.

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:

LC_errors__strings [-1] ==> LC_errors__strings [LC_RESPONSE_GENERIC_ERROR] ==> "LC_RESPONSE_GENERIC_ERROR"

Maschina
źródło
Ładny. To jest dokładnie to, czego szukałem i do czego go używałem. Te same błędy :)
mrbean
6

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_strtablicy 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.

jyvet
źródło
2

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);

)

QuentinUK
źródło
2

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 };
Lars
źródło
-2

Zwykle robię to:

#define COLOR_STR(color)                            \
    (RED       == color ? "red"    :                \
     (BLUE     == color ? "blue"   :                \
      (GREEN   == color ? "green"  :                \
       (YELLOW == color ? "yellow" : "unknown"))))   
gigilibala
źródło
To nie jest zła odpowiedź. Jest jasny, prosty i łatwy do zrozumienia. Jeśli pracujesz w systemach, w których inni ludzie muszą szybko przeczytać i zrozumieć Twój kod, przejrzystość jest bardzo ważna. Nie polecałbym używania sztuczek preprocesora, chyba że są one dokładnie skomentowane lub opisane w standardzie kodowania.
nielsen