メンバ変数を列挙する

メンバ変数を列挙する

2025-05/14

普通メンバ変数を列挙したいことなど無いと思いますが、シリアライザを作る際に必要になったので、残しておきます。C++ の黒魔術をやりたい方には面白いかもしれません。

C++11 で記述し、次のような動作を目指します。

struct Vec2
{
    double x;
    double y;
};

struct Rect
{
    Vec2 pos;
    Vec2 size;
};

int main()
{
    Rect rect{ { 10, 20 }, { 30, 40 } };

    Print(rect);
}
object: {
    object: {
        double: 10
        double: 20
    }
    object: {
        double: 30
        double: 40
    }
}

C++ には C# のリフレクションのように実行時にメンバ変数を列挙する機能がありません。そこでテンプレート黒魔術を駆使してコンパイル時にメンバ変数を列挙するコードを生成させます。

実装に当たり以下のシリアライザの実装を参考にしています。https://github.com/USCiLab/cereal

とりあえず列挙する

列挙するためのクラスを作成し、メンバ変数を列挙させます。

まず、列挙対象の構造体に列挙用の関数を追加し、列挙者クラス E がメンバ変数にアクセスできるようにします。テンプレートにしているのは、他にも列挙者を作成する可能性があるためです。

実は boost に列挙用の関数を作らずともメンバの列挙ができるライブラリがあるのですが、超黒魔術です。https://github.com/boostorg/pfr

struct Vec2
{
    double x;
    double y;

    template <typename E>
    void enumerate(E& e)
    {
        e.enumeration(x, y);
    }
};

次に列挙者クラスの実装です。enumeration 関数は可変長引数を取るため、引数を展開する必要があります。unpack 関数が再帰的に展開し、各引数は print 関数に渡されます。

#include <type_traits>
#include <iostream>

class Enumerator
{
public:
    template <typename... Args>
    void enumeration(Args&&... args)
    {
        unpack(std::forward<Args>(args)...);
    }

private:

    // 可変長引数展開
    template <typename Head, typename... Tails>
    void unpack(Head&& head, Tails&&... tails)
    {
        print(std::forward<Head>(head));  // 引数の頭だけ取り出し print へ渡す
        unpack(std::forward<Tails>(tails)...);
    }

    // 展開終端
    void unpack() {}

    template <typename T>
    void print(T&& value)
    {
        std::cout << value << std::endl;
    }
};

Print 関数で列挙者をインスタンス化し、列挙を開始します。

template <typename T>
void Print(T&& t)
{
    Enumerator e;
    t.enumerate(e);
}

メイン関数は次のようになります。

int main()
{
    Vec2 vec{ 100, 1 };
    Print(vec);
}

出力は次のようになります。ひとまず引数を展開して列挙することができました。

100
1

再帰的に列挙できるようにする

今のところ、メンバ変数にユーザー定義型のメンバ変数が含まれる場合、再帰的に列挙してくれません。例えば次のような構造体 Rect は列挙できません。

struct Vec2
{
    double x;
    double y;

    template <typename E>
    void enumerate(E& e)
    {
        e.enumeration(x, y);
    }
};

struct Rect
{
    Vec2 pos;
    Vec2 size;

    template <typename E>
    void enumerate(E& e)
    {
        e.enumeration(pos, size);
    }
};

そこで、enumerate 関数が定義されている型が引数にわたってきた場合、再帰的に enumerate を呼び出すようにします。まずは、enumerate 関数が定義されているかをコンパイル時に判定するメタ関数を作成します。

struct DummyEnumerator {};

template <typename T>
using VoidT = void;

/// @brief T に enumerate() が存在するかどうかを判定する
template <typename T, typename = void>
struct IsEnumerable
    : std::false_type
{
};

template <typename T>
struct IsEnumerable<T, VoidT<decltype(std::declval<T>().enumerate(std::declval<DummyEnumerator&>()))>>
    : std::true_type
{
};

