猫耳演算子 ^^ で覗く C++26 リフレクション

猫耳演算子 ^^ で覗く C++26 リフレクション

C++26 で追加された ^^ 演算子 (cat-ears operator) は、リフレクションへの入口です。何の準備もない素の struct から、メンバ名や型情報をコンパイル時に取り出し、print_struct や to_json のような汎用関数を宣言的に書けるようになります。
2026.06.17

はじめに

2025 年 6 月、ブルガリアのソフィアで開催された WG21 会合で、C++26 への取り込みが採択された提案文書のひとつに P2996 (Reflection for C++26) があります。これにより、C++ にも、言語コア機能としてのリフレクションが正式に追加される見通しです。

P2996 の入口となるのが、本記事の主役、^^ という新しい演算子です。ふたつ並んだ caret が、ちょこんと突き出た猫の耳に見えることから、委員会では cat-ears operator という通称で呼ばれています。日本語では猫耳演算子と訳されることが多いようです。

本記事は、普段 C++ を書かないエンジニアにも、この新機能の何が嬉しいのか、手短に把握してもらうことを目指して書きました。深い文法の話には踏み込まず、ユースケースから探っていきます。

対象読者

  • 普段は C++ を書かないが、C 系の構文には慣れているエンジニア
  • 別言語 (Python、Java、Go、TypeScript など) でリフレクションに触れた経験があり、C++ での書き味が気になっている方
  • C++26 の新機能を雰囲気だけでも掴んでおきたい方

検証環境

  • OS: macOS (darwin 25.5.0、Apple Silicon)
  • 実行環境: Docker Desktop 29.4.0 + イメージ vsavkov/clang-p2996:arm64
  • コンパイラ: Clang 21.0.0git (Bloomberg clang-p2996 フォーク)
  • 主要なビルドオプション: -std=c++26 -freflection-latest -stdlib=libc++
  • 参照した P2996 リビジョン: P2996R13
  • 検証日: 2026-06-17

参考

猫耳演算子 ^^ とは

^^ は、^^T のように、名前 (型名、関数名、変数名、名前空間など) の直前に置く単項前置演算子です。評価すると、その名前が指し示すエンティティに関するメタ情報を表す std::meta::info 型の値が返ります。リフレクションを行うための入口と捉えると分かりやすいかもしれません。

猫耳という愛称は字面どおりです。P2996 が C++26 入りした 2025 年のソフィア会合以降、海外コミュニティでもこの呼び方が広がっています。

https://x.com/CPPAlliance/status/2066492940046733732

何が嬉しいのか

別言語の経験者にとって、リフレクションという概念自体は珍しくないかもしれません。Java の Reflection API では、実行時に Class オブジェクトからフィールドやメソッドを列挙できます。Python なら dataclasses モジュールや __dict__ 属性で、構造体的なオブジェクトの内側を覗けます。

なお、C++ にも、これまでリフレクション風のことを行うテクニックは存在していました。X マクロ、BOOST_FUSION_ADAPT_STRUCTboost::hana::struct_ などです。ただし、これらはいずれもユーザー側に、いわゆるおまじないを書かせる必要があるアプローチでした。構造体ごとにマクロ定義を並べたり、リフレクション対象であることを表明する記述を残したりという手間がついて回ります。

C++26 のリフレクションは、これらと比べると、何の準備もない素のままの struct から、コンパイル時にそのままメンバ名や型情報を取り出せる 点が大きく違います。しかも、取り出した情報を使ってコードを組み立てる splicer [: :] という構文もセットで提供されるため、たとえばシリアライザのように構造体に応じて挙動を変えたいコードを、宣言的に書けるようになります。

実際にやってみた: 汎用 print_struct

まずは「任意の struct のメンバ名と値を 1 行ずつ出力する」 print_struct を書いてみます。

U1-print-struct.cpp
template <class T>
void print_struct(const T& obj) {
    template for (constexpr auto m :
                  std::define_static_array(
                      std::meta::nonstatic_data_members_of(
                          ^^T, std::meta::access_context::current()))) {
        std::cout << std::meta::identifier_of(m)
                  << ": " << obj.[:m:] << '\n';
    }
}
  • ^^T で、テンプレート引数 T の型情報を std::meta::info として取り出します。
  • std::meta::nonstatic_data_members_of に渡すと、T が持つ非 static データメンバの info 列が返ります。第 2 引数の access_context::current() は、アクセス制御の文脈は今の場所、を意味する P2996R10 以降に追加された引数です。
  • 戻り値は内部で動的確保された vector 系で、そのままでは template for には渡せません。std::define_static_array で静的配列にブリッジするのが、執筆時点の定石です。
  • template for (P1306 expansion statements) は、コンパイル時に与えられた要素列を 1 つずつ展開する制御構文です。ループ変数 m には 1 メンバずつの info が入ります。
  • 本体では、std::meta::identifier_of(m) でメンバ名を、obj.[:m:] でそのメンバの値を取り出しています。[:m:] が splicer で、info から「式」 (ここではメンバ参照) を組み立てる役割を担います。

呼び出してみると、違う型でも同じ呼び方が通ります。

struct Person { std::string name; int age; };
struct Book   { std::string title; int pages; double price; };

Person p{"Alice", 30};
print_struct(p);
std::cout << "---\n";
Book b{"C++ Reflection", 240, 39.95};
print_struct(b);

実行結果は次のとおりです。

name: Alice
age: 30
---
title: C++ Reflection
pages: 240
price: 39.95

print_struct の中身に PersonBook も登場していないにもかかわらず、メンバ名と値が正しく出力されています。リフレクションに馴染みのある別言語ユーザーには既視感があるかもしれませんが、これがマクロもおまじないもない素の struct からそのまま動くのが C++26 の新しいところです。

