技術 約8分で読めます

C++26の猫耳演算子 ^^ は静的リフレクションで型やメンバーを値として扱うための記号

いけさん目次

C++26に ^^ という演算子が入る。
見た目から「猫耳演算子」と呼ばれているらしい。

日本語圏の感覚だと、^^ はかなり顔文字に寄って見える。
コードの中に突然 ^^T と出てくると、コンパイラに煽られている感じがちょっとある。

でも、やっていることはふざけた名前よりかなり大きい。
^^ はC++26の静的リフレクションで、型、関数、変数、メンバーなどをコンパイル時の値に変換するための演算子だ。

P3381R0 では、もともと ^ だった案がObjective-C++のブロック拡張と衝突するため、^^e が候補に上がった。
同提案は、^^ を「新しい単一トークン」として扱う案を出している。
P2996R13 の履歴にも、P3381R0の採用に続いてリフレクション演算子が ^ から ^^ に変わったと書かれている。

^^T は型Tそのものではなくメタ情報の値を作る

C++は型をそのまま通常の値として渡せない。
たとえば f(int, float) と書いて「型 int と型 float を関数に渡す」ことはできない。

静的リフレクションでは、そこで ^^int^^std::string を書く。
^^ を付けると、その型や宣言を表す std::meta::info の値になる。

constexpr std::meta::info r = ^^int;

P2996R13の文言 では、^^ は単項演算子で、結果は std::meta::info の純右辺値とされている。
普通のランタイム値ではなく、コンパイル時に扱うための反射値だ。

この反射値を std::meta の関数に渡すと、列挙子、メンバー、型、名前、サイズ、アラインメントなどをコンパイル時に取り出せる。
たとえば提案文書内の説明では、enumerators_of(^^Color) は「Color の列挙子」を読む形になる。
reflectof(Color) のようなキーワード案より、対象の Color が見えやすいという判断だ。

このあたりは、JavaがV8やCPythonをそのまま組み込もうとしている Project Detroitの記事 と逆方向で面白い。
Project Detroitは別言語の実行系をプロセス内に持ち込む話だった。
C++26の ^^ は実行系ではなく、C++自身の構造をコンパイル時に値として扱う話になる。

[: :] で反射値からコード側へ戻る

^^ だけだと、型やメンバーを std::meta::info に変換するところまでしか行かない。
反射値から実際の型や名前として使うときは、スプライス構文 [: :] が出てくる。

P2996R13には、S::type を反射してから型として差し戻す例がある。

struct S { using type = int; };

void fn() {
  typename [:^^S::type:] *var;
}

ここで ^^S::typeS::type を表す反射値を作る。
[: ... :] はその反射値をソースコード側へ差し戻す。
typename が付くので、結果は int* の変数宣言として扱われる。

この組み合わせが入ると、テンプレートメタプログラミングでやっていた「型リストを回して、型に応じたコードを生成する」作業の一部が、標準の言語機能として書ける。
文字列でメンバー名を持っておいて実行時に探す、という方向ではない。
コンパイル時にC++の宣言を調べ、その結果を型や式として戻す。

^^[: :] を合わせると、流れは「コードからリフトして、調べて、コードへ戻す」往復になる。

flowchart TD
  A["コード上の型や宣言<br/>int / S::type / Color"] -->|"^^ でリフト"| B["std::meta::info<br/>コンパイル時の反射値"]
  B -->|"std::meta の関数で調べる"| C["名前・メンバー・列挙子<br/>などの情報"]
  C -->|"スプライスで戻す"| D["型や式としてコードに復帰"]

^^ が左から右への入口、[: :] が右から左への出口にあたる。
途中の std::meta の関数だけがコンパイル時に走り、実行時のコードに反射値そのものは残らない。

列挙型を文字列に変換する例

往復がそのまま役に立つのは、列挙型を文字列にする処理だ。
今までのC++だと、列挙子の名前は実行時には残らない。
switch を手書きするか、X-macroで列挙型の定義を二重に書くか、magic_enum のようにコンパイラの内部表現を当てにするライブラリを使うしかなかった。

C++26では、列挙型を反射して列挙子を回すだけで書ける。

#include <experimental/meta>
#include <string>

enum class Color { red, green, blue };

template <typename E>
  requires std::is_enum_v<E>
constexpr std::string enum_to_string(E value) {
  template for (constexpr auto e : std::meta::enumerators_of(^^E)) {
    if (value == [:e:]) {
      return std::string(std::meta::identifier_of(e));
    }
  }
  return "<unnamed>";
}

// enum_to_string(Color::green) は "green" になる

順番に見ると、^^E で列挙型 E を反射値にする。
enumerators_of がその列挙子を全部 std::meta::info の並びで返す。
template for はC++26で入る展開文で、ループ本体を列挙子の数だけコンパイル時に展開する。
[:e:] は列挙子の反射値 e を実際の値(Color::red など)に差し戻すスプライスで、value と比較できる。
一致したら identifier_of(e) で列挙子の名前を文字列として取り出す。

