【C++】C++26 Static Reflection完全ガイド:コンパイル時リフレクションでコードを自動生成する

目次

結論

C++26で導入される静的Reflection(P2996)を使うと、構造体のメンバ一覧をコンパイル時に取得して、シリアライズ処理を自動生成できます。まずは一番シンプルな例を見てみましょう。

#include <meta>
#include <print>

struct Point
{
    int   x;
    int   y;
    float z;
};

int main()
{
    // 全てのメンバ情報をコンパイル時に取得
    constexpr auto members = std::meta::nonstatic_data_members_of(^^Point);

    std::println("Point has {} members", members.size());

    // template for でメンバ名を列挙
    template for (constexpr auto m : members)
    {
        std::println("  - {}", std::meta::identifier_of(m));
    }
}

これをGCC 16以降で -std=c++26 -freflection フラグ付きでコンパイルすると、こんな出力が得られます。

Point has 3 members
  - x
  - y
  - z

静的Reflectionの3大原則:
①コンパイル時のみ動作する
②ランタイムオーバーヘッドはゼロ
③型安全。
マクロやSFINAEを使った天才的なテンプレートメタプログラミングの時代は、そろそろ終わりです。

C++26 静的Reflectionとは何か

Reflectionとは、プログラムが自分自身の構造(型・メンバ・関数など)を検査したり操作したりする能力のことです。C++26以前は、こんな方法で型情報にアクセスしていました。

  • X-Macroパターン(メンバ一覧をマクロで二重管理する地獄)
  • SFINAEと非常に複雑なテンプレートメタプログラミング
  • 外部コードジェネレータ(QtのMOCなど)

C++26の静的Reflection(P2996)では、^^ 演算子で型や変数やメンバのリフレクション情報を取得し、[: :](スプライサ)でリフレクション情報をコードに戻せます。「コードを書くコード」が、ついに標準化されたわけです。

基本構文:^^ 演算子と [: :] スプライサ

^^ 演算子:コードをリフレクション情報に変換する

^^ を型や変数やメンバの前に置くと、std::meta::info という型の値が手に入ります。

#include <meta>

struct Person
{
    std::string name;
    int         age;
};

// 型のリフレクション
constexpr auto person_info = ^^Person;

// メンバのリフレクション
constexpr auto name_info = ^^Person::name;
constexpr auto age_info  = ^^Person::age;

// 式のリフレクション
int global_var = 42;
constexpr auto var_info = ^^global_var;

[: :] スプライサ:リフレクション情報をコードに戻す

リフレクション情報から元のコードを復元するには、[: :] を使います。

// 型情報をコードに戻す
using MyType = [:^^Person:];  // Person と同じ意味

// メンバアクセスに使う
Person p{"Alice", 30};
int val = p.[:^^Person::age:];  // p.age と同じ(30になる)

// 条件付きで型を選ぶこともできる
using Bigger = [: sizeof(long) > sizeof(int) ? ^^long : ^^int :];

標準クエリ関数一覧

<meta> ヘッダには便利なクエリ関数がたくさん用意されています。

関数 戻り値 説明
identifier_of(info) string_view 名前を取得する
type_of(info) info メンバの型を取得する
nonstatic_data_members_of(info) range<info> 全データメンバを取得する
member_functions_of(info) range<info> 全メンバ関数を取得する
enumerators_of(info) range<info> 列挙子一覧を取得する
parameters_of(info) range<info> 関数のパラメータ一覧を取得する
base_classes_of(info) range<info> 基底クラス一覧を取得する

実践1:JSONシリアライザを自動生成する

「全ての構造体に to_json() を書く」という退屈な作業、もうやめましょう。Reflectionを使えばたったこれだけです。

template<typename T>
std::string to_json(const T& obj)
{
    std::ostringstream oss;
    oss << "{";

    bool first = true;

    template for (constexpr auto member : std::meta::nonstatic_data_members_of(^^T))
    {
        if (!first)
        {
            oss << ", ";
        }
        first = false;

        // キー:メンバ名
        oss << "\"" << std::meta::identifier_of(member) << "\": ";

        // 値:型に応じて出力を変える
        using MemberType = decltype(obj.[:member:]);

        if constexpr (std::is_same_v<MemberType, std::string>)
        {
            oss << "\"" << obj.[:member:] << "\"";
        }
        else
        {
            oss << obj.[:member:];
        }
    }

    oss << "}";
    return oss.str();
}

// 実際に使ってみる
struct Config
{
    std::string host;
    int         port;
    bool        use_tls;
};

