65.9K
CodeProject 正在变化。 阅读更多。
Home

C++ 中对枚举类型提供 .NET 式的反射支持

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.08/5 (8投票s)

2009年5月27日

CPOL

6分钟阅读

viewsIcon

35887

downloadIcon

166

本文提供了一个宏 + 模板解决方案,以支持枚举类型的 .NET 式反射,例如 ToString、IsDefined、Parse、GetValues、GetNames。

引言

.NET 程序员大多熟悉反射。反射是一个很大的话题。这里我只讨论枚举类型的反射。在本文中,我将展示一个 C++ [模板 + 宏] 解决方案,将 .NET 中枚举的反射能力引入 C++ 世界。该工具实现为一个模板类。

代码经以下工具检查:

  • Comeau C/C++ 4.3.10.1 (Oct 6 2008 11:28:09) for ONLINE_EVALUATION_BETA2;只有一个关于使用非标准 stricmp 的警告
  • VC2003 (/W4 /WX)
  • VC2008 (/W4 /WX)
  • gcc -c -Wall -xc++ (版本 gcc (GCC) 3.3.3 (Cygwin special))
  • PC-Lint online(9.00b)

背景

尽管 .NET/C# 越来越流行,但对于大多数程序员来说,在某些场景下 C++ 仍然是不可避免的。使用 .NET/C#,我们可以将 enum 作为真正的类型,并对其进行以下操作:

  • ToString()
  • Parse(Type, String), Parse(Type, String, Boolean)
  • IsDefined( Type enumType, Object value)
  • GetValues()
  • GetNames()

我正在进行的项目是一个混合了 C# 和 C++ 代码的混合项目。当我切换到 C++ 代码时,我突然失去了 C# 中大量的生产力。我不是在这里比较语言,这只是我个人的感受。我非常感兴趣的问题是:为什么?以及如何将 C# 中的生产力带到 C++?我发现有许多实际因素可以实现这些目标:如果可能,避免使用指针;使用 RAII/智能指针;使用 STL 容器而不是原生数组;重用现有库等。大多数规则都不是新的,并且在多年前的《Effective C++》、《More Effective C++》等书中已经讨论过。但我找不到一个解决方案能让我在 C++ 中对 enum 应用 C# 中的操作。

动机是我们项目中的日志系统。为了精细控制每个日志项,我们定义了一个 LogLevel 枚举和一个 LogBy 枚举,如下所示:

enum LogLevel{ Error, Info }
enum LogBy { ZhaoRuFei, Wang, Zhang }

当然,我们还希望在日志文件中将日志级别和作者以 string 形式记录。现在第一个问题出现了:我们如何在 C++ 中获取 enum 的声明字符串?

一个大的 switch 是可能的,但很丑陋。

以下稍微好一些,但仍然很难维护:

const char * get_LogLevel_str( LogLevel level)
{
    struct {
        LogLevel value;
        const char * str;
    } static s_all_log_levels[] = {
       { LogLevel::Info, "Info" },
       { LogLevel::Error, "Error"},
    };
    for(int i = 0; i < _countof(s_all_log_levels); i++)
    {
        if(s_all_log_levels[i].value == level)
            return s_all_log_levels[i].str;
    }
    return NULL;
}

