在C语言中,如何简单地将枚举类型的变量转换为字符串?

96

我想要做的是:

typedef enum { ONE, TWO, THREE } Numbers;

我正在尝试编写一个类似于以下的switch case函数:

char num_str[10];
int process_numbers_str(Numbers num) {
  switch(num) {
    case ONE:
    case TWO:
    case THREE:
    {
      strcpy(num_str, num); //some way to get the symbolic constant name in here?
    } break;
    default:
      return 0; //no match
  return 1;
}

有没有一种方法可以使用枚举变量来设置,而不是在每种情况下都进行定义,就像我上面尝试的那样?

20个回答

73

这里可以使用来自“将某些东西同时作为C标识符和字符串”的技术

通常情况下,使用预处理器的部分编写和理解可能很困难,包括将宏传递给其他宏并使用#和##操作符,但是使用它非常容易。我发现这种风格在长枚举中非常有用,因为维护两个相同列表可能会带来麻烦。

工厂代码 - 仅需键入一次,通常隐藏在头文件中:

enumFactory.h:

// expansion macro for enum value definition
#define ENUM_VALUE(name,assign) name assign,

// expansion macro for enum to string conversion
#define ENUM_CASE(name,assign) case name: return #name;

// expansion macro for string to enum conversion
#define ENUM_STRCMP(name,assign) if (!strcmp(str,#name)) return name;

/// declare the access function and define enum values
#define DECLARE_ENUM(EnumType,ENUM_DEF) \
  enum EnumType { \
    ENUM_DEF(ENUM_VALUE) \
  }; \
  const char *GetString(EnumType dummy); \
  EnumType Get##EnumType##Value(const char *string); \

/// define the access function names
#define DEFINE_ENUM(EnumType,ENUM_DEF) \
  const char *GetString(EnumType value) \
  { \
    switch(value) \
    { \
      ENUM_DEF(ENUM_CASE) \
      default: return ""; /* handle input error */ \
    } \
  } \
  EnumType Get##EnumType##Value(const char *str) \
  { \
    ENUM_DEF(ENUM_STRCMP) \
    return (EnumType)0; /* handle input error */ \
  } \

使用工厂

someEnum.h:

#include "enumFactory.h"
#define SOME_ENUM(XX) \
    XX(FirstValue,) \
    XX(SecondValue,) \
    XX(SomeOtherValue,=50) \
    XX(OneMoreValue,=100) \

DECLARE_ENUM(SomeEnum,SOME_ENUM)

someEnum.cpp:

#include "someEnum.h"
DEFINE_ENUM(SomeEnum,SOME_ENUM)

这项技术可以轻松扩展,使XX宏接受更多参数,您还可以准备更多的宏来替换XX以满足不同需求,类似于此示例中提供的三个宏。

与使用#include / #define / #undef的X-Macros的比较

尽管与其他人提到的X-Macros类似,但我认为这种解决方案更加优雅,因为它不需要#undef任何内容,这允许您隐藏更多复杂的内容是在头文件工厂中 - 当您需要定义新枚举时,您根本不会触及头文件,因此新的枚举定义更短和更清晰。


4
我不确定你如何说这比x-macros更好/更差 - 这就是x-macros。 SOME_ENUM(XX)正是X-macro(确切地说,是传递XX函数而不是使用#def``#undef的“用户形式”),然后再将整个X-MACRO传递给DEFINE_ENUM来使用它。并不是要贬低这种解决方案 - 它很有效。只是为了澄清这是X macros的用法。 - BeeOnRope
1
@BeeOnRope 您提到的差异是显著的,并且将此解决方案与 惯用的 X 宏(例如 维基百科的示例)区分开来。将 XX 作为参数传递的优点是前者模式可用于宏展开中。请注意,仅有的其他与此解决方案一样简洁的解决方案都需要创建并多次包含一个单独的文件以定义新枚举类型。 - pmttavara
2
另一个技巧是使用枚举名称作为宏名称。您可以简单地编写 #define DEFINE_ENUM(EnumType) ...,将 ENUM_DEF(...) 替换为 EnumType(...),并让用户说 #define SomeEnum(XX) ...。当后面跟着括号时,C 预处理器会在上下文中扩展 SomeEnum 为宏调用,并在其他情况下扩展为常规标记。(当然,如果用户喜欢使用 SomeEnum(2) 来转换为枚举类型而不是 (SomeEnum)2static_cast<SomeEnum>(2),这会导致问题。) - pmttavara
1
@pmttavara - 如果快速搜索是任何指示,则x-macro的最常见用法使用固定的内部宏名称,以及#define#undef。您是否不同意“用户表单”(例如建议在此文章底部)是x-macro的一种类型?我肯定也一直称其为x-macro,在我最近使用的C代码库中,它是最常见的形式(这显然是一种有偏见的观察)。尽管如此,我可能一直对原始帖子的解析有误。 - BeeOnRope
2
@BeeOnRope 目前的措辞是由编辑所做,因为你当时说服我这是 x-macro,即使它可能是一种较少使用的形式(或至少在文章中提及得较少)。 - Suma
显示剩余3条评论

61
// Define your enumeration like this (in say numbers.h);
ENUM_BEGIN( Numbers )
    ENUM(ONE),
    ENUM(TWO),
    ENUM(FOUR)
ENUM_END( Numbers )

// The macros are defined in a more fundamental .h file (say defs.h);
#define ENUM_BEGIN(typ) enum typ {
#define ENUM(nam) nam
#define ENUM_END(typ) };

// Now in one and only one .c file, redefine the ENUM macros and reinclude
//  the numbers.h file to build a string table
#undef ENUM_BEGIN
#undef ENUM
#undef ENUM_END
#define ENUM_BEGIN(typ) const char * typ ## _name_table [] = {
#define ENUM(nam) #nam
#define ENUM_END(typ) };
#undef NUMBERS_H_INCLUDED   // whatever you need to do to enable reinclusion
#include "numbers.h"

// Now you can do exactly what you want to do, with no retyping, and for any
//  number of enumerated types defined with the ENUM macro family
//  Your code follows;
char num_str[10];
int process_numbers_str(Numbers num) {
  switch(num) {
    case ONE:
    case TWO:
    case THREE:
    {
      strcpy(num_str, Numbers_name_table[num]); // eg TWO -> "TWO"
    } break;
    default:
      return 0; //no match
  return 1;
}

// Sweet no ? After being frustrated by this for years, I finally came up
//  with this solution for my most recent project and plan to reuse the idea
//  forever

3
这就是cpp设计的目的所在。+1。 - Derrick Turk
6
这是一个不错的回答,似乎在不使用特殊工具的情况下我们只能做到这个程度,我以前也做过类似的事情;但它仍然从未真正感觉“正确”,我从来没有真正喜欢做它... - Michael Burr
小改动:在“defs.h”文件中加入 #define ENUM_END(typ) }; extern const char * typ ## _name_table[]; - 这将在你使用它的文件中声明你的名称表。 (虽然我无法想出一个很好的方法来声明表的大小。)此外,个人认为可以省略最后的分号,但是两种做法的优点各有争议。 - Chris Lutz
1
@Bill,为什么在#define ENUM_END(typ) };这一行里要加上 typ - Pacerier
这个不起作用,我想把我的宏定义为“ONE = 5”。 - UKMonkey

16

没有内置的解决方案。最简单的方法是使用一个char*数组,其中枚举的int值索引到包含该枚举描述名称的字符串。如果您有一个稀疏的enum(不从0开始或编号中存在间隔)其中一些int映射足够高,使基于数组的映射变得不切实际,则可以改用哈希表。


进一步扩展,如果确实是一个线性递增的列表,您可以使用编辑器的宏工具记录和解析每个名称到字符串。几乎不需要额外的输入,也就避免了一开始需要定义的麻烦。我在复制的宏的最后点击记录,添加引号并继续到下一行的同样位置。我停止录制。我按运行X次并执行所有操作(或只是单步)。然后我可以将其包装在一个字符串数组中。 - user2262111

13

肯定有一种方法可以做到这一点--使用X()宏。这些宏使用C预处理器从源数据列表构建枚举,数组和代码块。您只需要将新项目添加到包含X()宏的#定义中即可。switch语句将自动扩展。

您的示例可以编写如下:

 // Source data -- Enum, String
 #define X_NUMBERS \
    X(ONE,   "one") \
    X(TWO,   "two") \
    X(THREE, "three")

 ...

 // Use preprocessor to create the Enum
 typedef enum {
  #define X(Enum, String)       Enum,
   X_NUMBERS
  #undef X
 } Numbers;

 ...

 // Use Preprocessor to expand data into switch statement cases
 switch(num)
 {
 #define X(Enum, String) \
     case Enum:  strcpy(num_str, String); break;
 X_NUMBERS
 #undef X

     default: return 0; break;
 }
 return 1;

还有更高效的方法(比如使用X宏创建一个字符串数组和枚举索引),但这是最简单的演示。


9

我知道你已经有了一些好的实际答案,但是你是否了解C预处理器中的#运算符?

它可以让你做到这样:

#define MACROSTR(k) #k

typedef enum {
    kZero,
    kOne,
    kTwo,
    kThree
} kConst;

static char *kConstStr[] = {
    MACROSTR(kZero),
    MACROSTR(kOne),
    MACROSTR(kTwo),
    MACROSTR(kThree)
};

static void kConstPrinter(kConst k)
{
    printf("%s", kConstStr[k]);
}

char const *kConstStr[] - Anne van Rossum

6
KISS。您将使用枚举执行各种其他开关/案例操作,那么为什么打印不应该不同呢?在考虑到有大约100个其他地方可以忘记一个案例时,在您的打印例程中忘记一个案例并不是一件大事。只需编译-Wall,它将警告非穷尽案例匹配。不要使用“默认”,因为这将使开关穷尽,您将不会收到警告。相反,让开关退出并像这样处理默认情况...
const char *myenum_str(myenum e)
{
    switch(e) {
    case ONE: return "one";
    case TWO: return "two";
    }
    return "invalid";
}

6

C或C++并没有提供这个功能,尽管我经常需要它。

下面的代码可以工作,但最适合非稀疏枚举。

typedef enum { ONE, TWO, THREE } Numbers;
char *strNumbers[] = {"one","two","three"};
printf ("Value for TWO is %s\n",strNumbers[TWO]);

非稀疏表示指不符合以下形式:

typedef enum { ONE, FOUR_THOUSAND = 4000 } Numbers;

因为这个方法存在很大的缺陷。

这种方法的优点在于它将枚举和字符串的定义放在彼此附近;在函数中使用switch语句会将它们分开。这意味着你更不可能改变一个而不改变另一个。


5

4

通过将这里的一些技术合并,我得出了最简单的形式:

#define MACROSTR(k) #k

#define X_NUMBERS \
       X(kZero  ) \
       X(kOne   ) \
       X(kTwo   ) \
       X(kThree ) \
       X(kFour  ) \
       X(kMax   )

enum {
#define X(Enum)       Enum,
    X_NUMBERS
#undef X
} kConst;

static char *kConstStr[] = {
#define X(String) MACROSTR(String),
    X_NUMBERS
#undef X
};

int main(void)
{
    int k;
    printf("Hello World!\n\n");

    for (k = 0; k < kMax; k++)
    {
        printf("%s\n", kConstStr[k]);
    }

    return 0;
}

4
使用boost::preprocessor可以实现以下优雅的解决方案:
步骤1:包含头文件:
#include "EnumUtilities.h"

第二步:使用以下语法声明枚举对象:
MakeEnum( TestData,
         (x)
         (y)
         (z)
         );

步骤三:使用您的数据:

获取元素数量:

td::cout << "Number of Elements: " << TestDataCount << std::endl;

获取相关的字符串:
std::cout << "Value of " << TestData2String(x) << " is " << x << std::endl;
std::cout << "Value of " << TestData2String(y) << " is " << y << std::endl;
std::cout << "Value of " << TestData2String(z) << " is " << z << std::endl;

从关联的字符串中获取枚举值:
std::cout << "Value of x is " << TestData2Enum("x") << std::endl;
std::cout << "Value of y is " << TestData2Enum("y") << std::endl;
std::cout << "Value of z is " << TestData2Enum("z") << std::endl;

这看起来干净而紧凑,没有额外的文件需要包含。 我在EnumUtilities.h中编写的代码如下:

#include <boost/preprocessor/seq/for_each.hpp>
#include <string>

#define REALLY_MAKE_STRING(x) #x
#define MAKE_STRING(x) REALLY_MAKE_STRING(x)
#define MACRO1(r, data, elem) elem,
#define MACRO1_STRING(r, data, elem)    case elem: return REALLY_MAKE_STRING(elem);
#define MACRO1_ENUM(r, data, elem)      if (REALLY_MAKE_STRING(elem) == eStrEl) return elem;


#define MakeEnum(eName, SEQ) \
    enum eName { BOOST_PP_SEQ_FOR_EACH(MACRO1, , SEQ) \
    last_##eName##_enum}; \
    const int eName##Count = BOOST_PP_SEQ_SIZE(SEQ); \
    static std::string eName##2String(const enum eName eel) \
    { \
        switch (eel) \
        { \
        BOOST_PP_SEQ_FOR_EACH(MACRO1_STRING, , SEQ) \
        default: return "Unknown enumerator value."; \
        }; \
    }; \
    static enum eName eName##2Enum(const std::string eStrEl) \
    { \
        BOOST_PP_SEQ_FOR_EACH(MACRO1_ENUM, , SEQ) \
        return (enum eName)0; \
    };

有一些限制,比如boost::preprocessor的限制。在这种情况下,常量列表不能超过64个元素。

按照相同的逻辑,您也可以考虑创建稀疏枚举:

#define EnumName(Tuple)                 BOOST_PP_TUPLE_ELEM(2, 0, Tuple)
#define EnumValue(Tuple)                BOOST_PP_TUPLE_ELEM(2, 1, Tuple)
#define MACRO2(r, data, elem)           EnumName(elem) EnumValue(elem),
#define MACRO2_STRING(r, data, elem)    case EnumName(elem): return BOOST_PP_STRINGIZE(EnumName(elem));

#define MakeEnumEx(eName, SEQ) \
    enum eName { \
    BOOST_PP_SEQ_FOR_EACH(MACRO2, _, SEQ) \
    last_##eName##_enum }; \
    const int eName##Count = BOOST_PP_SEQ_SIZE(SEQ); \
    static std::string eName##2String(const enum eName eel) \
    { \
        switch (eel) \
        { \
        BOOST_PP_SEQ_FOR_EACH(MACRO2_STRING, _, SEQ) \
        default: return "Unknown enumerator value."; \
        }; \
    };  

在这种情况下,语法是:
MakeEnumEx(TestEnum,
           ((x,))
           ((y,=1000))
           ((z,))
           );

使用方法与上面类似(减去eName##2Enum函数,您可以尝试从前面的语法推断出来)。

我在Mac和Linux上测试了它,但请注意boost::preprocessor可能不是完全可移植的。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接