【C++】【デバッグ】enum classの列挙定数を文字列で取得する方法


こんにちは。Militumと申します。
Qiita初投稿です。
よろしくお願い致します。

更新履歴

  • 2019/03/10 サンプルコードの std::endl"\n" に修正
  • 2019/03/31 GitHubリンク及びライセンス表記追加

【概要】

 C++でenum classを使用したロジックをデバッグする際、
 現在どの値が変数にセットされているのか数値ではなく、列挙定数を文字列で取得して、デバッグをやりやすくする方法を紹介します。
 因みに、私はStateパターンの状態遷移時のログ出力で活用しています。

【目的】

 enum class で定義した文字列を変数から取得する

【とりあえず実装してみる】

マクロとSTLを用います。
::Split は文字列を任意の区切り文字で分割する自作関数だと読み替えてください。

#include <iostream>
#include <string>
#include <vector>

namespace
{
#define DEFINE_ENUM_CLASS(Type,...)\
    enum class Type\
    {\
        __VA_ARGS__\
    };\
    static std::string ToString(Type value)\
    {\
        static std::vector<std::string> names = ::Split(#__VA_ARGS__, ',');\
        return names.at(static_cast<int32_t>(value));\
    }

    DEFINE_ENUM_CLASS(
        State,
        None,Init,Idle
    );
}

int main(void)
{
    // enum classを直指定する場合
    std::cout << static_cast<int32_t>(::State::Init) << "\n";
    std::cout << ::ToString(::State::Init) << "\n";

    // enum classの変数から文字列を取得する場合
    const State stateValue = ::State::Idle;
    std::cout << static_cast<int32_t>(stateValue) << "\n";
    std::cout << ::ToString(stateValue) << "\n";
}
  • 実行結果
1
Init
2
Idle

【欠点と制約】

これで実装完了! めでたしめでたし...でしたら良いのですが、そうはいきません。

return names.at(static_cast(value));

この実装には重大な欠点と制約があります。

  • 定義が零オリジンである必要がある
  • 値に隙間があってはならない

このようなケースで使用してしまうと、ToStringを呼び出した際に、out of range例外の送出や、異なる文字列を取得する場合があります。
デバッグ用途で用意した機能が不具合の原因になるのはいただけません。
よって、次のようなアプローチを試みます。

【改善案】

  • 定義が零オリジンである必要がある
  • 値に隙間があってはならない

これらの欠点をなくす為、vector配列で管理するのではなく、連想配列(unordered_map)で管理してみます。

#include <iostream>
#include <string>
#include <tuple>
#include <unordered_map>
#include <vector>

namespace
{
#define DEFINE_ENUM_CLASS(Type,...)\
    enum class Type\
    {\
        __VA_ARGS__\
    };\
    static std::string ToString(Type value)\
    {\
        static std::unordered_map<Type, std::string> nameMap;\
        if(nameMap.size() == 0)\
        {\
            const std::vector<std::string> names = ::Split(#__VA_ARGS__, ',');\
            nameMap = ::MakeEnumClassMap<Type>(names);\
        }\
        if(nameMap.count(value) == 0)\
        {\
            return "unknown value";\
        }\
        return nameMap.at(value);\
    }

    /// <summary>
    /// 定義したenumの値を取り出す
    /// = を記述していない場合は第二引数の値を使用する
    /// </summary>
    std::tuple<int32_t, std::string> FetchEnumValuePair(const std::string& source, const int32_t defaultValue)
    {
        const std::vector<std::string> values = ::Split(source, '=');
        if (values.size() == 1)
        {
            return {defaultValue, values.at(0)};
        }
        // unordered_mapなので、a, b = a は未対応
        return {std::stoi(values.at(1)), values.at(0)};
    }

    /// <summary>
    /// 定義したenum classの記述を元に、
    /// 列挙定数 => 定数と同じ文字列 の連想配列を作成
    /// </summary>
    template<class Type>
    std::unordered_map<Type, std::string> MakeEnumClassMap(const std::vector<std::string>& labels)
    {
        std::unordered_map<Type, std::string> result;
        int32_t defaultValue = 0;
        for(const std::string& label : labels)
        {
            const auto valueSet = ::FetchEnumValuePair(label, defaultValue);
            // 重複不可
            result[static_cast<Type>(std::get<0>(valueSet))] = std::get<1>(valueSet);
            defaultValue = std::get<0>(valueSet)+1;
        }
        return result;
    }

    DEFINE_ENUM_CLASS(
        State,
        None=8,
        Init=2,
        Three  // 3
    );
}

int main(void)
{
    std::cout << ::ToString(::State::None) << "\n";
    std::cout << ::ToString(::State::Init) << "\n";
    std::cout << ::ToString(::State::Three) << "\n";
    std::cout << ::ToString(static_cast<::State>(100)) << "\n";
}
  • 実行結果
None
 Init
 Three
unknown value

スペース消去をしていないので、結果にスペースが混ざっていますが、
デバッグする分には困らないのと、欠点と制約がなくなっている為、大きな問題ではないと判断します。

【断念したこと】

  • MakeEnumClassMap の 可変長引数テンプレート関数化
    enum class を可変長引数テンプレートで展開できれば、MakeEnumClassMap をもう少し見やすく出来そうでしたが、
    use of undeclared identifier 'None' エラーを解消できなかった。(enumであれば出来た)

  • 列挙定数で計算式が記述されているケース

enum class Sample
{
   One = 1,
   Two = 1 << 1,
   Three = One + Two
}

stoi で数値に出来ない為、計算を行っている定数以降もずれていきます。
また、そもそも複雑な定義をしている事が不具合の温床になっているかもしれないので、
このようなケースの場合は、違う値を割り当てるなどをして、原因を狭めていった方が良いのでは、と思います。

【まとめ】

enum class で定義した列挙定数を定数の文字列で取得する方法を紹介しました。
これにより、今まで数値を見てデバッグをしていた箇所が意味のある文字列として表示出来るようになるため、変数を書き換える直前(冒頭のStateパターンだと状態を切り替えるとき)にログを仕込むことで、格段に不具合の原因を追いやすくなるのではないでしょうか?

Qiitaは初投稿で不明点ばかりでしたが、気づいた箇所等ありましたら、コメントをいただけますと幸いです。

【GitHub】

今回作成したソースコードはこちら

【ライセンス表記】

These codes are licensed under CC0.