最終更新日時(UTC):
が更新

履歴 編集

静的リフレクション [P2996R13](C++26)

このページはC++26に採用される見込みの言語機能の変更を解説しています。

のちのC++規格でさらに変更される場合があるため関連項目を参照してください。

概要

C++26では、コンパイル時にプログラムの構造を検査・操作できる「静的リフレクション (static reflection)」機能を導入する。

この機能は以下の要素で構成される:

  • リフレクション演算子^^ : 型、名前空間、変数、関数、メンバなどのプログラム要素から、その情報を表すコンパイル時の値(リフレクション)を生成する
  • スプライス演算子[: :] : リフレクションをプログラム要素(型、式、テンプレート引数など)に変換して挿入する
  • std::meta::info : リフレクションを表すスカラ型。構造的型 (structural type) であり、定数テンプレートパラメータとして使用できる
  • <meta>ヘッダ : リフレクションを操作するためのメタ関数群を提供する新しいヘッダ
  • constevalブロック : consteval { ... }によるコンパイル時副作用の実行
  • アノテーション : 宣言にコンパイル時定数を付加し、リフレクションで取得できる機構
  • 関数パラメータのリフレクション : 関数の仮引数の型、名前、デフォルト引数の有無などをリフレクションで取得できる

#include <meta>
#include <print>

enum class Color { red, green, blue };

// 列挙値を文字列に変換する汎用関数
template <typename E>
  requires std::is_enum_v<E>
constexpr std::string_view to_string(E value) {
  // 型Eから列挙子のリストを取得
  template for (constexpr auto e : std::define_static_array(std::meta::enumerators_of(^^E))) {
    if (value == [:e:]) {
      return std::meta::identifier_of(e); // 列挙子の名前を文字列として取得
    }
  }
  return "<unknown>";
}

int main() {
  std::println("{}", to_string(Color::red));    // "red"
  std::println("{}", to_string(Color::green));  // "green"
  std::println("{}", to_string(Color::blue));   // "blue"
}

リフレクション演算子^^

^^は前置の単項演算子であり、プログラム要素からstd::meta::info型の値を生成する。

constexpr auto r1 = ^^int;           // 型のリフレクション
constexpr auto r2 = ^^std;           // 名前空間のリフレクション
constexpr auto r3 = ^^::;            // グローバル名前空間のリフレクション
constexpr auto r4 = ^^std::vector;   // テンプレートのリフレクション

リフレクションできる対象と、取得できる主な情報:

対象 構文例 取得できる情報
^^int, ^^std::string 名前、サイズ、アライメント、型特性、メンバ一覧、基底クラス一覧
名前空間 ^^std, ^^:: 名前、メンバ一覧
変数 ^^x 名前、型、記憶域期間、リンケージ
関数 ^^f 名前、戻り値型、パラメータ一覧、noexceptの有無
メンバ変数 ^^S::m 名前、型、オフセット、アクセス指定子、ビットフィールドの有無
列挙子 ^^Color::red 名前、値、所属する列挙型
テンプレート ^^std::vector 名前、テンプレート引数の置換
基底クラス関係 bases_of()経由 基底クラスの型、アクセス指定子、virtualの有無
関数パラメータ parameters_of()経由 名前、型、デフォルト引数の有無、明示的オブジェクトパラメータか
アノテーション annotations_of()経由 型、値(スプライスで取得)、ソース位置

オーバーロードされた関数のリフレクション

^^fで関数fをリフレクションする場合、fオーバーロードされていると不適格となる。^^オーバーロード集合ではなく単一の関数を対象とする。

void f(int);
void f(double);

// constexpr auto r = ^^f;  // エラー: fはオーバーロードされている

オーバーロードされた関数がクラスのメンバ関数である場合、個々のオーバーロードを取得するにはmembers_of()を使用する。members_of()は各オーバーロードを個別のリフレクションとして返す。

struct S {
  void f(int);
  void f(double);
};

consteval {
  // members_of()で個々のオーバーロードを取得
  auto members = std::meta::members_of(
      ^^S, std::meta::access_context::unchecked());
  // フィルタリングで特定のオーバーロードを選択できる
}