黒魔術ぽくなってきました。順に判定する仕組みを説明します。

  1. decltype(std::declval<T>().enumerate(std::declval<DummyEnumerator&>()))

    T 型の enumerate メンバ関数が存在するかを判定します。

    存在する場合、decltypeenumerate メンバ関数の戻り値型を返します。存在しない場合、decltype は SFINAE によって無視され、型の実体化がスキップされます。

    std::declval は、型 T のインスタンスを生成せずに、T 型インスタンスの情報を取得するために使います。イメージは次のような感じ

    T t;
    DummyEnumerator e;
    decltype(t.enumerate(e));
    

    ただしこの場合、T がデフォルトコンストラクタを持つ型である必要があります。std::declval を使うことで、デフォルトコンストラクタを持たない型でもインスタンスを生成せずに、メンバ関数の情報を取得できて便利。

  2. VoidT<decltype(...)> は decltype(…) が実体化されている場合、void 型になります。つまり、enumerate メンバ関数が存在する場合、VoidT は void 型になり、存在しない場合、これまた実体化がスキップされます。(std::void_t は C++ 17 から追加されたので、C++ 11 では自前で定義する必要があります)

  3. IsEnumerable は部分特殊化されており、第二引数が void の場合 std::true_type を継承する特殊化が生成されます。それ以外の場合、std::false_type を継承する型のみが生成されます。ややこしや。

    部分特殊化されている部分だけ抜き出すとこんな感じ。VoidT<decltype(...)> が void 型に実体化した場合。

    template <typename T, typename = void>
    struct IsEnumerable
        : std::false_type
    {
    };
    
    template <typename T>
    struct IsEnumerable<T, void>
        : std::true_type
    {
    };
    

    void 型に実体化されなかった場合。

    template <typename T, typename = void>
    struct IsEnumerable
        : std::false_type
    {
    };
    

    テンプレート第二引数にはデフォルト値として void 型が指定されているため、IsEnumerable<T> は、IsEnumerable<T, void> と同義です。

    T に enumerate メンバ関数が存在する場合、特殊化条件に一致するため std::true_type を継承した方の型が実体化されます。逆に enumerate メンバ関数が存在しない場合、IsEnumerable<T, void> は実体化されず、std::false_type を継承した方の型が実体化されます。

    これで enumerate メンバ関数が存在するかどうかを判定できるようになりました。

使い方はこうです。

/// @brief enumerate() が存在する型
struct Enumerable
{
    double value;

    template <typename E>
    void enumerate(E& e)
    {
        e.enumeration(value);
    }
};

/// @brief enumerate() が存在しない型
struct UnEnumerable
{
    double value;
};

int main()
{
    constexpr bool a = IsEnumerable<Enumerable>::value;    // true
    constexpr bool b = IsEnumerable<UnEnumerable>::value;  // false
}

print 関数を改良し、enumerate 関数が存在する場合、呼び出すように変更します。

class Enumerator
{
public:
    template <typename... Args>
    void enumeration(Args&&... args)
    {
        unpack(std::forward<Args>(args)...);
    }

private:

    /// @brief 可変長引数展開
    template <typename Head, typename... Tails>
    void unpack(Head&& head, Tails&&... tails)
    {
        print(std::forward<Head>(head));  // 引数の頭だけ取り出し print へ渡す
        unpack(std::forward<Tails>(tails)...);
    }

    /// @brief 展開終端
    void unpack() {}

    /// @brief enumerate() が存在する場合 (再帰的に列挙)
    template <typename Enumerable, typename std::enable_if<IsEnumerable<Enumerable>::value>, std::nullptr_t>::type = nullptr>
    void print(Enumerable&& e)
    {
        e.enumerate(*this);
    }

    /// @brief enumerate() が存在しない場合 (標準出力へ出力)
    template <typename UnEnumerable, typename std::enable_if<not IsEnumerable<UnEnumerable>::value>, std::nullptr_t>::type = nullptr>
    void print(UnEnumerable&& ue)
    {
        std::cout << ue << std::endl;
    }
};

template <typename T>
void Print(T&& t)
{
    Enumerator e;
    e.enumeration(t);  // 再起呼び出しができるのでこう書ける。
}

これで再帰的に呼び出せるようになりました。しかし、enable_if を用いた関数のコンパイル時条件分岐では、条件が増えるとややこしくなってきます(多重定義にならないように気を配る必要があるため)。

そこで部分特殊化を用いて、先に全条件が成立しない場合の関数を定義し、条件を満たす場合のみ他の関数が実体化されるようにします。メンバ関数の部分特殊化はできないので、部分特殊化された構造体でラップします。

class Enumerator
{
public:
    template <typename... Args>
    void enumeration(Args&&... args)
    {
        unpack(std::forward<Args>(args)...);
    }

private:

    /// @brief 可変長引数展開
    template <typename Head, typename... Tails>
    void unpack(Head&& head, Tails&&... tails)
    {
        Printer::print(std::forward<Head>(head));  // 引数の頭だけ取り出し print へ渡す
        unpack(std::forward<Tails>(tails)...);
    }

