有没有一种简单的方法将C++枚举转换为字符串?

139

假设我们有一些命名的枚举:

enum MyEnum {
      FOO,
      BAR = 0x50
};

我所需要的是一个脚本(任何语言),可以扫描我的项目中的所有标题,并生成一个包含每个枚举值的函数的标题。
char* enum_to_string(MyEnum t);

以下是类似于此的实现:

char* enum_to_string(MyEnum t){
      switch(t){
         case FOO:
            return "FOO";
         case BAR:
            return "BAR";
         default:
            return "INVALID ENUM";
      }
 }

问题出在typedef枚举和未命名的C风格枚举上。有没有人对此有解决方案?
编辑:解决方案不应该修改我的源代码,除了生成的函数。这些枚举在API中,因此目前提出的解决方案都不可行。

关于基于宏的工厂的答案已经移动到https://dev59.com/-XVC5IYBdhLWcg3w51ry - 在问题更新后,它在这里不再相关。 - Suma
35个回答

76

X-macros是最好的解决方案。示例:

#include <iostream>

enum Colours {
#   define X(a) a,
#   include "colours.def"
#   undef X
    ColoursCount
};

char const* const colours_str[] = {
#   define X(a) #a,
#   include "colours.def"
#   undef X
    0
};

std::ostream& operator<<(std::ostream& os, enum Colours c)
{
    if (c >= ColoursCount || c < 0) return os << "???";
    return os << colours_str[c];
}

int main()
{
    std::cout << Red << Blue << Green << Cyan << Yellow << Magenta << std::endl;
}

颜色定义:

X(Red)
X(Green)
X(Blue)
X(Cyan)
X(Yellow)
X(Magenta)

然而,我通常更喜欢以下的方法,这样就可以稍微调整字符串。

#define X(a, b) a,
#define X(a, b) b,

X(Red, "red")
X(Green, "green")
// etc.

15
灵巧的,尽管我不喜欢额外的文件。 - Ronny Brendel
2
确保您的构建过程不会在每个包含文件之前加入#pragma(once)。 - xtofl
30
我不确定什么是“最好”的解决方案! - Lightness Races in Orbit
2
这个解决方案比任何基于switch case或数组的解决方案都要好得多,因为它不会重复名称,使得更改枚举变得容易。 - Julien Guertault
2
@ikku100,你对于#define X(a, b) #b的理解是错误的。只有在定义看起来像X(Red, red)而不是答案中所示的定义X(Red, "red")时才需要这样做。 - learnvst
显示剩余4条评论

52

@hydroo:没有额外的文件:

#define SOME_ENUM(DO) \
    DO(Foo) \
    DO(Bar) \
    DO(Baz)

#define MAKE_ENUM(VAR) VAR,
enum MetaSyntacticVariable{
    SOME_ENUM(MAKE_ENUM)
};

#define MAKE_STRINGS(VAR) #VAR,
const char* const MetaSyntacticVariableNames[] = {
    SOME_ENUM(MAKE_STRINGS)
};

我喜欢这个解决方案。不过,如果SOME_UNION和MAKE_UNION被称为SOME_ENUM和MAKE_ENUM会更清晰明了。 - Bruno Martinez
这是一个很棒的解决方案。我拥有最易于维护的C++资源管理器,这是我曾经处理过的最好的。 - DCurro
太棒了!我进一步简化了它,通过将MAKE_ENUM和MAKE_STRINGS分组成一个单一的宏,使整个过程更加简单。如果有人感兴趣,我在这个帖子中添加了一个包含该代码的答案。 - Francois Bertrand
有人能解释一下这个解决方案中发生了什么吗?我还是不明白。例如,如果我在main函数之外有enum class MyClr {R, G, B};,那么我应该修改上面的代码的哪些部分,并将其放在哪里?我将我的枚举类放在最顶部,然后将SOME_ENUM替换为MyClr,将FooBarBaz替换为RGB。但是,在main中,当我尝试运行MyClr clr= MyClr::R; cout << MAKE_STRINGS(clr);时,我会收到一个错误:error: expected primary-expression before ‘<<’ token cout << MAKE_STRINGS(clr); ^~ - Milan
我不理解这个解决方案,你能提供一个使用示例吗? - Investing TS
显示剩余2条评论

