
猫耳演算子 ^^ で覗く C++26 リフレクション
はじめに
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
参考
- P2996R13 Reflection for C++26
- Wikipedia: C++26
- Bloomberg clang-p2996 (GitHub)
- vsavkov/clang-p2996 (Docker Hub)
- Compiler Explorer
- Learn Modern C++: Reflection in C++26 (P2996)
猫耳演算子 ^^ とは
^^ は、^^T のように、名前 (型名、関数名、変数名、名前空間など) の直前に置く単項前置演算子です。評価すると、その名前が指し示すエンティティに関するメタ情報を表す std::meta::info 型の値が返ります。リフレクションを行うための入口と捉えると分かりやすいかもしれません。
猫耳という愛称は字面どおりです。P2996 が C++26 入りした 2025 年のソフィア会合以降、海外コミュニティでもこの呼び方が広がっています。
何が嬉しいのか
別言語の経験者にとって、リフレクションという概念自体は珍しくないかもしれません。Java の Reflection API では、実行時に Class オブジェクトからフィールドやメソッドを列挙できます。Python なら dataclasses モジュールや __dict__ 属性で、構造体的なオブジェクトの内側を覗けます。
なお、C++ にも、これまでリフレクション風のことを行うテクニックは存在していました。X マクロ、BOOST_FUSION_ADAPT_STRUCT、boost::hana::struct_ などです。ただし、これらはいずれもユーザー側に、いわゆるおまじないを書かせる必要があるアプローチでした。構造体ごとにマクロ定義を並べたり、リフレクション対象であることを表明する記述を残したりという手間がついて回ります。
C++26 のリフレクションは、これらと比べると、何の準備もない素のままの struct から、コンパイル時にそのままメンバ名や型情報を取り出せる 点が大きく違います。しかも、取り出した情報を使ってコードを組み立てる splicer [: :] という構文もセットで提供されるため、たとえばシリアライザのように構造体に応じて挙動を変えたいコードを、宣言的に書けるようになります。
実際にやってみた: 汎用 print_struct
まずは「任意の struct のメンバ名と値を 1 行ずつ出力する」 print_struct を書いてみます。
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 の中身に Person も Book も登場していないにもかかわらず、メンバ名と値が正しく出力されています。リフレクションに馴染みのある別言語ユーザーには既視感があるかもしれませんが、これがマクロもおまじないもない素の struct からそのまま動くのが C++26 の新しいところです。
サンプル全文 (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 風の文字列に書き出す」関数も、同じ仕組みで作れます。
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)
#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)
#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 リフレクションに触れるきっかけの参考になれば幸いです。







