-
Notifications
You must be signed in to change notification settings - Fork 0
Enum Reflection
In short, reflection in a programming language refers to obtaining information such as the type of the language itself from the runtime. C++ lacks such a mechanism. For the simplest enum type, we may be able to implement an enum with reflection. We have implemented several macros, and the enum defined by the macros automatically has the reflection feature.
// can be called in any namespace, not in struct/class
#define ROCKSDB_ENUM_PLAIN(EnumType, IntRep, ...) details...
#define ROCKSDB_ENUM_CLASS(EnumType, IntRep, ...) details...
// can be called in any namespace, not in struct/class
#define ROCKSDB_ENUM_PLAIN_INCLASS(EnumType, IntRep, ...) details...
#define ROCKSDB_ENUM_CLASS_INCLASS(EnumType, IntRep, ...) details...
Supported functions are defined in the global namespace:
template<class Enum> Slice enum_name(Enum v);
template<class Enum> bool enum_value(const Slice& name, Enum* result);
/// for convenient
template<class Enum> Enum enum_value(const Slice& name, Enum Default);
// use case:
// enum_for_each([](Slice name, Enum val){...});
template<class Enum> void enum_for_each(Func fn);
template<class Enum> std::string enum_str_all_names();
#include <rocksdb/enum_reflection.hpp>
// call this macro in namespace, not in class/struct
ROCKSDB_ENUM_CLASS(MyEnum, char,
Value1,
Value2 = (SomeTemplate<1,2>::value),
Value3 = 30 // Restriction: no commas here
)
The macro expansion above produces the following code:
// enum definition from macro expansion
enum class MyEnum : int {
Value1, Value2 = (SomeTemplate<1,2>::value), Value3 = 30
};
// Reflection function from macro expansion:
int enum_rep_type(MyEnum*);
inline Slice enum_str_define(MyEnum*) {
return "enum class MyEnum : int"
" { Value1, Value2 = (SomeTemplate<1,2>::value), Value3 = 30 }";
}
inline std::pair<const Slice*, size_t>
enum_all_names(MyEnum*) {
static const Slice s_names[] = {
var_symbol("Value1"),
var_symbol("Value2 = (SomeTemplate<1,2>::value)"),
var_symbol("Value3 = 30")
};
return std::make_pair(s_names, sizeof(s_names)/s_names[0]);
}
inline const MyEnum* enum_all_values(MyEnum*) {
static const MyEnum s_values[] = {
EnumValueInit() - MyEnum::Value1,
EnumValueInit() - MyEnum::Value2 = (SomeTemplate<1,2>::value),
EnumValueInit() - MyEnum::Value3 = 30
};
return s_values;
}
The most typical application scenario is to process configuration information, convert the string configured by the user into an Enum value, and convert the Enum into a string when writing the Log. For example, there are a large number of such scenarios in RocksDB.
Currently, this enum reflection has been submitted to RocksDB as a Pull Request to improve a large number of manually implemented enum reflections (samples) in RocksDB.
In order to highlight the key points, only a few key points in the implementation are described.
s_name
and s_value
are parallel arrays. The name of some enumeration item is s_name[i]
, and its value is s_value[i]
. These two parallel arrays can be used to implement almost all reflection functions, and they are defined in enum_all_names and enum_all_values respectively. The key point is how to generate s_name and s_value through macro expansion.
Traverse the variable parameter list of the macro to generate a result list. The implementation of this macro contains a little trick, but the maximum length of the variable parameter list is limited to 61 (Visual C++ supports up to 127 macro parameters, and gcc supports almost unlimited macro parameters parameter).
A grammatical structure such as EnumName = SomeValue
, when used as a macro parameter, is a whole. You can turn it into a string "EnumName = SomeValue"
. Other than that, you cannot perform other operations on it (the disassembly we expect).
As the name of enum, in EnumName = SomeValue
, we only need EnumName, which is easier to handle. We have implemented a var_symbol function from which EnumName can be split. In the initialization list of s_name, we use ROCKSDB_PP_MAP
to call the var_symbol function one by one to generate EnumName. Therefore, compared to the initialization of s_value, the initialization of s_name is relatively simple.
EnumName = SomeValue
is also processed in the initialization of s_value, because to get the value of EnumName instead of its string form, what we have to deal with is the entire grammatical structure of EnumName = SomeValue
, where = SomeValue
is optional. So we should only Keep EnumName, and delete = SomeValue
. This requirement cannot be fulfilled in the preprocessor.
We can only find a way to use C++ syntax to realize the function of deleting = SomeValue
, which can be realized by operator overloading:
template<class Enum>
class EnumValueInit {
Enum val;
public:
operator Enum() const { return val; }
EnumValueInit& operator-(Enum v) { val = v; return *this; }
template<class IntRep> /// absorb the IntRep param
EnumValueInit& operator=(IntRep) { return *this; }
};
Thus, with EnumValueInit, we can define an expression that accepts EnumName or EnumName = SomeValue
and produces a value that is always EnumName. This expression is:
EnumValueInit() - EnumName = SomeValue
Here, EnumValueInit() constructs an object, then applies the - operator on the object, saves the value corresponding to EnumName into the val member, and then calls the = operator, which does nothing, which is equivalent to Removed the later =SomeValue part.
Finally, because the element type of s_value is Enum, operator Enum
will be called to return the saved val. This expression is equivalent to just adding something in front of EnumName = SomeValue
. In the implementation, you can directly use the predefined ROCKSDB_PP_PREPEND
macro as the map function of ROCKSDB_PP_MAP
, and its ctx is the prefix of prepend, that is, the aforementioned EnumValueInit() -
(note the following -
).
Macro expansion only provides the most basic reflection information, use templates to implement some wrapper functions, and wrap the reflection information of macro expansion.
There are two reasons to wrap s_names and s_values with inline functions:
Overloads are provided for different Enum types. Guaranteed initialization order: The initialization order of global objects in different translation units is uncertain. If, like v2, the initialization order of s_name and s_value is uncertain from the initialization order of global objects in other translation units, if in other translation units A global object (indirectly) calls Enum Reflection, which may result in access to uninitialized s_name and s_value. In addition, using C++'s parameter dependent name lookup function allows enum to be defined in any namespace, even within class/struct.
enum_rep_type is used to derive RepType, currently only used to generate format strings for printf.
When the enum is defined within the class/struct, the inline
in the macro expansion becomes a friend
, which is necessary, otherwise the related function will become a member function of the class/struct surrounding the enum.
For example, Value2 = (SomeTemplate<1,2>::value)
in the sample code, the parentheses are necessary, because the preprocessor does not know the parentheses <> of the template, and the absence of parentheses will cause macro expansion errors, which is a basic principle when mixing macros and templates.