如何反射枚举变量

2023/07/01 C++ 反射 共 5715 字,约 17 分钟

写在前面

这里分享一个C++在编译期枚反射枚举的一个方法。

__PRETTY_FUNCTION__

__PRETTY_FUNCTION__为我们提供了一个在编译期获得枚举变量字符串的可能性,__PRETTY_FUNCTION__的作用是在编译期,获取当前函数的签名。因此,可以借用这个方法获取枚举变量的字符串。

#include <iostream>
#include <string_view>

enum Fruit {
  APPLE,
  BANANA,
  ORANGE,
  COUNT, // COUNT == 3.
};

template <typename E, E V> constexpr std::string_view get_raw_name() {
#ifdef _MSC_VER
  return __FUNCSIG__;
#else
  return __PRETTY_FUNCTION__;
#endif
}

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wenum-constexpr-conversion"
int main() {
  std::cout << get_raw_name<Fruit, static_cast<Fruit>(2)>() << std::endl;
  std::cout << get_raw_name<Fruit, static_cast<Fruit>(10)>() << std::endl;
  std::cout << get_raw_name<int, 5>() << std::endl;
}
#pragma clang diagnostic pop

首先编写一个获取信息的 get_raw_name,这样在编译的时候,就会返回一个带有枚举信息的字符串,其结果如下:

// g++
/*
constexpr std::string_view get_raw_name() [with E = Fruit; E V = ORANGE; std::string_view = std::basic_string_view<char>]
constexpr std::string_view get_raw_name() [with E = Fruit; E V = (Fruit)10; std::string_view = std::basic_string_view<char>]
constexpr std::string_view get_raw_name() [with E = int; E V = 5; std::string_view = std::basic_string_view<char>]
*/

// clang
/*
std::string_view get_raw_name() [E = Fruit, V = ORANGE]
std::string_view get_raw_name() [E = Fruit, V = (Fruit)10]
std::string_view get_raw_name() [E = int, V = 5]
*/

// mvsc
/*
class std::basic_string_view<char,struct std::char_traits<char> > __cdecl get_raw_name<enum Fruit,ORANGE>(void)
class std::basic_string_view<char,struct std::char_traits<char> > __cdecl get_raw_name<enum Fruit,(enum Fruit)0xa>(void)
class std::basic_string_view<char,struct std::char_traits<char> > __cdecl get_raw_name<int,0x5>(void)
*/

尽管编译期输出的字符串不统一,但都带有所有的字符串信息,因此可以提取出这些字符串,这样就可以得到宏的名字了。

反射

不过上面的还不够,上面只是能简单的获取了宏变量在编译期的名字,但是这里的反射,希望能够通过一个类型,自动获取该类型的所有变量的名字和值,并且希望是一个map的形式,类似以下:

static constexpr auto str_to_enum = get_enum_map<true, Fruit>();
static constexpr auto enum_to_str = get_enum_map<false, Fruit>();

通过观察上面的字符串,我们可以发现,其中如果用static_cast强制转换了的数字超过了该类型的范围和合法的枚举类型是显然不一样的。因此,我们可以假定一个方案:通过枚举数字比如1-50,都进行强制转换,最终判断出合法的和非法的,合法的就可以插入哈希表中,最终在遍历了数字之后,就可以得到一个反射的map了。大概实现如下:

#pragma once
#include <string_view>
#include <array>
#include "frozen/string.h"
#include "frozen/unordered_map.h"

