Skip to content

Enum Reflection

ZengJingtao edited this page Jan 30, 2023 · 2 revisions

1. Overview

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.

2. Usage

2.2 Macro definition

// 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...

3. Supported features

3.1 Functions

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();

3.2 Examples

#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;
}

4. Application Scenario

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.

5. Implementation details

In order to highlight the key points, only a few key points in the implementation are described.

5.1 s_name and s_value

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.

5.2 Macro ROCKSDB_PP_MAP(map,ctx,...)

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).

5.3 EnumValue = SomeValue is a whole

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).

5.4 Initialization of s_name

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.

5.5 Initialization of s_value

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 -).

5.6 Preprocessing & C++:

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.

6. Precautions

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.

Clone this wiki locally