现代C++11/C++14/C++17及未来的C++20中如何将枚举类型转换为字符串

551
与所有其他类似的问题相反,这个问题是关于使用新的C++特性的。

阅读了许多答案,但仍未找到以下内容:

  • 使用C++11C++14C++17的新特性的优雅方法
  • 或者在Boost中已经有现成的东西可用
  • 另外,还有一些计划在C++20中实现的东西

示例

一个例子胜过千言万语的解释。
你可以在Coliru上编译和运行这段代码片段。
另一个之前的示例也可用)

#include <map>
#include <iostream>

struct MyClass
{
    enum class MyEnum : char {
        AAA = -8,
        BBB = '8',
        CCC = AAA + BBB
    };
};

// Replace magic() by some faster compile-time generated code
// (you're allowed to replace the return type with std::string
// if that's easier for you)
const char* magic (MyClass::MyEnum e)
{
    const std::map<MyClass::MyEnum,const char*> MyEnumStrings {
        { MyClass::MyEnum::AAA, "MyClass::MyEnum::AAA" },
        { MyClass::MyEnum::BBB, "MyClass::MyEnum::BBB" },
        { MyClass::MyEnum::CCC, "MyClass::MyEnum::CCC" }
    };
    auto   it  = MyEnumStrings.find(e);
    return it == MyEnumStrings.end() ? "Out of range" : it->second;
}

int main()
{
   std::cout << magic(MyClass::MyEnum::AAA) <<'\n';
   std::cout << magic(MyClass::MyEnum::BBB) <<'\n';
   std::cout << magic(MyClass::MyEnum::CCC) <<'\n';
}

约束条件

  • 请勿无意义地复制其他答案基本链接
  • 请避免使用臃肿的宏,或尽可能减少#define的开销。
  • 请勿手动进行enumstring的映射。

最好有的

  • 支持从非零数字开始的枚举
  • 支持负枚举
  • 支持分段枚举
  • 支持类枚举(C++11)
  • 支持任何允许的<type>class enum: <type>(C++11)
  • 编译时(而非运行时)将枚举转换为字符串,或者至少在运行时快速执行(例如,std::map不是一个好主意...)
  • constexpr(C++11,然后放宽到C++14/17/20)
  • noexcept(C++11)
  • C++17/C++20友好代码片段

一个可能的想法是利用C++编译器的能力,通过基于可变参数模板类和constexpr函数的元编程技巧,在编译时生成C++代码...


4
(可能与主题无关)看看这个与Qt相关的博客。http://woboq.com/blog/reflection-in-cpp-and-qt-moc.html。描述了使用C++反射(拟议中的标准)替换Qt的moc(元对象编译器)的可能性。 - ibre5041
17
N4113std::enumerator::identifier_v<MyEnum, MyEnum::AAA> - ecatmur
3
所有的问题都必须用C++解决吗?自动生成字符串表示代码非常容易,只需要几行代码。 - Karoly Horvath
6
如果可能的话,请不要提供基于 C 宏的答案。除非您愿意等待 C++17,否则几乎没有可用的东西,将枚举声明为DEC_ENUM(enumname, (a,b,c,(d,b),(e,42)))并不是那么糟糕,除非您必须维护生成宏... 在我看来,将这种情况放入语言中只是另一种在缺少更强大的模板/宏混合体的情况下的替代方法。我们不应该为了能够说宏不再有用而将所有这些有用的宏用例添加到语言中。 - PlasmaHH
2
@olibre,这个问题至少有两个今天可用的答案。1. @ecatmur关于C++17的不错回答,我们不能每次C++17讨论更新时都进行编辑。请参阅反射研究小组的邮件列表。2. 我的回答提供了当前C++的不错语法,被许多人在生产中使用,但在内部使用#define。你所问的是一个可用的解决方案。今天的正确答案是,在以后没有完全“正确”的解决方案之前(即现在接受@ecatmur)。 - antron
显示剩余21条评论
32个回答

125

Magic Enum是一个仅包含头文件的库,为C++17提供枚举类型的静态反射支持(包括转换为字符串、从字符串转换和迭代等功能)。

#include <magic_enum.hpp>

enum Color { RED = 2, BLUE = 4, GREEN = 8 };

Color color = Color::RED;
auto color_name = magic_enum::enum_name(color);
// color_name -> "RED"

std::string color_name{"GREEN"};
auto color = magic_enum::enum_cast<Color>(color_name)
if (color.has_value()) {
  // color.value() -> Color::GREEN
};

更多示例请查看主页存储库https://github.com/Neargye/magic_enum

哪里有缺陷?

该库使用编译器特定的 hack(基于__PRETTY_FUNCTION__ / __FUNCSIG__),适用于Clang >= 5,MSVC >= 15.3和GCC >= 9。

枚举值必须在范围内[MAGIC_ENUM_RANGE_MIN,MAGIC_ENUM_RANGE_MAX]

  • 默认情况下,MAGIC_ENUM_RANGE_MIN = -128MAGIC_ENUM_RANGE_MAX = 128

  • 如果需要为所有枚举类型设置另一个范围,请重新定义宏MAGIC_ENUM_RANGE_MINMAGIC_ENUM_RANGE_MAX

  • MAGIC_ENUM_RANGE_MIN必须小于或等于0,并且必须大于INT16_MIN

  • MAGIC_ENUM_RANGE_MAX必须大于0,并且必须小于INT16_MAX

  • 如果需要为特定的枚举类型设置另一个范围,请添加必要的枚举类型的专门化enum_range

    #include <magic_enum.hpp>
    
    enum number { one = 100, two = 200, three = 300 };
    
    namespace magic_enum {
    template <>
      struct enum_range<number> {
        static constexpr int min = 100;
        static constexpr int max = 300;
    };
    }
    

9
为什么有范围限制?是为了限制某种递归深度,还是因为编译时的线性搜索? - Emile Cormier
1
这太棒了。谢谢!如果编译器足够聪明以仅评估constexpr std :: array一次,那么它可能甚至更有效率。非常非常好。 - iestyn
6
范围限制是必要的,因为该库必须探查范围内的每个可能值,以查看它是否与枚举器对应。它为范围[-128,127]中的每个值实例化一个“is_valid”函数模板。这可能导致编译时间很长,因此默认情况下范围相当保守。这是一种简化版本的技术:https://godbolt.org/z/GTxfva - Alastair Harrison
7
对我来说,最重要的缺点是它会默默失败:https://godbolt.org/z/TTMx1v。虽然有值大小的限制,但当约束条件未满足时,不会出现编译错误、异常,只会返回空字符串。 - MateuszL
@Neargye,如果我有一个枚举值超出最小/最大范围,但我只使用枚举转字符串功能,这会有问题吗? - acegs
2
如果一个值超出了最小/最大范围,enum-to-string 将返回一个空字符串。 - Neargye

