最終更新日時:
が更新

履歴 編集

静的リフレクション [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が表す名前空間を挿入

変数を表すリフレクションに対する式スプライスは、対応する式と同じ振る舞いをする。たとえば事前条件式 (pre) や事後条件式 (post) の中の式は自動的にconst修飾されるが、式スプライス[:r:]の結果も同様にconst修飾される。

メンバアクセスにもスプライスを使用できる。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::stringやstd::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>
#include <type_traits>

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>
#include <concepts>
#include <cstddef>

// 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

アノテーションで指定した値の範囲を検証する

アノテーションでメンバ変数に制約を付加しておくことで、メンバごとに検証コードを書くことなく、任意のクラスに対して働く汎用の検証関数を実装できる。

以下の例では、整数用のIntRangeと浮動小数点数用のFloatRangeを定義し、それらが付加されたメンバ変数だけを検証して、範囲外であればstd::out_of_range例外を送出する。アノテーションが付いていないメンバ変数は検証の対象にならない。

#include <meta>
#include <cstdio>
#include <format>
#include <print>
#include <stdexcept>
#include <string_view>

// 値の範囲を表すアノテーションクラス
struct IntRange {
  int min_value;
  int max_value;
};

struct FloatRange {
  double min_value;
  double max_value;
};

struct Config {
  [[=IntRange{1, 65535}]] int port;
  [[=IntRange{1, 300}]] int timeout_seconds;
  [[=FloatRange{0.0, 1.0}]] double sampling_rate;
  int retry_count;  // アノテーションがないメンバ変数は検証しない
};

// IntRangeとFloatRangeのどちらにも使える検証処理
template <class R, class T>
void check_range(std::string_view name, T value, R range) {
  if (value < range.min_value || range.max_value < value) {
    throw std::out_of_range{
      std::format("{}: {}は{}以上{}以下でなければならない",
                  name,
                  value,
                  range.min_value,
                  range.max_value)};
  }
}

// 範囲のアノテーションが付加されたメンバ変数の値を検証する
template <class T>
void validate(const T& obj) {
  template for (constexpr auto m :
      std::define_static_array(std::meta::nonstatic_data_members_of(^^T,
          std::meta::access_context::unchecked()))) {
    static constexpr auto int_annotations = std::define_static_array(
      std::meta::annotations_of_with_type(m, ^^IntRange));
    static constexpr auto float_annotations = std::define_static_array(
      std::meta::annotations_of_with_type(m, ^^FloatRange));

    // アノテーションは値のリフレクションではないため、
    // constant_of()で値を取り出してからスプライスする
    if constexpr (int_annotations.size() > 0) {
      check_range(std::meta::identifier_of(m), obj.[:m:],
                  [:std::meta::constant_of(int_annotations[0]):]);
    }
    else if constexpr (float_annotations.size() > 0) {
      check_range(std::meta::identifier_of(m), obj.[:m:],
                  [:std::meta::constant_of(float_annotations[0]):]);
    }
  }
}

int main() {
  // すべて範囲内なので、例外は送出されない
  validate(Config{
    .port = 8080,
    .timeout_seconds = 30,
    .sampling_rate = 0.5,
    .retry_count = 3
  });

  try {
    // portが範囲外
    validate(Config{
      .port = 0,
      .timeout_seconds = 30,
      .sampling_rate = 0.5,
      .retry_count = 3
    });
  } catch (const std::out_of_range& e) {
    std::println(stderr, "{}", e.what());
  }

  try {
    // sampling_rateが範囲外
    validate(Config{
      .port = 8080,
      .timeout_seconds = 30,
      .sampling_rate = 1.5,
      .retry_count = 3
    });
  } catch (const std::out_of_range& e) {
    std::println(stderr, "{}", e.what());
  }
}

出力

port: 0は1以上65535以下でなければならない
sampling_rate: 1.5は0以上1以下でなければならない

アノテーションからコマンドラインオプションを解析する

アノテーションはメンバ変数の検証だけでなく、宣言からコードを生成する用途にも使える。以下の例では、Rustのclapのように、コマンドラインオプションの短縮名と説明をアノテーションでメンバ変数に付加し、そこからヘルプメッセージの生成と引数の解析の両方を行う。

オプションを追加したいときはArgsにメンバ変数とアノテーションを足すだけでよく、ヘルプメッセージと解析処理のどちらにも手を入れる必要がない。

#include <meta>
#include <format>
#include <print>
#include <stdexcept>
#include <string>
#include <string_view>

// オプションの短縮名を表すアノテーション
struct Short {
  char name;
};

// オプションの説明を表すアノテーション
struct Help {
  const char* text;
};

struct Args {
  [[=Short{'n'}, =Help{std::define_static_string("挨拶する相手の名前")}]]
  std::string name;

  [[=Short{'c'}, =Help{std::define_static_string("挨拶を繰り返す回数")}]]
  int count = 1;
};

void assign(std::string& member, std::string_view value) {
  member = value;
}

void assign(int& member, std::string_view value) {
  member = std::stoi(std::string{value});
}

// アノテーションからヘルプメッセージを生成する
template <class T>
void print_help() {
  std::println("オプション:");
  template for (constexpr auto m :
      std::define_static_array(std::meta::nonstatic_data_members_of(^^T,
          std::meta::access_context::unchecked()))) {
    static constexpr auto shorts = std::define_static_array(
      std::meta::annotations_of_with_type(m, ^^Short));
    static constexpr auto helps = std::define_static_array(
      std::meta::annotations_of_with_type(m, ^^Help));

    std::println("  -{}, --{:<8} {}",
                 [:std::meta::constant_of(shorts[0]):].name,
                 std::meta::identifier_of(m),
                 [:std::meta::constant_of(helps[0]):].text);
  }
}

// アノテーションをもとにコマンドライン引数を解析する
template <class T>
T parse(int argc, const char* const* argv) {
  T args{};
  for (int i = 1; i < argc; ++i) {
    std::string_view arg = argv[i];
    bool matched = false;

    template for (constexpr auto m :
        std::define_static_array(std::meta::nonstatic_data_members_of(^^T,
            std::meta::access_context::unchecked()))) {
      static constexpr auto shorts = std::define_static_array(
        std::meta::annotations_of_with_type(m, ^^Short));
      constexpr std::string_view long_name = std::meta::identifier_of(m);
      constexpr char short_name = [:std::meta::constant_of(shorts[0]):].name;

      bool is_long = arg.starts_with("--") && arg.substr(2) == long_name;
      bool is_short = arg.size() == 2 && arg[0] == '-' && arg[1] == short_name;

      if (is_long || is_short) {
        if (++i == argc) {
          throw std::invalid_argument{std::format("{}: 値が指定されていない", arg)};
        }
        assign(args.[:m:], argv[i]);
        matched = true;
        break;
      }
    }

    if (!matched) {
      throw std::invalid_argument{std::format("{}: 不明なオプション", arg)};
    }
  }
  return args;
}

int main() {
  print_help<Args>();

  // コマンドライン引数を模したデータ
  const char* argv[] = {"greet", "--name", "wg21", "-c", "3"};
  Args args = parse<Args>(5, argv);

  for (int i = 0; i < args.count; ++i) {
    std::println("Hello {}", args.name);
  }
}

出力

オプション:
  -n, --name     挨拶する相手の名前
  -c, --count    挨拶を繰り返す回数
Hello wg21
Hello wg21
Hello wg21

JSONにシリアライズする

リフレクションでメンバ変数を走査すれば、クラスごとにシリアライズ関数を書かなくてもJSONへ変換できる。さらにアノテーションを併用することで、JSON上の表現だけをクラスの定義から制御できる。

以下の例では、キー名を変更するRenameと、シリアライズの対象から外すSkipを定義する。どちらもC++のコード上での使い方には影響せず、JSONへの変換方法だけを指定する。

#include <meta>
#include <concepts>
#include <format>
#include <print>
#include <string>
#include <string_view>

// JSONのキー名を変更するアノテーション
struct Rename {
  const char* name;
};

// シリアライズの対象から外すアノテーション
struct Skip {};

struct Address {
  std::string city;
  int postal_code;
};

struct Person {
  [[=Rename{std::define_static_string("first name")}]] std::string first;
  [[=Rename{std::define_static_string("last name")}]] std::string last;
  int age;
  Address address;                    // ネストしたクラスも再帰的に変換される
  [[=Skip{}]] std::string password;   // シリアライズしない
};

template <class T>
std::string to_json(const T& value) {
  if constexpr (std::integral<T> || std::floating_point<T>) {
    return std::format("{}", value);
  }
  else if constexpr (std::convertible_to<T, std::string_view>) {
    return std::format("\"{}\"", value);
  }
  else {
    std::string result = "{";
    bool is_first = true;

    template for (constexpr auto m :
        std::define_static_array(std::meta::nonstatic_data_members_of(^^T,
            std::meta::access_context::unchecked()))) {
      static constexpr auto skips = std::define_static_array(
        std::meta::annotations_of_with_type(m, ^^Skip));

      if constexpr (skips.size() == 0) {
        static constexpr auto renames = std::define_static_array(
          std::meta::annotations_of_with_type(m, ^^Rename));

        // Renameアノテーションがあればその名前を、なければメンバ変数名をキーにする
        constexpr std::string_view key = [] {
          if constexpr (renames.size() > 0) {
            return std::string_view{[:std::meta::constant_of(renames[0]):].name};
          }
          else {
            return std::meta::identifier_of(m);
          }
        }();

        if (!is_first) {
          result += ',';
        }
        is_first = false;
        result += std::format("\"{}\":{}", key, to_json(value.[:m:]));
      }
    }
    return result + '}';
  }
}

int main() {
  Person person{
    .first = "Taro",
    .last = "Yamada",
    .age = 30,
    .address = {.city = "Tokyo", .postal_code = 1000001},
    .password = "secret"
  };

  std::println("{}", to_json(person));
}

出力

{"first name":"Taro","last name":"Yamada","age":30,"address":{"city":"Tokyo","postal_code":1000001}}

関連項目

参照

  • 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例外クラスを導入する。コンパイル時例外として動作する。
  • P3598R0 CWG 3158 — const-ification of Splice Expressions
    • 変数のリフレクションに対するスプライス式に対して、対応するid-expressionと同様の文脈依存な型調整(const化など)を適用するよう仕様を修正する。