サンプル全文 (U1-print-struct.cpp)
U1-print-struct.cpp
#include <experimental/meta>
#include <iostream>
#include <string>

template <class T>
void print_struct(const T& obj) {
    template for (constexpr auto m :
                  std::define_static_array(
                      std::meta::nonstatic_data_members_of(^^T, std::meta::access_context::current()))) {
        std::cout << std::meta::identifier_of(m) << ": " << obj.[:m:] << '\n';
    }
}

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

struct Book {
    std::string title;
    int pages;
    double price;
};

int main() {
    Person p{"Alice", 30};
    print_struct(p);
    std::cout << "---\n";
    Book b{"C++ Reflection", 240, 39.95};
    print_struct(b);
    return 0;
}

発展させてみた: to_json

print_struct の発想を発展させると、「任意の struct を JSON 風の文字列に書き出す」関数も、同じ仕組みで作れます。

U3-to-json.cpp
template <class T>
std::string to_json(const T& obj) {
    std::ostringstream os;
    os << '{';
    bool first = true;
    template for (constexpr auto m :
                  std::define_static_array(
                      std::meta::nonstatic_data_members_of(
                          ^^T, std::meta::access_context::current()))) {
        if (!first) os << ", ";
        first = false;
        os << '"' << std::meta::identifier_of(m) << "\": ";
        using MT = typename [: std::meta::type_of(m) :];
        if constexpr (std::is_same_v<MT, std::string>) {
            os << '"' << obj.[:m:] << '"';
        } else {
            os << obj.[:m:];
        }
    }
    os << '}';
    return os.str();
}

using MT = typename [: std::meta::type_of(m) :]; の行が、print_struct から増えた要です。メンバの info から型情報を取り出し、splicer 経由でローカル型エイリアス MT として実体化しています。こうしておけば、あとは通常の if constexpr で型ごとに分岐できます。上のコードでは、std::string だけクオートで囲むようにしました。

Person p{"Alice", 30};
std::cout << to_json(p) << '\n';
// {"name": "Alice", "age": 30}

シリアライザを書こうとすると、これまでは構造体ごとに専用のコードを書くか、マクロで登録するか、のどちらかが必要でした。リフレクションがあれば、構造体側に何の準備もないまま、シリアライザ側の数十行で済んでしまいます。to_json を起点に、to_yaml、ORM のスキーマ生成、テストデータの自動生成と発想を広げていけます。

サンプル全文 (U3-to-json.cpp)
U3-to-json.cpp
#include <experimental/meta>
#include <iostream>
#include <sstream>
#include <string>
#include <type_traits>

template <class T>
std::string to_json(const T& obj) {
    std::ostringstream os;
    os << '{';
    bool first = true;
    template for (constexpr auto m :
                  std::define_static_array(
                      std::meta::nonstatic_data_members_of(^^T, std::meta::access_context::current()))) {
        if (!first) os << ", ";
        first = false;
        os << '"' << std::meta::identifier_of(m) << "\": ";
        using MT = typename [: std::meta::type_of(m) :];
        if constexpr (std::is_same_v<MT, std::string>) {
            os << '"' << obj.[:m:] << '"';
        } else {
            os << obj.[:m:];
        }
    }
    os << '}';
    return os.str();
}

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

int main() {
    Person p{"Alice", 30};
    std::cout << to_json(p) << '\n';
    return 0;
}

活用するアイデア

^^ と splicer を組み合わせると、ほかにもさまざまな書き方が見えてきます。検証した中から印象的だったものを 4 つ紹介します。

  • enum to string
    これまでは X マクロが定番でしたが、std::meta::enumerators_of で enum 値の info を列挙し、splicer で実際の値と比較すれば、数十行のテンプレート関数として書けます。
  • 任意の struct の汎用 equal
    print_struct の出力部分を if (a.[:m:] != b.[:m:]) return false; に置き換えるだけで、メンバ全比較の関数になります。
  • annotation 経由のメタデータ付与
    メンバに [[=ColumnIndex{0}]] のような注釈を付け、std::meta::annotations_of で取り出します。ORM のカラム指定や CLI 引数のヘルプ文字列を、構造体に直接書き込めます。
  • 関数パラメータの列挙
    P3096 の std::meta::parameters_of で、関数 info から引数 1 つずつの名前と型を取り出せます。簡易なルーター生成などに使えそうです。
enum to string のサンプル全文 (U2-enum-to-string.cpp)
U2-enum-to-string.cpp
#include <experimental/meta>
#include <iostream>
#include <string_view>

template <class E>
constexpr std::string_view name_of_enum(E v) {
    template for (constexpr auto e :
                  std::define_static_array(std::meta::enumerators_of(^^E))) {
        if (v == [:e:]) return std::meta::identifier_of(e);
    }
    return "<unknown>";
}

enum class Status { Pending = 1, Active = 2, Closed = 3 };

int main() {
    std::cout << name_of_enum(Status::Pending) << '\n';
    std::cout << name_of_enum(Status::Active) << '\n';
    std::cout << name_of_enum(Status::Closed) << '\n';
    std::cout << name_of_enum(static_cast<Status>(99)) << '\n';
    return 0;
}

まとめ

C++26 では、猫耳演算子 ^^ と splicer の組み合わせで、構造体側に何の準備もないままメンバ名や型情報を取り出して扱えるようになります。実装はまだ実験段階ですが、Compiler Explorer の clang-p2996 を開けば、ブラウザだけで手触りを確かめられます。本記事が、C++26 リフレクションに触れるきっかけの参考になれば幸いです。

この記事をシェアする

関連記事