50

您可能需要查看GCCXML

在您的示例代码上运行GCCXML将会产生:

<GCC_XML>
  <Namespace id="_1" name="::" members="_3 " mangled="_Z2::"/>
  <Namespace id="_2" name="std" context="_1" members="" mangled="_Z3std"/>
  <Enumeration id="_3" name="MyEnum" context="_1" location="f0:1" file="f0" line="1">
    <EnumValue name="FOO" init="0"/>
    <EnumValue name="BAR" init="80"/>
  </Enumeration>
  <File id="f0" name="my_enum.h"/>
</GCC_XML>

您可以使用任何喜欢的语言来提取枚举和枚举值标签,并生成所需的代码。


6
+1,GCCXML看起来非常不错!(虽然我最初误读它是建议使用上面冗长的XML语法来编码您的枚举 - 这种解决方案充满了过度设计的味道!) - j_random_hacker
1
你能否发布一下Python脚本的更改? - phillipwei

36

我倾向于创建一个C数组,其中包含与枚举值相同的名称,顺序和位置也一致。

例如:

enum colours { red, green, blue };
const char *colour_names[] = { "red", "green", "blue" };

那么你可以在需要可读性较好的值的地方使用该数组,例如

colours mycolour = red;
cout << "the colour is" << colour_names[mycolour];

你可以尝试一下字符串化运算符(参见预处理器参考中的#号),在某些情况下会做你想要的事情-例如:

#define printword(XX) cout << #XX;
printword(red);

将会在控制台输出"red"。不幸的是,对于变量来说这种方法行不通(因为你会得到变量名作为输出)。


最后一个警告(对于变量无效)是一个很大的缺点,但还是加1。 - chappjc
4
仅在您不给枚举条目设置特殊数字值时才起作用。 - kyb

15

我有一个极其简单易用的宏,可以以完全DRY的方式实现这一点。它涉及可变参数宏和一些简单的解析魔法。下面是具体内容:

#define AWESOME_MAKE_ENUM(name, ...) enum class name { __VA_ARGS__, __COUNT}; \
inline std::ostream& operator<<(std::ostream& os, name value) { \
std::string enumName = #name; \
std::string str = #__VA_ARGS__; \
int len = str.length(); \
std::vector<std::string> strings; \
std::ostringstream temp; \
for(int i = 0; i < len; i ++) { \
if(isspace(str[i])) continue; \
        else if(str[i] == ',') { \
        strings.push_back(temp.str()); \
        temp.str(std::string());\
        } \
        else temp<< str[i]; \
} \
strings.push_back(temp.str()); \
os << enumName << "::" << strings[static_cast<int>(value)]; \
return os;} 

要在你的代码中使用这个,只需执行以下操作:

AWESOME_MAKE_ENUM(Animal,
    DOG,
    CAT,
    HORSE
);

1
使用强类型枚举(enum class)是个好主意。这里有一个演示:http://cpp.sh/4ife - chappjc
这个能够与外部定义的枚举/符号一起使用吗?例如,具有编号间隔的操作系统定义或库定义的符号? - Jason Harrison
非常好,但如果放在类中就无法编译(我无法弄清原因)。 - AlwaysLearning
我无法在VS2015中编译此代码。我收到了一个警告和一个错误:警告:多行注释[-Wcomment] #define MAKE_ENUM(name, ...) enum class name { VA_ARGS, __COUNT}错误:程序中的杂项'#' std *:string enumName = #name - Craig.Feied
不错,但这似乎只适用于诊断。顺便说一下,关于像“DRY”这样的原则:cout << Animal :: CAT仍应该只打印1(而不是Animal :: CAT),遵循最小惊奇原则。 :) - Sz.
无法在VS2017、icc 19.2中编译,我不知道如何改进: 错误C2601:'operator <<':本地函数定义是非法的。 - Investing TS

11

这可以在C++11中完成。

#include <map>
enum MyEnum { AA, BB, CC, DD };

static std::map< MyEnum, const char * > info = {
   {AA, "This is an apple"},
   {BB, "This is a book"},
   {CC, "This is a coffee"},
   {DD, "This is a door"}
};