将来某天我想向 LogLevel 添加一个 Warning 枚举器时,我很可能会忘记在这里添加它。这违反了 DRY (Don't Repeat Yourself) 原则。

经过 Google 搜索,我找到了 VC++ IDE 团队开发者 Rocky 的以下文章:

我初看时很兴奋,然后将这些技巧用在我们的项目中。然后我发现这个解决方案不切实际,因为一个非平凡的项目会涉及许多枚举定义。将所有这些枚举定义在一个单独的头文件中会使其难以使用和维护。

上述解决方案的一个变体是放弃头文件,转而使用宏:

#define ALL_THE_WEEK \ 
ENUM_ITEM(Sunday)\
ENUM_ITEM(Monday)

#define ENUM_ITEM(a)  a,
enum WeekDay { ALL_THE_WEEK };
#define ENUM_ITEM(a) {a, #a},
struct {
   WeekDay val;
   const char * str;
} static all_WeekDay_meta_data[] = {ALL_THE_WEEK};

开发者可以专注于 ALL_THE_WEEK 宏的定义,而忘记其他代码。这个想法最终演变为模板 + 宏解决方案,它提供了 C# 风格的枚举反射能力。

Using the Code

要使用该代码,您需要:

#include "Reflection4CppEnum.hpp"

定义一个宏来收集所有枚举器,如上所示。

#define ALL_THE_WEEK(x, y)          \
ENUM_ITEM(x, y, Sunday)             \
ENUM_ITEM(x, y, Monday)             \
ENUM_ITEM_VALUE(x, y, Tuesday, 25)  \
ENUM_ITEM(x, y, Wednesday)          \
ENUM_ITEM_VALUE(x, y, Thursday, 36) \
ENUM_ITEM(x, y, Friday)             \
ENUM_ITEM(x, y, Saturday)

实际上,此步骤涉及三个宏名称:ALL_THE_WEEKENUM_ITEMENUM_ITEM_VALUE

定义支持反射的枚举

DEF_ENUM(, WeekDay, ALL_THE_WEEK)

有点奇怪的是,宏的第一个参数留空了。注意:这不是一个错字!它是用于命名空间的,在这种情况下,它位于全局命名空间中。对于全局命名空间或默认命名空间,它必须留空。

ENUM_ITEMENUM_ITEM_VALUEDEF_ENUM 是此实用程序组件提供的宏,名称是固定的。

您有责任命名宏 ALL_THE_WEEK 并提供宏定义,使其与解决方案一起工作。这项工作总是与上述相同。与典型的枚举定义相比:

enum WeekDay { Sunday, Monday, Tuesday = 25, 
    Wednesday, Thursday = 36, Friday, Saturday, }; 

请注意,有两个不可避免的额外宏参数“x”和“y”,初看起来毫无用处。但它们对解决方案至关重要,我稍后会解释。另一个不便之处(据我所知没有解决方法)是使用 ENUM_ITEM_VALUE 宏而不是 ENUM_ITEM 来定义具有显式值的枚举项!

WeekDay 是枚举的类型名称。

它已准备好使用 EnumHelper<WeekDay>::xxx() 提供的操作。

获取枚举名称和枚举器数量

// .NET equivalent: typeof(WeekDay).Length       Enum.GetNames(typeof(WeekDay)).Name
printf("%d enumerators defined for Enum [%s]\n", EnumHelper<WeekDay>::count(),
      EnumHelper<WeekDay>::get_type_name() );

迭代所有名称和相应的值

// .NET equivalent:
//      foreach(string s in Enum.GetNames(typeof(WeekDay)) ) Console.WriteLine( s );
//      foreach(object o in Enum.GetValues(typeof(WeekDay))) Console.WriteLine( o );
EnumHelper<WeekDay>::const_str_iterator it_str   = EnumHelper<WeekDay>::str_begin();
EnumHelper<WeekDay>::const_value_iterator it_val = EnumHelper<WeekDay>::value_begin();

for(; it_str != EnumHelper<WeekDay>::str_end() && it_val != 
    EnumHelper<WeekDay>::value_end();
    ++it_str, ++it_val)
{
    printf("Enum str: [%20s],  value: %d\n", *it_str, *it_val);
}

确定整数值是否为定义的枚举器

// .NET equivalent:
//     Console.WriteLine(  Enum.IsDefined(typeof(WeekDay), 0) );
int test_enum_val = 0;
printf("Is %d a defined enumerator for %s: %s\n", test_enum_val,  
    EnumHelper<WeekDay>::get_type_name(),
        EnumHelper<WeekDay>::is_defined(test_enum_val)? "True" : "False" );

确定字符串值是否为定义的枚举器

// .NET equivalent:
//     Console.WriteLine(  Enum.IsDefined(typeof(WeekDay), "Sunday" ) );
const char * test_enum_str = "Sunday";
printf("Is %s a defined enumerator for %s: %s(case-sensitive)\n", test_enum_str,
    EnumHelper<WeekDay>::get_type_name(),
    EnumHelper<WeekDay>::is_defined(test_enum_str) ? "True": "False" );

支持不区分大小写的 is_defined

// .NET equivalent: No
test_enum_str = "sunday";
printf("Is %s a defined enumerator for %s: %s(case-insensitive)\n", test_enum_str,
    EnumHelper<WeekDay>::get_type_name(),
    EnumHelper<WeekDay>::is_defined(test_enum_str, true) ? "True": "False" );

从枚举器到字符串

// .NET equivalent:
//     WeekDay day = Sunday; Console.WriteLine( day.ToString() );
printf("enum string for %d is %s\n",  test_enum_val, EnumHelper<WeekDay>::to_string(0) );
printf("enum string for %d is %s\n",  Sunday, EnumHelper<WeekDay>::to_string( Sunday ) );
//     for a non-exist item, get digital representation
printf("enum string for %d is %s\n",  104, EnumHelper<WeekDay>::to_string(
    static_cast<WeekDay>(104) ) );

//     for a bit flags enum, returns comma-separated list
//     enum Color { Blue = 1, Red = 2 }   3 result  Blue, Red
printf("enum string for %d is %s\n",  3, EnumHelper<Color>::bitflags_to_string(
    static_cast<WeekDay>(3) ) );

从字符串到枚举器

// .NET equivalent:
//     WeekDay day = Enum.Parse( typeof(WeekDay), "Sunday");
printf("enum value for %s is %d(case-insensitive)\n"
    , test_enum_str, EnumHelper<WeekDay>::parse( test_enum_str, true ) );

try {
    EnumHelper<WeekDay>::parse( test_enum_str, false );
} catch( runtime_error & e) {
    fprintf(stderr, "exception: %s\n", e.what() );
}
test_enum_str = "Sunday";
printf("enum value for %s is %d(case-sensitive)\n",  test_enum_str,
    EnumHelper<WeekDay>::parse( test_enum_str) );

//     for a bit flags enum, parse a comma-separated list
//     enum Color { Blue = 1, Red = 2 }   parse  "Blue, Red" result 3
printf("enum string [Blue, Red] is %d\n", (int) EnumHelper<Color>::bitflags_parse(
    "Blue, Red" ) );

查找枚举器值的索引

// .NET equivalent: No
printf("enum value %d is the %d-th item in the declaration order\n", 
    0, EnumHelper<WeekDay>::index_of(0) );
//     return -1 for the non exist value
printf("enum value %d is the %d-th item in the declaration order\n", 
    104, EnumHelper<WeekDay>::index_of(104) );

查找枚举器字符串的索引

// .NET equivalent: No
printf("enum string [%s] is the %d-th item in the declaration order\n", test_enum_str,
    EnumHelper<WeekDay>::index_of(test_enum_str) );
//     return -1 for the non exist strings:
test_enum_str = "not exist";
printf("enum string [%s] is the %d-th item in the declaration order\n", test_enum_str,
    EnumHelper<WeekDay>::index_of(test_enum_str) );

关注点

上述 ALL_THE_WEEK 定义为函数式宏,而其使用是变量式。通过这样做,我们可以用不同的参数扩展宏;这就是“x”和“y”参数存在的原因!

#define DEF_ENUM(ns_cls, enum_type, list) \
    DEF_ENUM_ONLY_(enum_type, list(1, ns_cls) ) \
    REGISTER_ENUM_META_DATA_(ns_cls, enum_type, list(2, ns_cls) )

以“_”结尾的宏仅供内部使用

#define DEF_ENUM_ONLY_(enum_type, enum_list )  enum enum_type { enum_list };
#define ENUM_ITEM_VALUE_1_(ns_cls, enum_entry, enum_value)  enum_entry = enum_value,
#define ENUM_ITEM_1_(ns_cls, enum_entry)  enum_entry ,

#define ENUM_ITEM_VALUE(_12, ns_cls, enum_entry, enum_value) \
        ENUM_ITEM_VALUE_##_12##_(ns_cls, enum_entry, enum_value)
#define ENUM_ITEM(_12, ns_cls, enum_entry) ENUM_ITEM_##_12##_(ns_cls, enum_entry)

因此,宏列表(1, ns_cls)将在第一遍中扩展为 ALL_THE_WEEK(1, ns_cls)。然后,ALL_THE_WEEK(1, ns_cls) 将扩展为 ENUM_ITEM(1, ns_cls, Sunday)ENUM_ITEM_VALUE(1, ns_cls, Thursday, 25) 的列表;反过来,ENUM_ITEM(1, ns_cls, Sunday) 将在第三遍中扩展为 ENUM_ITEM_1_(ns_cls, Sunday)ENUM_ITEM_VALUE(1, Thursday, 25) 将扩展为 ENUM_ITEM_VALUE_1_(ns_cls, Thursday, 25)。最后,ENUM_ITEM_1ENUM_ITEM_VALUE_1_ 宏将扩展为以下列表:

Sunday, Tuesday, Wednesday, Thursday = 25... etc

这是一个 C/C++ 枚举定义的枚举器列表。

为了使模板类工作,有必要将枚举的值及其字符串表示形式“保存”在特定于枚举的模板类实例中。这就是 REGISTER_ENUM_META_DATA_ 宏和 ENUM_ITEM_2_ (及类似) 宏所做的工作。

让简单的事情变得简单,让困难的事情变得可能

上述枚举操作假设枚举是在文件作用域中定义的。

要在命名空间作用域中定义它,必须采用两阶段定义:

#define ALL_THE_WEEK(x, y)              \
ENUM_ITEM_VALUE(x, y, Sunday, 1)        \
ENUM_ITEM_VALUE(x, y, Monday, 3)        \
ENUM_ITEM_VALUE(x, y, Tuesday, Sunday)  \
ENUM_ITEM_VALUE(x, y, Wednesday, 10)    \
ENUM_ITEM_VALUE(x, y, Thursday, 7)      \
ENUM_ITEM(x, y, Friday )                \
ENUM_ITEM_VALUE(x, y, Saturday, 12)

namespace C {
DEF_ENUM_1(C, WeekDay, ALL_THE_WEEK)
};
DEF_ENUM_2(C, WeekDay, ALL_THE_WEEK)

有必要将枚举本身定义在类 C 内部,并在类外部实例化模板类。这是不可避免的,因为 C++ **不允许**为类内部的任意静态数据指定初始化器。根据实践,我认为 DEF_ENUM_1DEF_ENUM_2 比其他宏更容易记住。

也可以在匿名命名空间中定义枚举:

namespace {
DEF_ENUM_1(, WeekDay, ALL_THE_WEEK)
}
DEF_ENUM_2(, WeekDay, ALL_THE_WEEK)

在命名空间中定义枚举

namespace F {
...
DEF_ENUM_1(F, WeekDay, ALL_THE_WEEK)
...
}
DEF_ENUM_2(F, WeekDay, ALL_THE_WEEK)

在命名空间和类中定义枚举

namespace F {
    class C {
    private:
        DEF_ENUM_1(F::C, WeekDay, ALL_THE_WEEK)
        friend class EnumHelper<weekday>;
    }
}
DEF_ENUM_2(F::C, WeekDay, ALL_THE_WEEK)

请注意,在类内部,允许使用 private enum,但要使枚举可被 EnumHelper 访问,需要进行 friend 声明。我选择不在宏中包含 friend 声明,因为我希望保持命名空间和类语法的统一性以简化事情。宏无法区分其周围的上下文。 friend 声明在命名空间作用域中是非法的。

在命名空间/类中定义枚举时,您需要使用适当的前缀来访问它,这也不例外:

printf("[%s]\n", EnumHelper<F::C::WeekDay>::to_string(F::C::Sunday) );

使用 Visual C/C++ 枚举定义扩展

对于 Microsoft Visual C/C++,可以为枚举指定基础类型:

enum Color : short { Red, Blue };

您可以在文件作用域中使用以下宏:

DEF_ENUM_WITH_TYPE(Color , short, ALL_THE_COLORS)
//instead of
DEF_ENUM(Color , ALL_THE_COLORS)

在命名空间/类中使用以下宏:

DEF_ENUM_WITH_TYPE_1(namespace::class, Color , short, ALL_THE_COLORS)
DEF_ENUM_WITH_TYPE_2(namespace::class, Color , short, ALL_THE_COLORS)
instead of
DEF_ENUM_1(namespace::class, Color , ALL_THE_COLORS)
DEF_ENUM_2(namespace::class, Color , ALL_THE_COLORS)

对于所有宏,参数从左到右的顺序是:命名空间和类(如果存在)、枚举名称、基础类型(如果存在)、枚举器列表。

缺点

不幸的是,我还没有找到一种方法来支持函数内部(以及函数内部的类内部)的枚举与此解决方案一起使用。

历史

  • 2009 年 5 月 27 日:首次发布。
  • 2010 年 9 月 24 日:使用宏技巧改进可用性,支持命名空间和类内部的枚举。
© . All rights reserved.