非メンバ関数(名前空間スコープの関数)の場合、オーバーロード集合そのものをリフレクションとして取得する手段は存在しない。そのため、非メンバ関数でオーバーロード集合から特定のオーバーロードを扱いたい場合は、関数ポインタ型にキャストして型を明示するなどの手段が必要となる。汎用的に関数のオーバーロードをリフレクションで扱いたい場合は、クラスのメンバ関数として定義することが必要になる。

また、スプライスで関数のリフレクションを式に変換する場合、オーバーロード解決は行われず、そのリフレクションが表す特定の関数が直接使用される。

スプライス演算子[: :]

スプライス演算子は、std::meta::info型の値(リフレクション)を受け取り、それが表すプログラム要素(型、式、テンプレート、名前空間)に変換して挿入する。[::]の間にはstd::meta::info型に評価される式を記述する。

// 型スプライス:リフレクションが表す型を挿入する
constexpr std::meta::info r = ^^int;
typename[:r:] x = 42; // int x = 42; と等価

// 式スプライス:リフレクションが表す変数や関数を式として挿入する
int value = 10;
constexpr std::meta::info rv = ^^value;
[:rv:] = 20; // value = 20; と等価

リフレクションが表す要素の種類に応じて、スプライスの構文が異なる:

種類 構文 用途
型スプライス typename[:r:] リフレクションrが表す型を挿入。型のみの文脈ではtypenameを省略可能
式スプライス [:r:] リフレクションrが表す変数・関数・列挙子などを式として挿入
テンプレートスプライス template[:r:] リフレクションrが表すテンプレートを挿入
名前空間スプライス namespace[:r:] リフレクションrが表す名前空間を挿入

メンバアクセスにもスプライスを使用できる。obj.[:r:]の形式で、リフレクションrが表すメンバ変数や基底クラスにアクセスする:

struct S { int x; int y; };

consteval void example() {
  S s{1, 2};

  static constexpr auto members = std::define_static_array(
    std::meta::nonstatic_data_members_of(
      ^^S, std::meta::access_context::unchecked()));

  int a = s.[:members[0]:];  // s.x と等価。a == 1
  int b = s.[:members[1]:];  // s.y と等価。b == 2
}

基底クラスのサブオブジェクトへのスプライス

基底クラスのリフレクションに対してもメンバアクセスのスプライスを使用できる。これにより、メンバ変数と基底クラスを統一的に扱うことができる。

struct Base { int b; };
struct Derived : Base { int d; };

consteval void example() {
  Derived obj{{42}, 100};
  static constexpr auto bases = std::define_static_array(
    std::meta::bases_of(
      ^^Derived, std::meta::access_context::unchecked()));
  Base& base_ref = obj.[:bases[0]:];  // 基底クラスのサブオブジェクトへの参照
}

std::meta::info

std::meta::info型は、リフレクションを表す基本的な型である。

  • <meta>ヘッダでusing info = decltype(^^::);として定義される。^^::はグローバル名前空間のリフレクションを生成する式であり、decltypeでその型を取得している
  • スカラ型であり、クラス型ではない
  • ==!=をサポートするが、順序比較(<, >, <=>)はサポートしない
  • 構造的型 (structural type) であり、定数テンプレートパラメータとして使用できる
  • consteval-only型であり、実行時には存在できない

// 定数テンプレートパラメータとして使用する例
template <std::meta::info R>
using type_of_reflection = typename[:R:];

type_of_reflection<^^int> x = 42;  // int x = 42;

2つのリフレクションは、同じエンティティを反映している場合に等値となる。型の別名は、元の型とは区別される:

using MyInt = int;
static_assert(^^int != ^^MyInt);                       // 型の別名は区別される
static_assert(^^int == std::meta::dealias(^^MyInt));   // dealias()で元の型を取得

<meta>ヘッダのメタ関数

<meta>ヘッダは、リフレクションを操作するための多数のconstevalメタ関数を提供する。名前の取得、エンティティの分類・検査、メンバや基底クラスの列挙、型特性の判定と型変換、テンプレート操作、レイアウト情報の取得、集成体型の動的定義など、広範な機能を備える。

詳細は<meta>ヘッダのリファレンスを参照。

constevalブロック

consteval { ... }構文により、コンパイル時に副作用のあるコードを実行できる。std::meta::define_aggregate()と組み合わせて型を動的に生成する際に使用する。

struct S;

consteval {
  // Sを2つのintメンバを持つ集成体として定義
  std::meta::define_aggregate(^^S, {
    std::meta::data_member_spec(^^int, {.name = "x"}),
    std::meta::data_member_spec(^^int, {.name = "y"})
  });
}

