😎

C++20のコンパイル時 Enum名取得

2024/01/30に公開
2

C++20 から <source_location> と言う標準ライブラリが実装された、この機能はコンパイル時/実行時にソースコードの位置の名前を取得すると言うものである。
これを悪用することで、enum の名前を取得することができるようになる。
基本的には __FUNCTION__ や __PRETTY_FUNCTION__ として定義される識別子を用いた方法と変わらないが、ここではもっと簡潔に書いたバージョンを紹介する。また、 __FUNCTION__ や __PRETTY_FUNCTION__ と違って処理系定義ではないので、 MSVC などのコンパイラーでも動く。
ちなみに source_locationfunction_name で帰ってくる返り値は処理系定義なので、 clang 以外のコンパイラーで動くことは保証できない。

If this object represents a position in a body of a function, returns an implementation-defined null-terminated byte string corresponding to the name of the function.(cppreference)

実装

#include <source_location>
#include <string_view>

template <auto T>
consteval auto GetEnumName() {
  auto fn = std::string_view(std::source_location::current().function_name());
  auto p = fn.find("T = ");
  return fn.substr(p + 4, fn.size() - p - 5);
};

template <class T, int First, int Last>
auto GetEnumName(T v) {
  std::string_view en;
  [&v, &en]<auto... I>(std::index_sequence<I...>) {
    (void)((en = GetEnumName<static_cast<T>(I)>(),
            static_cast<int32_t>(v) == I) ||
           ...);
  }(std::make_index_sequence<Last - First + 1>{});
  return en;
}

使用例 (Compiler Explorer)

例1 コンパイル時に名前を取得する

#include <print>

enum class Test {
  Foo,
  Bar,
  Bazz,
}

auto main() -> int {
  std::println(GetEnumName<Test::Foo>());  // -> Test::Foo
  std::println(GetEnumName<Test::Bazz>()); // -> Test::Bazz
}

例2 実行時に名前を取得する

#include <print>

enum class Alphabet {
  A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z,
};

constexpr auto GetAlphabetName = []<class T>(T v) {
  return GetEnumName<T, 0, 25>(std::forward<T>(v));
};

auto main() -> int {
  std::println("{}", GetAlphabetName(Alphabet::A)); // -> Alphabet::A
  std::println("{}", GetAlphabetName(Alphabet::B)); // -> Alphabet::B
  std::println("{}", GetAlphabetName(Alphabet::C)); // -> Alphabet::C
  std::println("{}", GetAlphabetName(Alphabet::Z)); // -> Alphabet::Z
  return 0;
}

コンパイル時の境界チェック

C++ の規格に基づくと列挙型範囲外へのキャストは未定義動作である。([expr.static.cast])
したがって、使用は推奨しない。実際に llvm ではコンパイル時のキャストは不適格そして、実行時のコンテキストでも Warning になろうとしている。

実装

以下は 0 始まりの連続した値を取ると仮定し、 0 から underlying_type_t の最大の値まで二分探索を行うコードである。

template <class T, std::underlying_type_t<T> L = 0,
          std::underlying_type_t<T> H =
              std::numeric_limits<std::underlying_type_t<T>>::max(),
          bool Enabled = true>
struct UpperBound {
  static constexpr bool found =
      !GetEnumName<static_cast<T>((L + H) / 2)>().contains('(');
  static constexpr int value =
      (H - L < 2) ? L
                  : UpperBound<T, L, (L + H) / 2, !found>::value +
                        UpperBound<T, (L + H) / 2, H, found>::value;
};

template <class T, std::underlying_type_t<T> L, std::underlying_type_t<T> H>
struct UpperBound<T, L, H, false> {
  static constexpr std::underlying_type_t<T> value = 0;
};

これを用いれば以下のように GetEnumName を記述することができる

template <class T, int First = 0, int Last = UpperBound<T>::value>
auto GetEnumName(T v) {
  std::string_view en;
  [&v, &en]<auto... I>(std::index_sequence<I...>) {
    (void)((en = GetEnumName<static_cast<T>(I)>(),
            static_cast<int32_t>(v) == I) ||
           ...);
  }(std::make_index_sequence<Last - First + 1>{});
  return en;
}
GitHubで編集を提案

Discussion

yaito3014yaito3014

また、 __FUNC__ と違って処理系定義ではないので、 MSVC などのコンパイラーでも動く。

処理系が独自に提供している識別子という意味ならば __FUNCTION____PRETTY_FUNCTION__ などを意図していますか?
似たような名前の __func__ は C99 および C++11 で標準化されているのでどの処理系でも使えるべきですが…

gen740gen740

__FUNC____FUNCTION____PRETTY_FUNCTION__ を意図していますね。こんがらがってしまっていたので修正します。ありがとうございます!
ちなみに __func__ はテンプレートパラメーターの型の名前とかを含まないのでこのようなトリックを使うことはできないんですよね。