int main()
{
    Config cfg{"localhost", 8080, true};

    std::println("{}", to_json(cfg));
    // 出力: {"host": "localhost", "port": 8080, "use_tls": 1}
}

template for はC++26のexpansion statement(P1306)という機能で、コンパイル時にループを展開します。従来の std::apply と比べると、はるかに読みやすいですね。

実践2:SQLクエリを自動生成する(簡易ORM)

template<typename T>
std::string generate_insert(const T& obj)
{
    std::string table  = std::meta::identifier_of(^^T);
    std::string cols   = "";
    std::string values = "";

    bool first = true;

    template for (constexpr auto m : std::meta::nonstatic_data_members_of(^^T))
    {
        if (!first)
        {
            cols   += ", ";
            values += ", ";
        }
        first = false;

        cols   += std::meta::identifier_of(m);
        values += std::format("'{}'", obj.[:m:]);
    }

    return std::format("INSERT INTO {} ({}) VALUES ({})", table, cols, values);
}

struct Product
{
    int         id;
    std::string name;
    double      price;
};

int main()
{
    Product p{1, "Laptop", 999.99};

    std::println("{}", generate_insert(p));
    // INSERT INTO Product (id, name, price) VALUES ('1', 'Laptop', '999.99')
}

メンバを1つ追加しても、insert文を生成するコードは一切修正する必要がありません。これがReflectionの本当の価値です。

実践3:Enum→文字列変換(ゼロオーバーヘッド)

enumの値から文字列への変換、これまでマクロか手動マッピングが必要でしたが、C++26ならこんなにシンプルです。

template<typename T>
    requires std::is_enum_v<T>
consteval auto make_enum_table() noexcept
{
    constexpr auto enumerators = std::meta::enumerators_of(^^T);
    std::array<std::pair<T, std::string_view>, enumerators.size()> table{};

    std::size_t i = 0;

    template for (constexpr auto item : enumerators)
    {
        table[i] = { [:item:], std::meta::identifier_of(item) };
        i++;
    }

    return table;
}

template<typename T>
    requires std::is_enum_v<T>
constexpr std::string_view enum_to_string(T value) noexcept
{
    constexpr auto table = make_enum_table<T>();

    template for (constexpr auto [v, name] : table)
    {
        if (v == value)
        {
            return name;
        }
    }

    return "unknown";
}

enum class Color { Red, Green, Blue };

int main()
{
    auto c = Color::Green;

    std::println("{}", enum_to_string(c));  // Green
}

実行時のメモリ確保もテーブル生成コストもゼロです。全てコンパイル時に確定し、バイナリの .rodata セクションに配置されます。

契約プログラミング(Contracts)

C++26では契約プログラミング(P2900)も導入されます。関数の事前条件や事後条件を言語レベルで書けるようになります。

int divide(int dividend, int divisor)
    [[pre: divisor != 0]]
    [[post r: r * divisor == dividend]]
{
    return dividend / divisor;
}

void set_volume(int level)
    [[pre: 0 <= level && level <= 100]]
    [[post: get_volume() == level]]
{
    volume_ = level;
}

契約はビルドモードで制御できます。プロダクションではチェックをオフにしてオーバーヘッドをゼロにし、テストやデバッグビルドで厳格にチェックする、といった使い方が可能です。

コンパイラの対応状況

コンパイラ Reflection対応 コンパイルフラグ
GCC 16+ 実験的対応(P2996の大部分) -std=c++26 -freflection
Clang 19+ 部分的対応 -std=c++26 -freflection
MSVC 開発中 未定

GCC 16が一番進んでいます。すぐに試したい方は Compiler Explorer で GCC 16 を選んで -std=c++26 -freflection を指定すれば、ブラウザからそのまま実行できます。

【広告】ドメインを取得してC++プロジェクトを公開するならお名前.com。格安で独自ドメインを取得しましょう。

まとめ

C++26の静的Reflectionは、C++11以来最大の転換点だと言っても過言ではありません。「テンプレートメタプログラミングで天才的なコードを書く」時代から、「コンパイラに型情報を直接問い合わせてコードを生成する」時代へのパラダイムシフトです。今後10年かけて、C++のエコシステムを大きく変えていくでしょう。

  • ^^ 演算子と [: :] スプライサが基本構文です
  • 自動シリアライズ、ORM、enum変換が数行で書けます
  • オーバーヘッドゼロ、型安全、コンパイル時検査
  • Contractsと組み合わせれば、より堅牢なコードベースに
  • GCC 16 + -freflection で今すぐ試せます

【広告】このサイトはConoHa WINGで運営しています。安定した高速サーバーで快適にブログを書けています。いつもありがとう!

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

CAPTCHA


目次