S s{1, 2};  // s.x == 1, s.y == 2

アノテーション

宣言にコンパイル時定数値を付加し、リフレクションで取得できる機構である。属性とは異なり、ユーザー定義の意味的データを運ぶ。

構文

[[=定数式]]の構文でアノテーションを付加する。=接頭辞により通常の属性と区別される。

struct [[=1]] Annotated {
  [[=42, =24]] int value;
};

アノテーションの式は構造的型でなければならない。アノテーションの取得にはannotations_of()annotations_of_with_type()メタ関数を使用する。

struct Name { const char* value; };

struct [[=Name{std::define_static_string("点")}]] Point {
  [[=Name{std::define_static_string("x座標")}]] int x;
  [[=Name{std::define_static_string("y座標")}]] int y;
};

// メンバのアノテーションを取得
template for (constexpr auto m :
    std::define_static_array(std::meta::nonstatic_data_members_of(^^Point,
        std::meta::access_context::unchecked()))) {
  static constexpr auto annots = std::define_static_array(
    std::meta::annotations_of_with_type(m, ^^Name));
  if constexpr (annots.size() > 0) {
    // アノテーションは値のリフレクションではないため、
    // constant_of()で値を取り出してからスプライスする
    std::println("{}: {}", [:std::meta::constant_of(annots[0]):].value,
                           std::meta::identifier_of(m));
  }
}
// 出力:
// x座標: x
// y座標: y

define_static_string / define_static_array / define_static_object

これらは、コンパイル時に計算した値を静的ストレージに配置し、実行時に使用可能にするための関数群である。これらは<meta>ヘッダで提供されるが、std名前空間に定義される(std::meta名前空間ではない)。

関数 説明
std::define_static_string() コンパイル時文字列を静的ストレージに配置し、ヌル終端const CharT*を返す
std::define_static_array() コンパイル時配列を静的ストレージに配置し、std::span<const T>を返す
std::define_static_object() コンパイル時オブジェクトを静的ストレージに配置し、const T*を返す

主な用途

これらの関数は、以下のようなリフレクションの典型的なユースケースで頻繁に使用される。

(1) template for文のrangeを静的配列に変換する

members_of()enumerators_of()などのメタ関数はstd::vector<std::meta::info>を返すが、std::vectorは動的メモリ確保を伴うためtemplate forのrangeとして直接使用できない。std::define_static_array()で静的ストレージに配置したstd::spanに変換することで、template for文で走査できるようになる。

template for (constexpr auto m :
    std::define_static_array(std::meta::nonstatic_data_members_of(^^S,
        std::meta::access_context::unchecked()))) {
  // mを使った処理
}

(2) コンパイル時文字列を実行時に返す

コンパイル時に構築したstd::stringstd::string_viewは、そのままでは実行時にアクセスできない(一時オブジェクトの寿命が尽きる、またはconsteval型の制約)。std::define_static_string()で静的ストレージに配置することで、ヌル終端const char*として実行時まで持ち越せる。

ただし、identifier_of()のように既にstring_viewを返す関数は、そのまま戻り値として返せる場合もある(静的ストレージに存在する識別子の文字列を指すため)。

(3) アノテーションの文字列リテラル

アノテーションの式は構造的型でなければならず、文字列リテラルを直接メンバとして持つと寿命の問題が生じる可能性がある。std::define_static_string()で静的ストレージに配置することで、安全にconst char*メンバに格納できる。

struct Name { const char* value; };
struct [[=Name{std::define_static_string("ラベル")}]] Tagged {};

(4) コンパイル時に構築したオブジェクトを実行時に参照する

コンパイル時に構築した任意のオブジェクトを静的ストレージに配置し、実行時にポインタで参照するにはstd::define_static_object()を使用する。

struct Config { int width; int height; };

// コンパイル時に構築したConfigオブジェクトを静的ストレージに配置
constexpr const Config* config = std::define_static_object(Config{1920, 1080});

リフレクションのエラー処理

リフレクションのメタ関数は、不正な入力に対してstd::meta::exceptionを送出する。定数評価中の例外処理として動作するため、実行時のオーバーヘッドはない。

構造体のメンバを列挙する

#include <meta>
#include <print>
#include <string>

struct Point {
  int x;
  int y;
  std::string name;
};