    /// @brief 展開終端
    void unpack() {}

    /// @brief 全条件が成立しない場合 (標準出力へ出力)
    template <typename UnEnumerable, typename = void>
    struct Printer
    {
        static void print(UnEnumerable&& ue)
        {
            std::cout << std::forward(ue) << std::endl;
        };
    };

    /// @brief enumerate() が存在する場合
    template <typename Enumerable>
    struct Printer<Enumerable, typename std::enable_if<IsEnumerable<Enumerable>::value>::type>
    {
        static void print(Enumerable&& e)
        {
            e.enumerate(*this);  // 再帰呼び出し
        };
    };
};

template <typename T>
void Print(T&& t)
{
    Enumerator e;
    t.enumerate(e);
}

これで再帰的な列挙ができるようになりました。

型情報を取得する

print 関数を更に特殊化することで、double や int などの型情報を取得し、出力するように変更して完成です。ついでにインデントを付けて見やすくします。

また IsEnumerable<RemoveModifierT<Enumerable>>::value として修飾子を取り除いて判定するようにしています。例えば、const double& 型は、double 型と等しくないため特殊化条件に引っかかってくれません。これを回避するために、RemoveModifierT を使って修飾子を取り除いています。

#include <type_traits>
#include <iostream>

struct DummyEnumerator {};

template <typename T>
using VoidT = void;

/// @brief T に enumerate() が存在するかどうかを判定する
template <typename T, typename = void>
struct IsEnumerable
    : std::false_type
{
};

template <typename T>
struct IsEnumerable<T, VoidT<decltype(std::declval<T>().enumerate(std::declval<DummyEnumerator&>()))>>
    : std::true_type
{
};

template <typename T>
using RemoveModifierT = std::remove_cv_t<std::remove_reference_t<T>>;

class Enumerator
{
public:
    template <typename... Args>
    void enumeration(Args&&... args)
    {
        unpack(std::forward<Args>(args)...);
    }

private:

    int indent = 0;
    void print_indent() const
    {
        for (int i = 0; i < indent * 4; ++i)
        {
            std::cout << ' ';
        }
    }

    /// @brief 可変長引数展開
    template <typename Head, typename... Tails>
    void unpack(Head&& head, Tails&&... tails)
    {
        Printer<Head>::print(*this, std::forward<Head>(head));  // 引数の頭だけ取り出し print へ渡す
        unpack(std::forward<Tails>(tails)...);
    }

    /// @brief 展開終端
    void unpack() {}

    // エラーを出すための構造体 遅延static_assertというやつ
    template <typename T>
    struct AlwaysFalse : std::false_type {};

    /// @brief 全条件が成立しない場合 (コンパイル時エラー)
    template <typename Invalid, typename = void>
    struct Printer
    {
        static void print(...)
        {
            static_assert(AlwaysFalse<Invalid>::value, "Not supported type" __FUNCSIG__);
        };
    };

    /// @brief enumerate() が存在する場合
    template <typename Enumerable>
    struct Printer<Enumerable, typename std::enable_if<IsEnumerable<RemoveModifierT<Enumerable>>::value, void>::type>
    {
        static void print(Enumerator& self, Enumerable&& e)
        {
            self.print_indent();
            std::cout << "object: {" << std::endl;

            ++self.indent;  // インデントを増やす
            e.enumerate(self);  // 再帰呼び出し
            --self.indent;  // インデントを戻す

            self.print_indent();
            std::cout << "}" << std::endl;
        };
    };

    /// @brief double の場合
    template <typename Double>
    struct Printer<Double, typename std::enable_if<std::is_same<RemoveModifierT<Double>, double>::value, void>::type>
    {
        static void print(Enumerator& self, Double&& d)
        {
            self.print_indent();
            std::cout << "double: " << d << std::endl;
        };
    };

    /// @brief int の場合
    template <typename Int>
    struct Printer<Int, typename std::enable_if<std::is_same<RemoveModifierT<Int>, int>::value, void>::type>
    {
        static void print(Enumerator& self, Int&& i)
        {
            self.print_indent();
            std::cout << "int: " << i << std::endl;
        };
    };

    /// @brief bool の場合
    template <typename Bool>
    struct Printer<Bool, typename std::enable_if<std::is_same<RemoveModifierT<Bool>, bool>::value, void>::type>
    {
        static void print(Enumerator& self, Bool&& b)
        {
            self.print_indent();
            std::cout << "bool: " << b << std::endl;
        };
    };