namespace iguana {

// clang 17需要使用
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wenum-constexpr-conversion"

template <typename E, E V> constexpr std::string_view get_raw_name() {
#ifdef _MSC_VER
  return __FUNCSIG__;
#else
  return __PRETTY_FUNCTION__;
#endif
}

template<typename E, E V>
constexpr inline std::pair<bool, std::string_view> try_get_enum_name() {
  constexpr std::string_view sample_raw_name = get_raw_name<int, 5>();
  constexpr size_t pos = sample_raw_name.find("5");
  constexpr std::string_view enum_raw_name = get_raw_name<E, V>();
  constexpr auto enum_end = enum_raw_name.rfind(&sample_raw_name[pos + 1]);
#ifdef _MSC_VER
  constexpr auto enum_begin = enum_raw_name.rfind(',', enum_end) + 1;
#else
  constexpr auto enum_begin = enum_raw_name.rfind(' ', enum_end) + 1;
#endif
  constexpr std::string_view res = enum_raw_name.substr(enum_begin, enum_end - enum_begin);
  
  constexpr size_t pos_brackets = res.find(')');

  size_t pos_colon = res.find("::");
  return {pos_brackets == std::string_view::npos,
          res.substr(pos_colon == std::string_view::npos ? 0 : pos_colon + 2)};
}

template <typename E, std::size_t I>
constexpr auto get_enum_name_helper() {
    return try_get_enum_name<E, static_cast<E>(I)>();
}

template <typename E, std::size_t... Is>
constexpr inline auto get_enum_arr(const std::index_sequence<Is...>&) {
  constexpr std::size_t N = sizeof...(Is);
  std::array<std::string_view, N> enum_names= {};
  std::array<E, N> enum_values = {};
  std::size_t num = 0;
  (([&]() {
      constexpr auto res = get_enum_name_helper<E, Is>();
      if constexpr (res.first) {
          enum_names[num] = res.second;
          enum_values[num] = static_cast<E>(Is);
          ++num;
      }
  })(), ...);
  return std::make_tuple(num, enum_values, enum_names); 
}

 

template <typename E, size_t N, size_t... Is>
constexpr inline auto get_enum_to_str_map(
    const std::array<std::string_view, N> &enum_names, const std::array<E, N> &enum_values,
    const std::index_sequence<Is...>&) {
  return frozen::unordered_map<E, frozen::string, sizeof...(Is)>{
      {enum_values[Is], enum_names[Is]}...};
}

template <typename E, size_t N, size_t... Is>
constexpr inline auto get_str_to_enum_map(
    const std::array<std::string_view, N> &enum_names, const std::array<E, N> &enum_values,
    const std::index_sequence<Is...>&) {
  return frozen::unordered_map<frozen::string, E, sizeof...(Is)>{
      {enum_names[Is], enum_values[Is]}...};
}

template <bool str_to_enum, typename E, size_t N = 50>
constexpr inline auto get_enum_map() {
  constexpr auto t = get_enum_arr<E>(std::make_index_sequence<N>());

  if constexpr (str_to_enum) {
    return get_str_to_enum_map<E, N>(
        std::get<2>(t), std::get<1>(t),
        std::make_index_sequence<std::get<0>(t)>{});
  } else {
    return get_enum_to_str_map<E, N>(
        std::get<2>(t), std::get<1>(t),
        std::make_index_sequence<std::get<0>(t)>{});
  }
}
#pragma clang diagnostic pop

} // namespace iguana

// clang++-14 test_util.cpp -I .  -std=c++17  -g -o test

其中的frozen是第三方库,可以生成静态的哈希表!测试代码如下:

#include "util.hpp"
#include <iostream>

using namespace iguana;

enum class Fruit {
  APPLE = 5,
  BANANA = 4 ,
};



int main() {
  static constexpr auto str_to_enum = get_enum_map<true, Fruit>();
  static constexpr auto enum_to_str = get_enum_map<false, Fruit>();
  std::cout << static_cast<int>(str_to_enum.find("APPLE")->second) <<std::endl;
  auto str = enum_to_str.find(Fruit::APPLE)->second;
  std::cout << std::string_view(str.data(),str.size()) <<std::endl;
}

运行结果如下:

5
APPLE

缺点

目前缺点是只能针对小范围的,大范围的编译时间太长

Reference

Enum reflection in C++ with template metaprogramming (taylorconor.com)

purecpp - a cool open source modern c++ community

Search

    Table of Contents