111

(better_enums库的方法)

目前在C ++中有一种将枚举转换为字符串的方法,看起来像这样:

ENUM(Channel, char, Red = 1, Green, Blue)

// "Same as":
// enum class Channel : char { Red = 1, Green, Blue };

使用方法:

Channel     c = Channel::_from_string("Green");  // Channel::Green (2)
c._to_string();                                  // string "Green"

for (Channel c : Channel::_values())
    std::cout << c << std::endl;

// And so on...

所有操作都可以使用constexpr。您还可以实现@ecatmur提到的C++17反射提案。

  • 只有一个宏。我认为这是最小的可能性,因为预处理器字符串化(#)是当前C ++中将令牌转换为字符串的唯一方法。
  • 宏非常不显眼 - 常量声明(包括初始化程序)被粘贴到内置枚举声明中。这意味着它们具有与内置枚举相同的语法和含义。
  • 消除了重复。
  • 由于constexpr,在至少C++11中,实现最自然和有用。它也可以在C ++ 98 + __VA_ARGS__中使用。它绝对是现代C ++。

该宏的定义有一定的复杂性,因此我将以几种方式进行回答。
  • 本答案的主要部分是我认为适用于StackOverflow空间限制的实现。
  • 此外,还有一个CodeProject文章,介绍了实现的基础知识,是一个长篇教程。[我应该把它移到这里吗?我觉得这对于SO答案来说太多了]。
  • 有一个全功能库 "Better Enums",它在单个头文件中实现了该宏。它还实现了N4428 Type Property Queries,即C++17反射提案N4113的当前修订版。因此,对于通过该宏声明的枚举,在C++11/C++14中现在就可以使用提议中的C++17枚举反射。

将此答案扩展到库的特性非常简单-这里没有遗漏任何“重要”的内容。然而,这相当繁琐,并且存在编译器可移植性问题。

免责声明:我是CodeProject文章和该库的作者。

您可以在Wandbox上在线体验此答案中的代码, 该库以及N4428的实现。该库文档还包含一个如何将其用作N4428的概述,其中解释了该提案的枚举部分。


说明

下面的代码实现了枚举和字符串之间的转换。但是,它也可以扩展到做其他事情,比如迭代。这个答案将一个枚举包装在一个 struct 中。你也可以在一个枚举旁边生成一个 traits struct

策略是生成像这样的东西:

struct Channel {
    enum _enum : char { __VA_ARGS__ };
    constexpr static const Channel          _values[] = { __VA_ARGS__ };
    constexpr static const char * const     _names[] = { #__VA_ARGS__ };

    static const char* _to_string(Channel v) { /* easy */ }
    constexpr static Channel _from_string(const char *s) { /* easy */ }
};

问题如下:
我们最终会得到类似 {Red = 1, Green, Blue} 的值数组初始化器。这不是有效的 C++,因为 Red 不是可分配的表达式。解决方法是将每个常量转换为具有赋值运算符但会丢弃赋值的类型 T{(T)Red = 1, (T)Green, (T)Blue}
同样地,我们最终会得到 {"Red = 1", "Green", "Blue"} 作为名称数组的初始化器。我们需要去掉 " = 1"。我不知道有什么好的方法可以在编译时做到这一点,所以我们将其推迟到运行时。因此,_to_string 将不是 constexpr,但 _from_string 仍然可以是 constexpr,因为我们可以将空格和等号视为未修剪字符串的终止符进行比较。
以上两者都需要一个“映射”宏,该宏可以将另一个宏应用于 __VA_ARGS__ 中的每个元素。这是相当标准的。此答案包括一个简单版本,可以处理多达 8 个元素。
如果宏要真正自包含,它就不能声明需要单独定义的静态数据。实际上,这意味着数组需要特殊处理。有两种可能的解决方案:命名空间作用域中的 constexpr(或只是 const)数组,或者在非 constexpr 静态内联函数中使用常规数组。本答案的代码适用于 C++11,并采用前一种方法。CodeProject 文章适用于 C++98,并采用后一种方法。

代码

#include <cstddef>      // For size_t.
#include <cstring>      // For strcspn, strncpy.
#include <stdexcept>    // For runtime_error.



// A "typical" mapping macro. MAP(macro, a, b, c, ...) expands to
// macro(a) macro(b) macro(c) ...
// The helper macro COUNT(a, b, c, ...) expands to the number of
// arguments, and IDENTITY(x) is needed to control the order of
// expansion of __VA_ARGS__ on Visual C++ compilers.
#define MAP(macro, ...) \
    IDENTITY( \
        APPLY(CHOOSE_MAP_START, COUNT(__VA_ARGS__)) \
            (macro, __VA_ARGS__))

#define CHOOSE_MAP_START(count) MAP ## count

#define APPLY(macro, ...) IDENTITY(macro(__VA_ARGS__))

#define IDENTITY(x) x

#define MAP1(m, x)      m(x)
#define MAP2(m, x, ...) m(x) IDENTITY(MAP1(m, __VA_ARGS__))
#define MAP3(m, x, ...) m(x) IDENTITY(MAP2(m, __VA_ARGS__))
#define MAP4(m, x, ...) m(x) IDENTITY(MAP3(m, __VA_ARGS__))
#define MAP5(m, x, ...) m(x) IDENTITY(MAP4(m, __VA_ARGS__))
#define MAP6(m, x, ...) m(x) IDENTITY(MAP5(m, __VA_ARGS__))
#define MAP7(m, x, ...) m(x) IDENTITY(MAP6(m, __VA_ARGS__))
#define MAP8(m, x, ...) m(x) IDENTITY(MAP7(m, __VA_ARGS__))

#define EVALUATE_COUNT(_1, _2, _3, _4, _5, _6, _7, _8, count, ...) \
    count

#define COUNT(...) \
    IDENTITY(EVALUATE_COUNT(__VA_ARGS__, 8, 7, 6, 5, 4, 3, 2, 1))



// The type "T" mentioned above that drops assignment operations.
template <typename U>
struct ignore_assign {
    constexpr explicit ignore_assign(U value) : _value(value) { }
    constexpr operator U() const { return _value; }

    constexpr const ignore_assign& operator =(int dummy) const
        { return *this; }

    U   _value;
};



// Prepends "(ignore_assign<_underlying>)" to each argument.
#define IGNORE_ASSIGN_SINGLE(e) (ignore_assign<_underlying>)e,
#define IGNORE_ASSIGN(...) \
    IDENTITY(MAP(IGNORE_ASSIGN_SINGLE, __VA_ARGS__))

// Stringizes each argument.
#define STRINGIZE_SINGLE(e) #e,
#define STRINGIZE(...) IDENTITY(MAP(STRINGIZE_SINGLE, __VA_ARGS__))



// Some helpers needed for _from_string.
constexpr const char    terminators[] = " =\t\r\n";

// The size of terminators includes the implicit '\0'.
constexpr bool is_terminator(char c, size_t index = 0)
{
    return
        index >= sizeof(terminators) ? false :
        c == terminators[index] ? true :
        is_terminator(c, index + 1);
}

constexpr bool matches_untrimmed(const char *untrimmed, const char *s,
                                 size_t index = 0)
{
    return
        is_terminator(untrimmed[index]) ? s[index] == '\0' :
        s[index] != untrimmed[index] ? false :
        matches_untrimmed(untrimmed, s, index + 1);
}



// The macro proper.
//
// There are several "simplifications" in this implementation, for the
// sake of brevity. First, we have only one viable option for declaring
// constexpr arrays: at namespace scope. This probably should be done
// two namespaces deep: one namespace that is likely to be unique for
// our little enum "library", then inside it a namespace whose name is
// based on the name of the enum to avoid collisions with other enums.
// I am using only one level of nesting.
//
// Declaring constexpr arrays inside the struct is not viable because
// they will need out-of-line definitions, which will result in
// duplicate symbols when linking. This can be solved with weak
// symbols, but that is compiler- and system-specific. It is not
// possible to declare constexpr arrays as static variables in
// constexpr functions due to the restrictions on such functions.
//
// Note that this prevents the use of this macro anywhere except at
// namespace scope. Ironically, the C++98 version of this, which can
// declare static arrays inside static member functions, is actually
// more flexible in this regard. It is shown in the CodeProject
// article.
//
// Second, for compilation performance reasons, it is best to separate
// the macro into a "parametric" portion, and the portion that depends
// on knowing __VA_ARGS__, and factor the former out into a template.
//
// Third, this code uses a default parameter in _from_string that may
// be better not exposed in the public interface.

#define ENUM(EnumName, Underlying, ...)                               \
namespace data_ ## EnumName {                                         \
    using _underlying = Underlying;                                   \
    enum { __VA_ARGS__ };                                             \
                                                                      \
    constexpr const size_t           _size =                          \
        IDENTITY(COUNT(__VA_ARGS__));                                 \
                                                                      \
    constexpr const _underlying      _values[] =                      \
        { IDENTITY(IGNORE_ASSIGN(__VA_ARGS__)) };                     \
                                                                      \
    constexpr const char * const     _raw_names[] =                   \
        { IDENTITY(STRINGIZE(__VA_ARGS__)) };                         \
}                                                                     \
                                                                      \
struct EnumName {                                                     \
    using _underlying = Underlying;                                   \
    enum _enum : _underlying { __VA_ARGS__ };                         \
                                                                      \
    const char * _to_string() const                                   \
    {                                                                 \
        for (size_t index = 0; index < data_ ## EnumName::_size;      \
             ++index) {                                               \
                                                                      \
            if (data_ ## EnumName::_values[index] == _value)          \
                return _trimmed_names()[index];                       \
        }                                                             \
                                                                      \
        throw std::runtime_error("invalid value");                    \
    }                                                                 \
                                                                      \
    constexpr static EnumName _from_string(const char *s,             \
                                           size_t index = 0)          \
    {                                                                 \
        return                                                        \
            index >= data_ ## EnumName::_size ?                       \
                    throw std::runtime_error("invalid identifier") :  \
            matches_untrimmed(                                        \
                data_ ## EnumName::_raw_names[index], s) ?            \
                    (EnumName)(_enum)data_ ## EnumName::_values[      \
                                                            index] :  \
            _from_string(s, index + 1);                               \
    }                                                                 \
                                                                      \
    EnumName() = delete;                                              \
    constexpr EnumName(_enum value) : _value(value) { }               \
    constexpr operator _enum() const { return (_enum)_value; }        \
                                                                      \
  private:                                                            \
    _underlying     _value;                                           \
                                                                      \
    static const char * const * _trimmed_names()                      \
    {                                                                 \
        static char     *the_names[data_ ## EnumName::_size];         \
        static bool     initialized = false;                          \
                                                                      \
        if (!initialized) {                                           \
            for (size_t index = 0; index < data_ ## EnumName::_size;  \
                 ++index) {                                           \
                                                                      \
                size_t  length =                                      \
                    std::strcspn(data_ ## EnumName::_raw_names[index],\
                                 terminators);                        \
                                                                      \
                the_names[index] = new char[length + 1];              \
                                                                      \
                std::strncpy(the_names[index],                        \
                             data_ ## EnumName::_raw_names[index],    \
                             length);                                 \
                the_names[index][length] = '\0';                      \
            }                                                         \
                                                                      \
            initialized = true;                                       \
        }                                                             \
                                                                      \
        return the_names;                                             \
    }                                                                 \
};

并且

// The code above was a "header file". This is a program that uses it.
#include <iostream>
#include "the_file_above.h"

ENUM(Channel, char, Red = 1, Green, Blue)

constexpr Channel   channel = Channel::_from_string("Red");

int main()
{
    std::cout << channel._to_string() << std::endl;

    switch (channel) {
        case Channel::Red:   return 0;
        case Channel::Green: return 1;
        case Channel::Blue:  return 2;
    }
}

static_assert(sizeof(Channel) == sizeof(char), "");

上面的程序输出了Red,正如你所期望的那样。由于您无法创建未初始化的枚举类型,因此存在一定程度的类型安全性,并且从switch中删除其中一个case将导致编译器发出警告(取决于您的编译器和标志)。此外,请注意,在编译期间将"Red"转换为枚举类型。

嗨 @mrhthepie,很抱歉你的编辑被拒绝了。我刚看到了有关此事的电子邮件。我将把它纳入回答中 - 感谢您修复错误! - antron
2
你发布的代码中_trimmed_names()函数似乎存在内存泄漏问题(new char[length + 1]但是你没有将initialized设置为true)。我有什么遗漏吗?我在你的Github代码中没有看到同样的问题。 - user3240688
1
它被设置为“true”,但在“if”分支之外(最初由@mrhthepie捕获的内存泄漏)。应该将其移动到内部...正在编辑。感谢您对此和GH代码的仔细检查。 - antron
2
从C++17开始,to_string函数可以返回一个string_view,它不需要空终止符,并且可以成为constexpr - Yakk - Adam Nevraumont
对我来说,你的ENUM和BETTER_ENUM库之间的区别并不清楚。首先,似乎我无法比较BETTER_ENUM(因此创建一个,并将其与其预定义值之一进行比较会导致一些奇怪的编译错误)。其次,ENUM仅实现了最多8个值(而BETTER_ENUM则更多)。 - ikku100
显示剩余8条评论

92

对于C++20,您可能会对Reflection Study Group (SG7)的工作感兴趣,该组有一系列并行论文,涵盖措辞P0194)和理由、设计和演化P0385)。 (链接解析到每个系列中的最新论文。)

截至P0194r2(2016年10月15日),语法将使用拟议的reflexpr关键字:

meta::get_base_name_v<
  meta::get_element_m<
    meta::get_enumerators_m<reflexpr(MyEnum)>,
    0>
  >
例如(改编自 Matus Choclik 的 reflexpr 分支的 clang,链接如下:https://github.com/matus-chochlik/clang/blob/df25b4ced722ff85064fb464306602436d4f105d/examples/reflection/test03.cpp):
#include <reflexpr>
#include <iostream>

enum MyEnum { AAA = 1, BBB, CCC = 99 };

int main()
{
  auto name_of_MyEnum_0 = 
    std::meta::get_base_name_v<
      std::meta::get_element_m<
        std::meta::get_enumerators_m<reflexpr(MyEnum)>,
        0>
    >;

  // prints "AAA"
  std::cout << name_of_MyEnum_0 << std::endl;
}

静态反射未能进入C++17(更确切地说,未能进入2016年11月在伊萨夸标准会议上提交的最终草案),但有信心它将进入C++20; 来自Herb Sutter的会议报告

特别是,Reflection研究小组审查了最新合并的静态反射提案,并发现它已经准备好进入主要进化小组,在我们下次会议上开始考虑静态反射TS或下一代标准的统一提案。


3
抱歉,您的编辑被拒绝了;如果我及时看到了它,我会批准的。我之前没有看到N4428,所以感谢您提醒我。 - ecatmur
3
没问题,感谢您加入它。我有点想知道为什么它被拒绝了。虽然我看到了“不会使其更准确”的通用原因,但对于当今的情况来说,它显然更准确。 - antron
25
这样一个概念上非常简单的任务需要三个嵌套的模板参数,这太过于过度工程化了。我相信有具体的技术原因。但这并不意味着最终结果对用户友好。我喜欢C++,代码对我来说很有意义。但我每天与之合作的其他程序员中有90%都因为像这样的代码而避开C++。我很失望没有看到任何更简单或更内置的解决方案。 - void.pointer
8
目前似乎预计将在C++23标准中包含即将到来的Reflection TS: https://herbsutter.com/2018/04/02/trip-report-winter-iso-c-standards-meeting-jacksonville/ - Tim Rae
2
@Sz 最近我非常喜欢C#语言、框架、包管理和.NET生态系统的工具支持。我再也不想回到C++了! - void.pointer
显示剩余5条评论

35

2011年,我花了一个周末微调一个基于宏的解决方案,结果从未使用过。

我的当前流程是启动Vim,在空的switch体中复制枚举器,开始新的宏,将第一个枚举器转换为case语句,将光标移到下一行的开头位置,停止宏并通过对其他枚举器运行宏生成其余的case语句。

Vim宏比C++宏更有趣。

现实生活中的例子:

enum class EtherType : uint16_t
{
    ARP   = 0x0806,
    IPv4  = 0x0800,
    VLAN  = 0x8100,
    IPv6  = 0x86DD
};

我会创建这个:

std::ostream& operator<< (std::ostream& os, EtherType ethertype)
{
    switch (ethertype)
    {
        case EtherType::ARP : return os << "ARP" ;
        case EtherType::IPv4: return os << "IPv4";
        case EtherType::VLAN: return os << "VLAN";
        case EtherType::IPv6: return os << "IPv6";
        // omit default case to trigger compiler warning for missing cases
    };
    return os << static_cast<std::uint16_t>(ethertype);
}

这就是我生存的方式。

尽管如此,原生支持枚举字符串化会更好。我非常期待C++17中反射工作组的成果。

@sehe在评论中发表了一种替代方法。


1
我确实这样做。虽然我通常会在路上使用Surround vim和块选择。 - sehe
@sehe 很有趣。我应该看一下“surround”,因为我目前需要太多按键了。 - StackedCrooked
这是完整的程序代码,没有宏(除非点号 . 也算):http://i.imgur.com/gY4ZhBE.gif - sehe
1
动态GIF很可爱,但很难确定它何时开始和结束,以及我们到了多远。实际上,划掉那句话,它不是可爱的,而是分散注意力的。我建议删除它。 - einpoklum
在vim中的块选择方法是很好的,但为什么不直接使用类似于 :'<,'>s/ *\(.*\)=.*/case EtherType::\1: return os << "\1";/ 的东西呢? - Ruslan

30

这类似于Yuri Finkelstein的方法,但不需要使用boost库。我使用了一个映射表,所以您可以将任何值分配给枚举类型,而且顺序也无所谓。

枚举类型的声明如下:

DECLARE_ENUM_WITH_TYPE(TestEnumClass, int32_t, ZERO = 0x00, TWO = 0x02, ONE = 0x01, THREE = 0x03, FOUR);

以下代码将自动创建枚举类和重载:
  • 对于std::string,使用'+' '+='
  • 对于流,使用'<<'
  • 仅用于转换为字符串的'~'(任何一元运算符都可以,但出于清晰起见,我个人不喜欢它)
  • 使用'*'获取枚举计数
无需boost,所有必需函数已提供。
代码:
#include <algorithm>
#include <iostream>
#include <map>
#include <sstream>
#include <string>
#include <vector>

#define STRING_REMOVE_CHAR(str, ch) str.erase(std::remove(str.begin(), str.end(), ch), str.end())

std::vector<std::string> splitString(std::string str, char sep = ',') {
    std::vector<std::string> vecString;
    std::string item;

    std::stringstream stringStream(str);

    while (std::getline(stringStream, item, sep))
    {
        vecString.push_back(item);
    }

    return vecString;
}

#define DECLARE_ENUM_WITH_TYPE(E, T, ...)                                                                     \
    enum class E : T                                                                                          \
    {                                                                                                         \
        __VA_ARGS__                                                                                           \
    };                                                                                                        \
    std::map<T, std::string> E##MapName(generateEnumMap<T>(#__VA_ARGS__));                                    \
    std::ostream &operator<<(std::ostream &os, E enumTmp)                                                     \
    {                                                                                                         \
        os << E##MapName[static_cast<T>(enumTmp)];                                                            \
        return os;                                                                                            \
    }                                                                                                         \
    size_t operator*(E enumTmp) { (void) enumTmp; return E##MapName.size(); }                                 \
    std::string operator~(E enumTmp) { return E##MapName[static_cast<T>(enumTmp)]; }                          \
    std::string operator+(std::string &&str, E enumTmp) { return str + E##MapName[static_cast<T>(enumTmp)]; } \
    std::string operator+(E enumTmp, std::string &&str) { return E##MapName[static_cast<T>(enumTmp)] + str; } \
    std::string &operator+=(std::string &str, E enumTmp)                                                      \
    {                                                                                                         \
        str += E##MapName[static_cast<T>(enumTmp)];                                                           \
        return str;                                                                                           \
    }                                                                                                         \
    E operator++(E &enumTmp)                                                                                  \
    {                                                                                                         \
        auto iter = E##MapName.find(static_cast<T>(enumTmp));                                                 \
        if (iter == E##MapName.end() || std::next(iter) == E##MapName.end())                                  \
            iter = E##MapName.begin();                                                                        \
        else                                                                                                  \
        {                                                                                                     \
            ++iter;                                                                                           \
        }                                                                                                     \
        enumTmp = static_cast<E>(iter->first);                                                                \
        return enumTmp;                                                                                       \
    }                                                                                                         \
    bool valid##E(T value) { return (E##MapName.find(value) != E##MapName.end()); }

#define DECLARE_ENUM(E, ...) DECLARE_ENUM_WITH_TYPE(E, int32_t, __VA_ARGS__)
template <typename T>
std::map<T, std::string> generateEnumMap(std::string strMap)
{
    STRING_REMOVE_CHAR(strMap, ' ');
    STRING_REMOVE_CHAR(strMap, '(');

    std::vector<std::string> enumTokens(splitString(strMap));
    std::map<T, std::string> retMap;
    T inxMap;

    inxMap = 0;
    for (auto iter = enumTokens.begin(); iter != enumTokens.end(); ++iter)
    {
        // Token: [EnumName | EnumName=EnumValue]
        std::string enumName;
        T enumValue;
        if (iter->find('=') == std::string::npos)
        {
            enumName = *iter;
        }
        else
        {
            std::vector<std::string> enumNameValue(splitString(*iter, '='));
            enumName = enumNameValue[0];
            //inxMap = static_cast<T>(enumNameValue[1]);
            if (std::is_unsigned<T>::value)
            {
                inxMap = static_cast<T>(std::stoull(enumNameValue[1], 0, 0));
            }
            else
            {
                inxMap = static_cast<T>(std::stoll(enumNameValue[1], 0, 0));
            }
        }
        retMap[inxMap++] = enumName;
    }

    return retMap;
}

例子:

DECLARE_ENUM_WITH_TYPE(TestEnumClass, int32_t, ZERO = 0x00, TWO = 0x02, ONE = 0x01, THREE = 0x03, FOUR);

int main(void) {
    TestEnumClass first, second;
    first = TestEnumClass::FOUR;
    second = TestEnumClass::TWO;

    std::cout << first << "(" << static_cast<uint32_t>(first) << ")" << std::endl; // FOUR(4)

    std::string strOne;
    strOne = ~first;
    std::cout << strOne << std::endl; // FOUR

    std::string strTwo;
    strTwo = ("Enum-" + second) + (TestEnumClass::THREE + "-test");
    std::cout << strTwo << std::endl; // Enum-TWOTHREE-test

    std::string strThree("TestEnumClass: ");
    strThree += second;
    std::cout << strThree << std::endl; // TestEnumClass: TWO
    std::cout << "Enum count=" << *first << std::endl;
}

You can run the code here


2
这个宏定义里面可以有换行吗? - einpoklum
1
我添加了*的重载以获取枚举的计数...希望你不介意 :-) - Peter VARGA
2
这个实现为什么使用 std::map (O(log(n)) 索引) 而不是 std::unordered_map (O(1) 索引)? - River Tam
1
此外,我认为应将这些方法标记为“inline”,这样您就可以像通常在头文件中声明枚举一样进行操作,而不会从链接器获得“多重定义”的错误。(不确定是否实际上这是最清晰/最佳的解决方案,但是)。 - River Tam
1
这个头文件还有其他问题。地图(E##MapName)需要移动到一个编译单元中,该单元也可以访问枚举。我已经创建了一个解决方案,但它不是很干净,而且我需要获得分享的权限。目前,我只是评论说,在没有必要支持头文件中的使用的其他功能的情况下,标记方法为内联没有意义。 - River Tam
显示剩余4条评论

23

我不知道你是否会喜欢这个解决方案,但我对它并不是太满意。不过它是一个C++14友好的方法,因为它使用了模板变量并滥用了模板特化:

enum class MyEnum : std::uint_fast8_t {
   AAA,
   BBB,
   CCC,
};

template<MyEnum> const char MyEnumName[] = "Invalid MyEnum value";
template<> const char MyEnumName<MyEnum::AAA>[] = "AAA";
template<> const char MyEnumName<MyEnum::BBB>[] = "BBB";
template<> const char MyEnumName<MyEnum::CCC>[] = "CCC";

int main()
{
    // Prints "AAA"
    std::cout << MyEnumName<MyEnum::AAA> << '\n';
    // Prints "Invalid MyEnum value"
    std::cout << MyEnumName<static_cast<MyEnum>(0x12345678)> << '\n';
    // Well... in fact it prints "Invalid MyEnum value" for any value
    // different of MyEnum::AAA, MyEnum::BBB or MyEnum::CCC.

    return 0;
}

这种方法最糟糕的地方是很难维护,但其他类似的方法也同样难以维护,不是吗?

这个方法的优点有:

  • 使用变量模板(C++14 特性)
  • 通过模板特化,我们可以“检测”何时使用了无效值(但我不确定这是否有用)。
  • 看起来很整洁。
  • 名称查找在编译时完成。

示例链接

编辑

神秘的user673679,你是对的;C++14 变量模板方法不能处理运行时情况,忘记这一点是我的错 :(

但我们仍然可以利用一些现代 C++ 特性和变量模板加可变参数模板技巧实现从枚举值到字符串的运行时转换...它和其他方法一样麻烦,但仍然值得一提。

让我们开始使用一个模板别名来缩短访问枚举值到字符串的映射:

// enum_map contains pairs of enum value and value string for each enum
// this shortcut allows us to use enum_map<whatever>.
template <typename ENUM>
using enum_map = std::map<ENUM, const std::string>;

// This variable template will create a map for each enum type which is
// instantiated with.
template <typename ENUM>
enum_map<ENUM> enum_values{};

然后,就是变长模板的技巧:

template <typename ENUM>
void initialize() {}

template <typename ENUM, typename ... args>
void initialize(const ENUM value, const char *name, args ... tail)
{
    enum_values<ENUM>.emplace(value, name);
    initialize<ENUM>(tail ...);
}

这里最好的技巧是使用变量模板来存储包含每个枚举条目的值和名称的映射;这个映射将在每个翻译单元中都是相同的,并且在任何地方都具有相同的名称,因此非常简单和整洁,如果我们像这样调用initialize函数:

initialize
(
    MyEnum::AAA, "AAA",
    MyEnum::BBB, "BBB",
    MyEnum::CCC, "CCC"
);
我们为每个MyEnum条目指定名称,并可以在运行时使用:
std::cout << enum_values<MyEnum>[MyEnum::AAA] << '\n';

但可以通过SFINAE和重载<<运算符来改进:

template<typename ENUM, class = typename std::enable_if<std::is_enum<ENUM>::value>::type>
std::ostream &operator <<(std::ostream &o, const ENUM value)
{
    static const std::string Unknown{std::string{typeid(ENUM).name()} + " unknown value"};
    auto found = enum_values<ENUM>.find(value);

    return o << (found == enum_values<ENUM>.end() ? Unknown : found->second);
}

现在我们有了正确的operator <<,可以这样使用枚举:

std::cout << MyEnum::AAA << '\n';

这也很麻烦,需要维护和改进,但希望你能明白。

现场示例


这看起来相当整洁(有没有可能只是不定义未特化的变量?)。也许我错过了什么,因为我根本看不到它如何处理运行时情况。 - user673679
@Paula_plus_plus:你不应该使用std::array代替笨重的map吗?只有当枚举值从2^10开始时,它才会变得更可取。甚至可能更多。 - einpoklum
1
@einpoklum 如果我们能在运行时确保一个枚举有多少个元素,那将是非常棒的。不幸的是,我们无法做到这一点。而映射的整个重点就是将名称与值关联起来,这正是 std::map 擅长的地方。 - PaperBirdMaster
@einpoklum 这确实是一个非常好的观点,我会考虑一下,谢谢!唯一让我担心的是 std::array 不是一个键值容器,因此缺少查找方法;无论如何,我会仔细思考。 - PaperBirdMaster
@Paula_plus_plus:std::find是你的好朋友。 - einpoklum
显示剩余2条评论

10

如果你的enum长这样

enum MyEnum
{
  AAA = -8,
  BBB = '8',
  CCC = AAA + BBB
};

你可以将 enum 内容移动到一个新文件中:

AAA = -8,
BBB = '8',
CCC = AAA + BBB

然后这些值可以由一个宏包围:

// default definition
#ifned ITEM(X,Y)
#define ITEM(X,Y)
#endif

// Items list
ITEM(AAA,-8)
ITEM(BBB,'8')
ITEM(CCC,AAA+BBB)

// clean up
#undef ITEM

下一步可能是再次将这些项目包含在enum中:

enum MyEnum
{
  #define ITEM(X,Y) X=Y,
  #include "enum_definition_file"
};

最后,您可以生成关于此枚举的实用函数:

std::string ToString(MyEnum value)
{
  switch( value )
  {
    #define ITEM(X,Y) case X: return #X;
    #include "enum_definition_file"
  }

  return "";
}

MyEnum FromString(std::string const& value)
{
  static std::map<std::string,MyEnum> converter
  {
    #define ITEM(X,Y) { #X, X },
    #include "enum_definition_file"
  };

  auto it = converter.find(value);
  if( it != converter.end() )
    return it->second;
  else
    throw std::runtime_error("Value is missing");
}

这个解决方案可以应用于较旧的C++标准,并且它不使用现代C++元素,但是它可以用于生成大量代码,而不需要太多的工作和维护。


4
不需要单独的文件。这本质上是一个x-macro - HolyBlackCat
@HolyBlackCat 如果你将解决方案拆分成一些文件,你可以重复使用枚举值来实现不同的目的。 - eferion
我想告诉你,如果你将值列表与枚举定义放在头文件中的单个宏中,就可以做到同样的事情。 - HolyBlackCat
好的。我不应该给它点踩,因为它确实有一些用处。(抱歉进行了虚拟编辑,否则系统会锁定我的投票。) - HolyBlackCat
在clang中,定义是在单独的源文件中的,因为它们由tablegen工具自动生成。 - gigabytes
显示剩余2条评论

7

几天前我也遇到了同样的问题。我在没有奇怪的宏魔法的情况下找不到任何C++解决方案,因此我决定编写一个CMake代码生成器来生成简单的switch case语句。

使用方法:

enum2str_generate(
  PATH          <path to place the files in>
  CLASS_NAME    <name of the class (also prefix for the files)>
  FUNC_NAME     <name of the (static) member function>
  NAMESPACE     <the class will be inside this namespace>
  INCLUDES      <LIST of files where the enums are defined>
  ENUMS         <LIST of enums to process>
  BLACKLIST     <LIST of constants to ignore>
  USE_CONSTEXPR <whether to use constexpr or not (default: off)>
  USE_C_STRINGS <whether to use c strings instead of std::string or not (default: off)>
)

该函数在文件系统中搜索包含文件(使用include_directories命令提供的包含目录),读取它们并执行一些正则表达式以生成类和函数。

注意:在C++中,constexpr意味着内联,因此使用USE_CONSTEXPR选项将生成一个仅头文件的类!

例如:

./includes/a.h:

enum AAA : char { A1, A2 };

typedef enum {
   VAL1          = 0,
   VAL2          = 1,
   VAL3          = 2,
   VAL_FIRST     = VAL1,    // Ignored
   VAL_LAST      = VAL3,    // Ignored
   VAL_DUPLICATE = 1,       // Ignored
   VAL_STRANGE   = VAL2 + 1 // Must be blacklisted
} BBB;

./CMakeLists.txt:

include_directories( ${PROJECT_SOURCE_DIR}/includes ...)

enum2str_generate(
   PATH       "${PROJECT_SOURCE_DIR}"
   CLASS_NAME "enum2Str"
   NAMESPACE  "abc"
   FUNC_NAME  "toStr"
   INCLUDES   "a.h" # WITHOUT directory
   ENUMS      "AAA" "BBB"
   BLACKLIST  "VAL_STRANGE")

Generates:

./enum2Str.hpp:

/*!
  * \file enum2Str.hpp
  * \warning This is an automatically generated file!
  */

#ifndef ENUM2STR_HPP
#define ENUM2STR_HPP

#include <string>
#include <a.h>

namespace abc {

class enum2Str {
 public:
   static std::string toStr( AAA _var ) noexcept;
   static std::string toStr( BBB _var ) noexcept;
};

}

#endif // ENUM2STR_HPP

./enum2Str.cpp:

/*!
  * \file enum2Str.cpp
  * \warning This is an automatically generated file!
  */

#include "enum2Str.hpp"

namespace abc {

/*!
 * \brief Converts the enum AAA to a std::string
 * \param _var The enum value to convert
 * \returns _var converted to a std::string
 */
std::string enum2Str::toStr( AAA _var ) noexcept {
   switch ( _var ) {
      case A1: return "A1";
      case A2: return "A2";
      default: return "<UNKNOWN>";
   }
}

/*!
 * \brief Converts the enum BBB to a std::string
 * \param _var The enum value to convert
 * \returns _var converted to a std::string
 */
std::string enum2Str::toStr( BBB _var ) noexcept {
   switch ( _var ) {
      case VAL1: return "VAL1";
      case VAL2: return "VAL2";
      case VAL3: return "VAL3";
      default: return "<UNKNOWN>";
   }
}
}

更新:

该脚本现在还支持有作用域的枚举(enum class|struct),并将其移动到了一个单独的存储库中,其中包含我经常使用的其他脚本:https://github.com/mensinda/cmakeBuildTools


哇!非常原创和创新的想法 :-) 我希望你有勇气升级你的生成器,以提供 constexprnoexcept 版本 ;-) 我也刚刚开始关注 你的 GitHub 项目 ;-) 祝好! - oHo
1
更新了生成器。函数现在将始终是constexpr和enum:<type>现在得到支持。 感谢您的点赞 :) - Mense
链接已经损坏了... -.- - yeoman
链接现在已经修复。 - Mense

4

只需生成您的枚举。为此编写生成器大约需要五分钟的时间。

Java和Python中的生成器代码,非常容易移植到任何您喜欢的语言,包括C ++。

而且非常容易通过您想要的任何功能进行扩展。

示例输入:

First = 5
Second
Third = 7
Fourth
Fifth=11

生成的标题:

#include <iosfwd>

enum class Hallo
{
    First = 5,
    Second = 6,
    Third = 7,
    Fourth = 8,
    Fifth = 11
};

std::ostream & operator << (std::ostream &, const Hallo&);

生成的cpp文件

#include <ostream>

#include "Hallo.h"

std::ostream & operator << (std::ostream &out, const Hallo&value)
{
    switch(value)
    {
    case Hallo::First:
        out << "First";
        break;
    case Hallo::Second:
        out << "Second";
        break;
    case Hallo::Third:
        out << "Third";
        break;
    case Hallo::Fourth:
        out << "Fourth";
        break;
    case Hallo::Fifth:
        out << "Fifth";
        break;
    default:
        out << "<unknown>";
    }

    return out;
}

生成器以非常简洁的形式作为移植和扩展模板。这个示例代码确实试图避免覆盖任何文件,但仍需自行承担风险。

package cppgen;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.nio.charset.Charset;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class EnumGenerator
{
    static void fail(String message)
    {
        System.err.println(message);
        System.exit(1);
    }

    static void run(String[] args)
    throws Exception
    {
        Pattern pattern = Pattern.compile("\\s*(\\w+)\\s*(?:=\\s*(\\d+))?\\s*", Pattern.UNICODE_CHARACTER_CLASS);
        Charset charset = Charset.forName("UTF8");
        String tab = "    ";

        if (args.length != 3)
        {
            fail("Required arguments: <enum name> <input file> <output dir>");
        }

        String enumName = args[0];

        File inputFile = new File(args[1]);

        if (inputFile.isFile() == false)
        {
            fail("Not a file: [" + inputFile.getCanonicalPath() + "]");
        }

        File outputDir = new File(args[2]);

        if (outputDir.isDirectory() == false)
        {
            fail("Not a directory: [" + outputDir.getCanonicalPath() + "]");
        }

        File headerFile = new File(outputDir, enumName + ".h");
        File codeFile = new File(outputDir, enumName + ".cpp");

        for (File file : new File[] { headerFile, codeFile })
        {
            if (file.exists())
            {
                fail("Will not overwrite file [" + file.getCanonicalPath() + "]");
            }
        }

        int nextValue = 0;

        Map<String, Integer> fields = new LinkedHashMap<>();

        try
        (
            BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(inputFile), charset));
        )
        {
            while (true)
            {
                String line = reader.readLine();

                if (line == null)
                {
                    break;
                }

                if (line.trim().length() == 0)
                {
                    continue;
                }

                Matcher matcher = pattern.matcher(line);

                if (matcher.matches() == false)
                {
                    fail("Syntax error: [" + line + "]");
                }

                String fieldName = matcher.group(1);

                if (fields.containsKey(fieldName))
                {
                    fail("Double fiend name: " + fieldName);
                }

                String valueString = matcher.group(2);

                if (valueString != null)
                {
                    int value = Integer.parseInt(valueString);

                    if (value < nextValue)
                    {
                        fail("Not a monotonous progression from " + nextValue + " to " + value + " for enum field " + fieldName);
                    }

                    nextValue = value;
                }

                fields.put(fieldName, nextValue);

                ++nextValue;
            }
        }

        try
        (
            PrintWriter headerWriter = new PrintWriter(new OutputStreamWriter(new FileOutputStream(headerFile), charset));
            PrintWriter codeWriter = new PrintWriter(new OutputStreamWriter(new FileOutputStream(codeFile), charset));
        )
        {
            headerWriter.println();
            headerWriter.println("#include <iosfwd>");
            headerWriter.println();
            headerWriter.println("enum class " + enumName);
            headerWriter.println('{');
            boolean first = true;
            for (Entry<String, Integer> entry : fields.entrySet())
            {
                if (first == false)
                {
                    headerWriter.println(",");
                }

                headerWriter.print(tab + entry.getKey() + " = " + entry.getValue());

                first = false;
            }
            if (first == false)
            {
                headerWriter.println();
            }
            headerWriter.println("};");
            headerWriter.println();
            headerWriter.println("std::ostream & operator << (std::ostream &, const " + enumName + "&);");
            headerWriter.println();

            codeWriter.println();
            codeWriter.println("#include <ostream>");
            codeWriter.println();
            codeWriter.println("#include \"" + enumName + ".h\"");
            codeWriter.println();
            codeWriter.println("std::ostream & operator << (std::ostream &out, const " + enumName + "&value)");
            codeWriter.println('{');
            codeWriter.println(tab + "switch(value)");
            codeWriter.println(tab + '{');
            first = true;
            for (Entry<String, Integer> entry : fields.entrySet())
            {
                codeWriter.println(tab + "case " + enumName + "::" + entry.getKey() + ':');
                codeWriter.println(tab + tab + "out << \"" + entry.getKey() + "\";");
                codeWriter.println(tab + tab + "break;");

                first = false;
            }
            codeWriter.println(tab + "default:");
            codeWriter.println(tab + tab + "out << \"<unknown>\";");
            codeWriter.println(tab + '}');
            codeWriter.println();
            codeWriter.println(tab + "return out;");
            codeWriter.println('}');
            codeWriter.println();
        }
    }

    public static void main(String[] args)
    {
        try
        {
            run(args);
        }
        catch(Exception exc)
        {
            exc.printStackTrace();
            System.exit(1);
        }
    }
}

因为Python 3.5与之前版本有所不同,因此需要进行移植以提高效率。

import re
import collections
import sys
import io
import os

def fail(*args):
    print(*args)
    exit(1)

pattern = re.compile(r'\s*(\w+)\s*(?:=\s*(\d+))?\s*')
tab = "    "

if len(sys.argv) != 4:
    n=0
    for arg in sys.argv:
        print("arg", n, ":", arg, " / ", sys.argv[n])
        n += 1
    fail("Required arguments: <enum name> <input file> <output dir>")

enumName = sys.argv[1]

inputFile = sys.argv[2]

if not os.path.isfile(inputFile):
    fail("Not a file: [" + os.path.abspath(inputFile) + "]")

outputDir = sys.argv[3]

if not os.path.isdir(outputDir):
    fail("Not a directory: [" + os.path.abspath(outputDir) + "]")

headerFile = os.path.join(outputDir, enumName + ".h")
codeFile = os.path.join(outputDir, enumName + ".cpp")

for file in [ headerFile, codeFile ]:
    if os.path.exists(file):
        fail("Will not overwrite file [" + os.path.abspath(file) + "]")

nextValue = 0

fields = collections.OrderedDict()

for line in open(inputFile, 'r'):
    line = line.strip()

    if len(line) == 0:
        continue

    match = pattern.match(line)

    if match == None:
        fail("Syntax error: [" + line + "]")

    fieldName = match.group(1)

    if fieldName in fields:
        fail("Double field name: " + fieldName)

    valueString = match.group(2)

    if valueString != None:
        value = int(valueString)

        if value < nextValue:
            fail("Not a monotonous progression from " + nextValue + " to " + value + " for enum field " + fieldName)

        nextValue = value

    fields[fieldName] = nextValue

    nextValue += 1

headerWriter = open(headerFile, 'w')
codeWriter = open(codeFile, 'w')

try:
    headerWriter.write("\n")
    headerWriter.write("#include <iosfwd>\n")
    headerWriter.write("\n")
    headerWriter.write("enum class " + enumName + "\n")
    headerWriter.write("{\n")
    first = True
    for fieldName, fieldValue in fields.items():
        if not first:
            headerWriter.write(",\n")

        headerWriter.write(tab + fieldName + " = " + str(fieldValue))

        first = False
    if not first:
        headerWriter.write("\n")
    headerWriter.write("};\n")
    headerWriter.write("\n")
    headerWriter.write("std::ostream & operator << (std::ostream &, const " + enumName + "&);\n")
    headerWriter.write("\n")

    codeWriter.write("\n")
    codeWriter.write("#include <ostream>\n")
    codeWriter.write("\n")
    codeWriter.write("#include \"" + enumName + ".h\"\n")
    codeWriter.write("\n")
    codeWriter.write("std::ostream & operator << (std::ostream &out, const " + enumName + "&value)\n")
    codeWriter.write("{\n")
    codeWriter.write(tab + "switch(value)\n")
    codeWriter.write(tab + "{\n")
    for fieldName in fields.keys():
        codeWriter.write(tab + "case " + enumName + "::" + fieldName + ":\n")
        codeWriter.write(tab + tab + "out << \"" + fieldName + "\";\n")
        codeWriter.write(tab + tab + "break;\n")
    codeWriter.write(tab + "default:\n")
    codeWriter.write(tab + tab + "out << \"<unknown>\";\n")
    codeWriter.write(tab + "}\n")
    codeWriter.write("\n")
    codeWriter.write(tab + "return out;\n")
    codeWriter.write("}\n")
    codeWriter.write("\n")
finally:
    headerWriter.close()
    codeWriter.close()

1
非常感谢您分享双语生成器 :-) 但是您有没有想过如何在编译时生成呢?例如,我们可以想象使用CMake语句来翻译您的生成器,以便在输入数据更改时刷新C++生成的代码吗?我的梦想是通过元编程(可变参数模板类constexpr函数)强制C++编译器在编译时生成枚举。 - oHo
关于在编译时生成东西,在LISP中非常容易,因为语法非常干净和简单。 这得益于它是动态类型的,这使得它可以不需要太多语法就能简洁易读。在C++中,相当于LISP宏的东西需要一种非常复杂的方式来描述你想要生成的AST。而且C++的AST从来都不好看 :( - yeoman
1
直接替换代码通常不是一个好主意。如果你决定添加二十个新的枚举字段,那么这个输入就会被覆盖掉。此外,当你试图让它更智能化时,保留输入并在其下方添加生成的代码,并在输入发生更改时用新生成的代码替换你生成的代码,你可以打赌,迟早会出现一些问题,你的生成器会删除一些本不应该删除的代码。-.- - yeoman
感谢您的反馈 :-) +1 您是正确的,更改C++源文件的内容不是一个好主意(例如,该文件可能是只读的)。因此,生成的代码应该与其他生成的文件一起放在某个地方(例如,*.o文件)。使用您的想法,构建工具链(Makefile或CMake等)可以调用外部模块(Java或Python...)直接检测C++代码中的枚举并生成相应的enum_to_string函数。您的代码可以发展成使用clang-parser来理解C++源代码的抽象语法树(AST)... 干杯 - oHo
这会增加很多复杂性并且代价高昂。C++代码易于调试,但没有真正源代码的目标文件则不然。 - yeoman
显示剩余19条评论

4
你可以使用自定义字面值来达到所需的结果:

自定义字面值 可以被滥用。

enum
{
  AAA = "AAA"_h8,
  BB = "BB"_h8,
};
   
std::cout << h8::to_string(AAA) << std::endl;
std::cout << h8::to_string(BB) << std::endl;

将一个字符串打包成一个可逆的整数。点击这里查看示例。

很遗憾,只适用于长度小于等于8的字符串。 - Jean-Michaël Celerier
我们很快就会到达16个字符。 - user1095108
这里是h8的当前版本:https://github.com/user1095108/h8。 - user1095108

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