int main() {
  Point p{10, 20, "origin"};

  template for (constexpr auto m :
      std::define_static_array(std::meta::nonstatic_data_members_of(^^Point,
          std::meta::access_context::unchecked()))) {
    std::println("{}: {}", std::meta::identifier_of(m), p.[:m:]);
  }
}

出力

x: 10
y: 20
name: origin

列挙値を文字列に変換する

#include <meta>
#include <print>
#include <string_view>

enum class Color { red, green, blue };

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

int main() {
  std::println("{}", to_string(Color::red));
  std::println("{}", to_string(Color::green));
  std::println("{}", to_string(Color::blue));
}

出力

red
green
blue

関数パラメータの名前と型を列挙する

#include <meta>
#include <print>

void process(int id, double value, const char* name) {}

int main() {
  template for (constexpr auto p : std::define_static_array(std::meta::parameters_of(^^process))) {
    std::println("パラメータ: {} (型: {})",
      std::meta::identifier_of(p),
      std::meta::display_string_of(std::meta::type_of(p)));
  }
}

出力

パラメータ: id (型: int)
パラメータ: value (型: double)
パラメータ: name (型: const char*)

任意の型のハッシュ値を自動で求める

リフレクションを使うことで、任意のクラス型に対する汎用的なハッシュ関数を実装できる。以下の例では以下の方針で実装する:

  • std::hash<T>が特殊化されていればそれを使う
  • 特殊化されていない場合は、クラスのすべてのメンバ変数に対して再帰的にハッシュ値を計算しXORで合成する

これにより、ネストしたクラス型に対しても自動的にハッシュ値を計算できる。

#include <meta>
#include <functional>
#include <string>
#include <print>

// std::hash<T>が使用可能かを判定するコンセプト
template <class T>
concept has_std_hash = requires(const T& x) {
  { std::hash<T>{}(x) } -> std::convertible_to<std::size_t>;
};

template <class T>
constexpr std::size_t hash_value(const T& obj) {
  if constexpr (has_std_hash<T>) {
    // std::hashが特殊化されていれば利用する
    return std::hash<T>{}(obj);
  } else {
    // 特殊化されていなければ、各メンバのハッシュ値を再帰的に計算してXORで合成
    std::size_t result = 0;
    template for (constexpr auto m :
        std::define_static_array(std::meta::nonstatic_data_members_of(^^T,
            std::meta::access_context::unchecked()))) {
      result ^= hash_value(obj.[:m:]);
    }
    return result;
  }
}

struct Inner {
  int a;
  std::string b;
};

struct Outer {
  int x;
  Inner inner;  // ネストしたクラス型も再帰的にハッシュ化される
};

int main() {
  Outer o1{10, {1, "hello"}};
  Outer o2{10, {1, "hello"}};
  Outer o3{10, {2, "hello"}};

  std::println("hash(o1) == hash(o2): {}", hash_value(o1) == hash_value(o2));
  std::println("hash(o1) == hash(o3): {}", hash_value(o1) == hash_value(o3));
}

出力

hash(o1) == hash(o2): true
hash(o1) == hash(o3): false

関連項目

参照

  • P2996R13 Reflection for C++26
    • C++26での静的リフレクションの基本機能。リフレクション演算子^^、スプライス演算子[: :]std::meta::info型、<meta>ヘッダのメタ関数群を導入する。
  • P3394R4 Annotations for Reflection
    • 宣言にコンパイル時定数を付加するアノテーション機能[[=expr]]と、それを取得するためのannotations_of()メタ関数を追加する。
  • P3293R3 Splicing a base class subobject
    • 基底クラスのリフレクションに対するメンバアクセスのスプライスobj.[:base:]と、基底クラスとメンバ変数を統合取得するsubobjects_of()を追加する。
  • P3096R12 Function Parameter Reflection in Reflection for C++26
    • 関数パラメータのリフレクションを追加する。parameters_of()return_type_of()has_default_argument()などのメタ関数を導入する。
  • P3491R3 define_static_{string,object,array}
    • コンパイル時に計算した値を静的ストレージに配置するためのdefine_static_string()define_static_array()define_static_object()を追加する。
  • P3560R2 Error Handling in Reflection
    • リフレクションのメタ関数のエラー処理としてstd::meta::exception例外クラスを導入する。コンパイル時例外として動作する。