void main()
{
    std::cout << info[AA] << endl
              << info[BB] << endl
              << info[CC] << endl
              << info[DD] << endl;
}

2
这并没有回答楼主的问题:他正在寻找一种自动生成函数以将枚举成员的名称作为字符串返回的方法。 - Spooky

8
QT能够实现这一点(多亏了元对象编译器):
QNetworkReply::NetworkError error;

error = fetchStuff();

if (error != QNetworkReply::NoError) {

    QString errorValue;

    QMetaObject meta = QNetworkReply::staticMetaObject;

    for (int i=0; i < meta.enumeratorCount(); ++i) {

        QMetaEnum m = meta.enumerator(i);

        if (m.name() == QLatin1String("NetworkError")) {

            errorValue = QLatin1String(m.valueToKey(error));

            break;

        }

    }

    QMessageBox box(QMessageBox::Information, "Failed to fetch",

                "Fetching stuff failed with error '%1`").arg(errorValue),

                QMessageBox::Ok);

    box.exec();

    return 1;

}

在Qt中,每个带有Q_OBJECT宏的类都会自动拥有一个类型为QMetaObject的静态成员"staticMetaObject"。然后,您可以找到各种很酷的东西,比如属性、信号、槽和枚举类型。来源

8
我今天重新发明了这个轮子,想要分享一下。
这个实现不需要对定义常量的代码做出任何更改,这些常量可以是枚举、#define或者任何其他能转化为整数的东西 - 在我自己的情况中,它们是通过其他符号定义的。它还可以很好地处理稀疏值。它甚至允许为相同的值使用多个名称,始终返回第一个名称。唯一的缺点是需要您制作一个常量表格,当添加新的常量时可能会过时。
struct IdAndName
{
   int          id;
   const char * name;
   bool operator<(const IdAndName &rhs) const { return id < rhs.id; }
};
#define ID_AND_NAME(x) { x, #x }

const char * IdToName(int id, IdAndName *table_begin, IdAndName *table_end)
{
   if ((table_end - table_begin) > 1 && table_begin[0].id > table_begin[1].id)
      std::stable_sort(table_begin, table_end);

   IdAndName searchee = { id, NULL };
   IdAndName *p = std::lower_bound(table_begin, table_end, searchee);
   return (p == table_end || p->id != id) ? NULL : p->name;
}

template<int N>
const char * IdToName(int id, IdAndName (&table)[N])
{
   return IdToName(id, &table[0], &table[N]);
}

一个使用它的示例:

static IdAndName WindowsErrorTable[] =
{
   ID_AND_NAME(INT_MAX),               // flag value to indicate unsorted table
   ID_AND_NAME(NO_ERROR),
   ID_AND_NAME(ERROR_INVALID_FUNCTION),
   ID_AND_NAME(ERROR_FILE_NOT_FOUND),
   ID_AND_NAME(ERROR_PATH_NOT_FOUND),
   ID_AND_NAME(ERROR_TOO_MANY_OPEN_FILES),
   ID_AND_NAME(ERROR_ACCESS_DENIED),
   ID_AND_NAME(ERROR_INVALID_HANDLE),
   ID_AND_NAME(ERROR_ARENA_TRASHED),
   ID_AND_NAME(ERROR_NOT_ENOUGH_MEMORY),
   ID_AND_NAME(ERROR_INVALID_BLOCK),
   ID_AND_NAME(ERROR_BAD_ENVIRONMENT),
   ID_AND_NAME(ERROR_BAD_FORMAT),
   ID_AND_NAME(ERROR_INVALID_ACCESS),
   ID_AND_NAME(ERROR_INVALID_DATA),
   ID_AND_NAME(ERROR_INVALID_DRIVE),
   ID_AND_NAME(ERROR_CURRENT_DIRECTORY),
   ID_AND_NAME(ERROR_NOT_SAME_DEVICE),
   ID_AND_NAME(ERROR_NO_MORE_FILES)
};

const char * error_name = IdToName(GetLastError(), WindowsErrorTable);

IdToName函数依赖于std::lower_bound进行快速查找,需要对表进行排序。如果表中前两个条目排序不正确,则该函数将自动对其进行排序。