列挙型に列挙子を足しても、変換側のコードは何も触らずに追従する。
定義の二重管理も、外部のコードジェネレータもいらない。

std::meta で型から何を取り出せるか

enumerators_of 以外にも、構造体やクラスを調べる関数が std::meta に並ぶ。
反射値を渡すと、コンパイル時にメンバーや名前や型を返す。

関数返すもの
enumerators_of(r)列挙型の列挙子の一覧
members_of(r)型のメンバー全部(データと関数)
nonstatic_data_members_of(r)非静的データメンバーだけ
identifier_of(r)宣言の名前を std::string_view
type_of(r)その宣言の型を表す反射値
parent_of(r)一つ外側のスコープの反射値
is_public(r)アクセス指定が public かどうか

構造体のフィールドを順に舐めてJSONにする場合なら、nonstatic_data_members_of(^^T) で各フィールドの反射値を取り、identifier_of でキー名を、obj.[:member:] で値そのものを読む。
シリアライザやORM、設定ファイルのマッパーは、こうしたメンバー走査の上に作れる。

ただし関数名やシグネチャはP2996のリビジョン間でまだ動いている。
古い記事のコードがそのまま通らないことはわりとある。

何に使うのか

用途は、型の定義にある情報から機械的なコードを作る場面に集まる。
列挙型と構造体の例の他にも、コマンドライン引数のパース、フォーマッタ、ハッシュ計算、構造体同士の変換あたりが P2996R13 の例に並んでいる。

今までのC++でも、マクロ、テンプレート、外部コード生成、独自IDLで近いことはできた。
ただ、どれもC++の構文とは別の仕組みになる。
^^[: :] は、標準のC++構文の中で「宣言を調べる」「調べた結果を型や式に戻す」をやる。

もちろん、最初からアプリケーションコードのあちこちに ^^ が出るというより、ライブラリ作者が内側で使う機能になりやすい。
P3381R0も、リフレクション自体は初心者が頻繁に直接見る機能ではなく、ライブラリの内側で複雑な処理を受け持つものだと書いている。

今はまだ実験的な実装

C++26としての採用は決まったが、2026年時点で本家のGCCやClangにはまだ入っていない。
試すだけなら Compiler Explorer に実験的なClang/EDGのフォークがあり、^^[: :] をその場で動かせる。

手元の環境で動かすときは、ヘッダが現状 <experimental/meta> になる。
標準では <meta> になる予定だが、実験実装は接頭辞付きのまま出ていることが多い。
template for の展開文も別提案で入る機能なので、リフレクション単体ではなくC++26の他の新機能とセットで初めてまとまったコードになる。

変な名前の演算子はだいたい見た目から来る

^^ だけが変というより、プログラミング言語は昔から記号に変なあだ名を付けてきた。
実用上の意味は普通でも、見た目が強いと名前が先に残る。

名前言語記号だいたいの用途
猫耳演算子C++26^^型や宣言を std::meta::info にする
宇宙船演算子PHP、C++20など<=>三方向比較。小さい、等しい、大きいを1つの結果で返す
エルビス演算子Kotlinなど?:左辺がnullなら右辺を使う
セイウチ演算子Python:=式の中で代入し、その値を返す
セーフナビゲーション演算子Rubyなど&.レシーバがnilならメソッド呼び出しを飛ばす
スプラット演算子Rubyなど*配列を引数列に展開する

PHPの migration 7.0ページ<=> を宇宙船演算子として説明し、比較結果として -1、0、1 を返すとしている。
Kotlinの keywords and operators では、?: が左辺null時に右辺を返すエルビス演算子と書かれている。
Pythonの PEP 572NAME := expr の代入式を定義し、議論中にセイウチ演算子と呼ばれるようになったと説明している。

Rubyの calling methodsドキュメント には、&. がセーフナビゲーション演算子として出てくる。
同じページでは * をスプラット演算子として、配列を引数列に変換する記法も説明している。

こうして並べると、^^ はむしろおとなしい。
<=> が宇宙船で、:= がセイウチで、?: がエルビスなら、^^ が猫耳になるのは流れとしては自然だった。

顔文字に見えるのはたぶん日本語圏のほうが強い

英語圏でも ^^ は見た目の記号として読まれるが、日本語圏では昔のチャットや掲示板の顔文字としてかなり馴染みがある。
ありがとうございます^^ みたいな文脈で見てきた人には、C++の型名にくっついた ^^T が妙に煽りっぽく見える。

構文としては、^^ は「ちょっとかわいい新演算子」ではなく、C++の型や宣言をコンパイル時データとして取り出す入口だ。
ただ、std::meta::info r = ^^int; はどうしても int に笑いかけているように見える。