    /// @brief 配列の場合
    template <typename Array>
    struct Printer<Array, typename std::enable_if<std::is_array<RemoveModifierT<Array>>::value, void>::type>
    {
        static void print(Enumerator& self, Array&& array)
        {
            self.print_indent();
            std::cout << "array: [" << std::endl;
            ++self.indent;

            for (auto&& element : array)
            {
                Printer<typename std::remove_extent<RemoveModifierT<Array>>::type>::print(
                    self, std::forward<typename std::remove_extent<RemoveModifierT<Array>>::type>(element)
                );
            }

            --self.indent;
            self.print_indent();
            std::cout << "]" << std::endl;
        };
    };
};

template <typename T>
void Print(T&& t)
{
    Enumerator e;
    e.enumeration(std::forward<T>(t));
}
struct Vec2
{
    double x;
    double y;

    template <typename E>
    void enumerate(E& e)
    {
        e.enumeration(x, y);
    }
};

struct Rect
{
    Vec2 pos;
    Vec2 size;

    template <typename E>
    void enumerate(E& e)
    {
        e.enumeration(pos, size);
    }
};

int main()
{
    Rect rect{ { 10, 20 }, { 30, 40 } };

    Print(rect);
}

出力は次のようになります。

object: {
    object: {
        double: 10
        double: 20
    }
    object: {
        double: 30
        double: 40
    }
}

おわりに

以上で、メンバ変数を列挙することができるようになりました。まさに C++ の面白い所を詰め込んだような感じです。

また、規則に従った構造体であればどこまでも列挙できるというのも、人知を超えられたような気分で面白いです。例えば次のような構造体を定義しても列挙できます。

ご覧いただき有難うございました。

struct Vector
{
    double values[3];

    template <typename E>
    void enumerate(E& e)
    {
        e.enumeration(values);
    }
};

struct Polygon
{
    Vector vertices[3];

    template <typename E>
    void enumerate(E& e)
    {
        e.enumeration(vertices);
    }
};

struct Shape
{
    Polygon polygons[4];

    template <typename E>
    void enumerate(E& e)
    {
        e.enumeration(polygons);
    }
};

int main()
{
    Shape shape{
        Polygon{ Vector{  1,  2,  3 }, Vector{  4,  5,  6 }, Vector{  7,  8,  9 } },
        Polygon{ Vector{ 10, 11, 12 }, Vector{ 13, 14, 15 }, Vector{ 16, 17, 18 } },
        Polygon{ Vector{ 19, 20, 21 }, Vector{ 22, 23, 24 }, Vector{ 25, 26, 27 } },
        Polygon{ Vector{ 28, 29, 30 }, Vector{ 31, 32, 33 }, Vector{ 34, 35, 36 } },
    };

    Print(shape);
}
object: {
    array: [
        object: {
            array: [
                object: {
                    array: [
                        double: 1
                        double: 2
                        double: 3
                    ]
                }
                object: {
                    array: [
                        double: 4
                        double: 5
                        double: 6
                    ]
                }
                object: {
                    array: [
                        double: 7
                        double: 8
                        double: 9
                    ]
                }
            ]
        }
        object: {
            array: [
                object: {
                    array: [
                        double: 10
                        double: 11
                        double: 12
                    ]
                }
                object: {
                    array: [
                        double: 13
                        double: 14
                        double: 15
                    ]
                }
                object: {
                    array: [
                        double: 16
                        double: 17
                        double: 18
                    ]
                }
            ]
        }
        object: {
            array: [
                object: {
                    array: [
                        double: 19
                        double: 20
                        double: 21
                    ]
                }
                object: {
                    array: [
                        double: 22
                        double: 23
                        double: 24
                    ]
                }
                object: {
                    array: [
                        double: 25
                        double: 26
                        double: 27
                    ]
                }
            ]
        }
        object: {
            array: [
                object: {
                    array: [
                        double: 28
                        double: 29
                        double: 30
                    ]
                }
                object: {
                    array: [
                        double: 31
                        double: 32
                        double: 33
                    ]
                }
                object: {
                    array: [
                        double: 34
                        double: 35
                        double: 36
                    ]
                }
            ]
        }
        object: {
            array: [
                object: {
                    array: [
                        double: 0
                        double: 0
                        double: 0
                    ]
                }
                object: {
                    array: [
                        double: 0
                        double: 0
                        double: 0
                    ]
                }
                object: {
                    array: [
                        double: 0
                        double: 0
                        double: 0
                    ]
                }
            ]
        }
    ]
}