编辑:评论让我想到了另一种使用相同原理的方法。宏简化了生成大型switch语句的过程。

#define ID_AND_NAME(x) case x: return #x

const char * WindowsErrorToName(int id)
{
    switch(id)
    {
        ID_AND_NAME(ERROR_INVALID_FUNCTION);
        ID_AND_NAME(ERROR_FILE_NOT_FOUND);
        ID_AND_NAME(ERROR_PATH_NOT_FOUND);
        ID_AND_NAME(ERROR_TOO_MANY_OPEN_FILES);
        ID_AND_NAME(ERROR_ACCESS_DENIED);
        ID_AND_NAME(ERROR_INVALID_HANDLE);
        ID_AND_NAME(ERROR_ARENA_TRASHED);
        ID_AND_NAME(ERROR_NOT_ENOUGH_MEMORY);
        ID_AND_NAME(ERROR_INVALID_BLOCK);
        ID_AND_NAME(ERROR_BAD_ENVIRONMENT);
        ID_AND_NAME(ERROR_BAD_FORMAT);
        ID_AND_NAME(ERROR_INVALID_ACCESS);
        ID_AND_NAME(ERROR_INVALID_DATA);
        ID_AND_NAME(ERROR_INVALID_DRIVE);
        ID_AND_NAME(ERROR_CURRENT_DIRECTORY);
        ID_AND_NAME(ERROR_NOT_SAME_DEVICE);
        ID_AND_NAME(ERROR_NO_MORE_FILES);
        default: return NULL;
    }
}

好的解决方案。但对我来说,我更喜欢使用switch和case,因为它简单易懂。 - Deqing

6
有点晚了,但我真的很喜欢这个模式,因为它可以避免复制粘贴错误,并且如果枚举类型没有映射到字符串,它将无法编译。此外,它非常适用于constexpr,所以可以很好地进行内联处理。它还不需要中间类、switch语句或运行时值。
// Create a mapping between the enum value and the string
#define MY_ENUM_LIST(DECLARE) \
DECLARE(foo, "This is a foo!") \
DECLARE(bar, "This is a bar!") \
DECLARE(bam, "This is a bam!")

// Define the enum officially
enum class MyEnum {
#define ENUM_ENTRY(NAME, TEXT) NAME, // TEXT expressly not used here
    MY_ENUM_LIST(ENUM_ENTRY)
#undef ENUM_ENTRY // Always undef as a good citizen ;)
};

// Create a template function that would fail to compile if called
template <MyEnum KEY> constexpr const char* MyEnumText() {}

// Specialize that bad function with versions that map the enum value to the string declared above
#define ENUM_FUNC(NAME, TEXT) template <> constexpr const char* MyEnumText<MyEnum::NAME>() { return TEXT; }
MY_ENUM_LIST(ENUM_FUNC)
#undef ENUM_FUNC

你使用它的方式非常直接。如果你总是在需要字符串的地方硬编码枚举值,那么你只需调用 MyEnumText 的专门版本即可:
const auto text{::MyEnumText<MyEnum::foo>()}; // inlines beautifully

如果您需要处理动态枚举值,可以添加以下辅助函数:
constexpr const char* MyEnumText(MyEnum key) {
    switch (key) {
#define ENUM_CASE(NAME, TEXT) case MyEnum::NAME: return MyEnumText<MyEnum::NAME>();
        MY_ENUM_LIST(ENUM_CASE)
#undef ENUM_CASE
    }
    return nullptr;
}

这个语法与模板特化类似:

const auto text{::MyEnumText(MyEnum::foo)}; // inlines beautifully

或者
const MyEnum e{GetTheEnumValue()};
const auto text{::MyEnumText(e)};

1
这种模板特化方法很棒。没有静态数组构建。如果缺少字符串,则编译错误,支持具有间隔或不从0开始的枚举(例如错误代码字符串)。只需在同一行中输入ID和字符串(DECLARE行)即可。 - DaveSawyer

5

7
实际上这个方法相当无用,因为将枚举类型转换成字符串的操作是在编译时进行的,并且是非常字面的。如果你将所需的枚举类型放在一个变量内,尝试将该变量转换成字符串将只会给出变量名,而不是枚举类型名。 - srcspider

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