C++ 中对枚举类型提供 .NET 式的反射支持
本文提供了一个宏 + 模板解决方案,以支持枚举类型的 .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_WEEK
、ENUM_ITEM
、ENUM_ITEM_VALUE
。
定义支持反射的枚举
DEF_ENUM(, WeekDay, ALL_THE_WEEK)
有点奇怪的是,宏的第一个参数留空了。注意:这不是一个错字!它是用于命名空间的,在这种情况下,它位于全局命名空间中。对于全局命名空间或默认命名空间,它必须留空。
ENUM_ITEM
、ENUM_ITEM_VALUE
、DEF_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_1
和 ENUM_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_1
和 DEF_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 日:使用宏技巧改进可用性,支持命名空间和类内部的枚举。