yohhoyの日記

技術的メモをしていきたい日記

C++ Reflection(P2996R10)

次期C++2c(C++26)標準規格に向けて検討が進んでいる リフレクション(reflection) についてメモ。本記事の内容は提案文書 P2996R10 に基づく。

要約:

  • 新しい演算子:^^、[:と:]の組
  • 新しい構文
    • リフレクト式:^^識別子、^^型名
    • スプライサ構文:[:r:]
    • constevalブロック宣言:consteval { statement(s) }
  • 新しい標準ヘッダ<meta>
    • std::meta::infoåž‹
    • 名前空間std::meta以下のリフレクション・メタ関数群
  • C++エンティティが持つあらゆる静的情報の問合せをサポート
    • 例1:列挙子の名前を取得(→id:yohhoy:20250228)、エンティティのソースコード位置source_locationを取得。
    • 例2:クラス型の全メンバ情報を取得、テンプレート引数情報を取得、エイリアス元情報を取得。
  • テンプレートパラメータの置換(substitute)操作をサポート
    • SFINAE(Substitution Failure Is Not An Error)エミュレーション動作を行える。と思う。
  • リフレクション情報からの集成体(aggregate)定義をサポート
    • クラスや共用体(union)の型情報をコンパイル時に組み立てるメタ・プログラミング。
  • テンプレート・メタ関数(<type_traits>)に対応するリフレクション・メタ関数(<meta>)
    • 例1:is_pointer_v<T>⇔bool meta::is_pointer_type(info)
    • 例2:remove_cvref_t<T>⇔info meta::remove_cvref(info)

リフレクション演算子

C++エンティティに対してリフレクション演算子(reflection operator)^^を適用したリフレクト式(reflect-expression)は、std::meta::info型のリフレクション値(reflection value; reflection)に評価される。リフレクション演算子^^オペランドには下記C++エンティティを指定できる。

次のC++エンティティには適用できない。

  • 非型テンプレートパラメータ(non-type template parameter) *1
  • パック・インデクス式(pack-index-expression) *2

ノート:属性(attribute)に対するリフレクションはP3385にて検討中*3。属性と構文が似たユーザ定義アノテーション(annotation)導入とリフレクションはP3394にて検討中*4。

リフレクション値

フレクション値を表現するstd::meta::info型の特徴:

  • デフォルト構築はヌル・リフレクション(null reflection)値となる。
  • リフレクション値の等値比較(=, !=)をサポートする。
  • 単一の不透明(opaque)型として設計される。
    • <type_traits>ヘッダ:リフレクション値の問合せ(query)を行うリフレクション・メタ関数群を提供。
  • 新しいプライマリ型カテゴリ(→id:yohhoy:20141122)として追加される。
    • <type_traits>ヘッダ:std::is_reflection関数を追加。
  • リフレクション演算子では直接取得できない特殊なリフレクション値も存在する。
    • 無名ビットフィールド(unnamed bit-field):無名ビットフィールドを含むクラス型Tからmembers_of(^^T)[n]
    • データメンバ記述(data member description):data_member_spec(^^T, {.name="m1"})
    • 直接基底クラス関係(direct base class relationship):public/protected/private継承関係。派生クラス型Dからbases_of(^^D)[0]
    • ノート:関数パラメータ(function parameter)は(PDF)P3096にて検討中。*5
#include <meta>
// 結果型は全て std::meta::info
constexpr auto r1 = ^^int;  // åž‹
constexpr auto r2 = ^^std::string;  // 型エイリアス
constexpr auto r3 = ^^std::malloc;  // 関数
constexpr auto r4 = ^^std::vector;  // クラステンプレート
constexpr auto r5 = ^^std::same_as;  // コンセプト
constexpr auto r6 = ^^std::linalg;  // 名前空間

// リフレクション・メタ関数は名前空間 std::meta に属する
// 引数型 std::meta::info からのADLによりスコープ指定は省略可能
static_assert( is_type(r1) );
static_assert( is_type(r2) && is_type_alias(r2) );
static_assert( is_function(r3) );
static_assert( is_template(r4) && is_class_template(r4) );
static_assert( is_template(r5) && is_concept(r5) );
static_assert( is_namespace(r6) );

スプライサ構文

std::meta::info型のリフレクション値rからC++エンティティを得る、スプライス指定子(splice-specifier)[:r:]が新しい構文要素として追加される。

  • スプライス式(splice-expression):[:r:]、template [:r:]で定数値を得る。
    • rがテンプレートを表すときtemplateキーワードが必要。
    • rがコンセプトを表すときはスプライス不可。*6
  • スプライス・型指定子(splice-type-specifier):[:r:]、typename [:r:]で型を指定。
    • 型が自明に要求されるコンテキストではtypenameキーワード省略可能。
  • スプライス・スコープ指定子(splice-scope-specifier):[:r:]::で名前空間を指定。
constexpr auto r_type = ^^int;
typename [:r_type:] x;  // int x;
         [:r_type:] y;  // int y;

namespace NS { void fn(); }
constexpr auto r_ns = ^^NS;
[:r_ns:]::fn();  // NS::fn();

template<int N> void fn();
constexpr int C = 42;
constexpr auto r_tfn = ^^fn;
constexpr auto r_var = ^^C;
template [:r_tfn:]<[:r_var:]>();  // f<42>();

集成体・共用体の定義

std::meta::define_aggregate関数とconstevalブロック宣言(consteval-block-declaration)を組み合わせて、クラス型や共用体型に対してリフレクション値からデータメンバを定義できる。std::meta::data_member_spec関数+std::meta::data_member_options型*7によりデータメンバ記述・リフレクション値を生成し、定義対象型とデータメンバ記述リストを指定する。

#include <meta>

struct Point;
consteval {
  std::meta::define_aggregate(^^Point, {
    data_member_spec(^^float, {.name = "x"}),
    data_member_spec(^^float, {.name = "y"})
  });
}
// struct Point { float x; float y; };と等価
// P2996R10, §4.8 A Simple Tuple Type
#include <meta>

template<typename... Ts> struct Tuple {
  struct storage;
  consteval {
    // storageクラスに(無名)データメンバを追加する
    define_aggregate(^^storage, {data_member_spec(^^Ts)...});
  }
  storage data;

  Tuple(const Ts& ...vs): data{vs...} {}
};

// Tuple<int, char>から下記クラス(相当)を定義
struct Tuple<int, char>::storage {
  int  _u0;
  char _u1;
};
// _uN は無名データメンバのためメンバ名でのアクセス不可
// nonstatic_data_members_of関数とスプライス式を利用し
// data.[:nonstatic_data_members_of(^^storage)[N]:] とする

関連URL

*1:非型テンプレートパラメータ V に対して std::meta::reflect_value(V) とすればリフレクション値を得られる。

*2:C++2c言語仕様への採択が決定している args...[n] 式。(PDF)P2662R3 参照。

*3:https://github.com/cplusplus/papers/issues/2042

*4:https://github.com/cplusplus/papers/issues/2074

*5:https://github.com/cplusplus/papers/issues/1764

*6:リフレクション・メタ関数 std::meta::can_substitute を用いて、あるコンセプトを用いた制約が満たされる(satisfied)か否かを判定可能。例:can_substitute(^^std::integral, {^^int});

*7:データメンバの name, alignment, bit_width, no_unique_address を制御可能。

Expansion statement for C++

プログラミング言語C++の次期標準C++2c(C++26)向けに提案*1されている展開文(Expansion statement)について。template for構文をもちいて本体処理を反復的にコンパイル時展開する。

// P1306R3: destructurable expression
auto tup = std::make_tuple(0, 'a', 3.14);
template for (auto elem : tup)
  std::println("{}", elem);
// 下記コードに展開される
auto&& tup = std::make_tuple(0, 'a', 3.14);
{ auto elem = std::get<0>(tup); std::println("{}", elem); }
{ auto elem = std::get<1>(tup); std::println("{}", elem); }
{ auto elem = std::get<2>(tup); std::println("{}", elem); }
// P1306R3: expansion-init-list
template for (auto elem : {0, 'a', 3.14})
  std::println("{}", elem);
// 下記コードに展開される
{ auto elem = 0;    std::println("{}", elem); }
{ auto elem = 'a';  std::println("{}", elem); }
{ auto elem = 3.14; std::println("{}", elem); }

C++2c向けに同時検討されているC++ Reflection機能(→id:yohhoy:20250305)と組み合わせると、全C++プログラマの夢(?)であった「列挙型の値から文字列への変換関数」をジェネリックに実装できる。

// P1306R3 + P2996(Reflection)
#include <type_traits>
#include <meta>  // P2996

template <typename E>
  requires std::is_enum_v<E>
constexpr std::string enum_to_string(E value) {
  // リフレクション(reflection)演算子^^ + enumerators_ofメタ関数
  //   列挙型Eの列挙子リストをstd::vector<std::meta::info>で返す
  template for (constexpr auto e : std::meta::enumerators_of(^^E)) {
    // リフレクション値(reflection value) eの型 == std::meta::info
    // スプライス式(splice-expression) [:e:] により列挙子の値を取得
    if (value == [:e:]) {
      // identifier_ofメタ関数
      //   列挙子eの識別子(名前)をstd::string_viewで返す
      return std::string(std::meta::identifier_of(e));
    }
  }
  return "<unnamed>";
}

template for構文においても通常のfor構文と同じくbreak文/continue文をサポートする。コンパイル時展開はbreak/continue制御によらず行われ、評価フェーズで実行パス分岐が行われることに注意。

// P1306R3
template for (auto v : {1,2,3,4,5,6,7,8,9}) {
  if (v % 2 == 0) continue;
  std::print("{} ", v);
  if (v % 5 == 0) break;
}
// 1 3 5

関連URL

サンプルコードによるC++23ジェネレータの紹介

サンプルコードによるC++23ジェネレータの紹介

本文こちら→C++ MIX #13に参加しました - yohhoyの日記(別館)

スライド資料:https://www.docswell.com/s/yohhoy/KEXQPG-cpp23gen

関連URL

declcall演算子

プログラミング言語C++の次期標準C++2c(C++26)に導入されるdeclcall演算子についてメモ。

declcall演算子は関数呼び出しを含む式をオペランドにとり、オーバーロード解決後の関数ポインタ/メンバ関数ポインタを返す定数式(constant expression)となる。このときdeclcall演算子のオペランドは評価されない(unevaluated)。

// C++2c
#include <functional>

void f(int);    // #1
void f(float);  // #2

auto pf1 = declcall(f(0)  ); // #1のアドレス
auto pf2 = declcall(f(.0f)); // #2のアドレス

std::function<void(int)> fn0{f};  // NG: ill-formed
std::function<void(int)> fn1{declcall(f(0))};  // OK: #1

declcall演算子の固有機能として、仮想メンバ関数呼び出し式に適用すると脱仮想化ポインタ(devirtualized pointer)を生成する。下記例示においてpmf2は基底クラスのメンバ関数B::mfに固定された脱仮想化ポインタとなる。

// C++2c
#include <cassert>
#include <concepts>

struct B {
  virtual int mf() { return 1; }
};
struct D : B {
  virtual int mf() override { return 2; }
};

D obj;
assert(obj.mf() == 2);     // D::mf
assert(obj.B::mf() == 1);  // B::mf

auto pmf1 = &B::mf;  // int(B::*)()
assert((obj.*pmf1)() == 2);  // D::mf

auto pmf2 = declcall(obj.B::mf());  // int(B::*)()
assert((obj.*pmf2)() == 1);  // B::mf (devirtualized)

ノート:演算子名declcallは、評価されないコンテキストにおいて指定型の値を生成するヘルパ関数std::declvalからの連想と考えられる。

関連URL

名前付きループ in 標準C

プログラミング言語Cの次期標準C2yでは、名前付きループ(Named Loop)構文としてbreak/continue文へのラベル指定がサポートされる。

// C2y
outer:
for (int i = 0; i < N; ++i) {
  for (int j = 0; j < M; ++j) {
    break;       // 内部ループ脱出: 1)へ
    break outer; // 外部ループ脱出: 2)へ  
  }
  // 1)
}
// 2)

同様の構文はJava*1, JavaScript*2, Rust*3, Go*4等でもサポートされている。

2025-02-23追記:C++言語では2014年頃に同等機能の追加提案(PDF)N3879が却下された過去がある。C2yでの採用をうけて、2025年1月現在はP3568にて再検討が行われている。おそらくC++2d(C++29)頃がターゲット。*5

関連URL

8進数リテラルプレフィクス in 標準C

プログラミング言語Cの次期標準C2yでは、8進数リテラルプレフィクス0o/0Oが導入される。

// C2y
int n1 = 0o52;  // 42
int n2 = 0O52;  // 42
int n0 = 052;   // 42 (従来記法; 廃止予定)

0o/0Oプレフィクス追加と同時に、数値0のみプレフィクスとする従来8進数リテラル表記は廃止予定(obsolescent feature)とされる。

プログラミング言語C++に対しても2005年頃(!)に同等の提案P0085R0がなされており、C2y採択に伴ってC++2c(C++26)に向けた検討が再開されている。*1

関連URL

OpenMP 6.0仕様リリース

2024年11月 OpenMP 6.0仕様リリース記事 https://www.openmp.org/home-news/openmp-arb-releases-openmp-6-0-for-easier-programming/ より抄訳。

OpenMP仕様バージョン6.0はOpenMP ARB、主要なコンピュータハードウェア/ソフトウェアベンダーのグループ、そしてOpenMPコミュニティ全体のユーザーによって共同開発されました。改訂仕様ではいくつかの細かな改良に加え、次の大きな追加が行われました:

  • タスクプログラミングの簡略化:
    • 並列リージョンを実行するどのチームにも割当てられない、フリーエージェント(free-agent)スレッドによるタスク実行のサポート。
    • 効率的な再現実行(replay execution)のためのタスクグラフ記録(taskgraph record)の有効化。
    • 依存関係を指定できるタスクの集合を拡張する、透過タスク(transparent task)の追加。
  • デバイスサポートの拡張:
    • 配列構文アプリケーションのサポート追加:workdistributeディレクティブは配列記法の実行を個別の作業単位へ分割し、Fortranのデバイスサポートを強化します。
    • メモリ割当とアクセシビリティの制御が強化され、割当可能な変数の管理が容易になります。
    • デフォルトのデータ環境属性のサポートを拡張。
    • 構造化された非同期データマッピング領域の追加による、非同期データ転送の記述の容易化。
    • デバイスへのデータマッピング拡張によるメモリ制御の向上。
    • groupprivateディレクティブより、デバイス上にチーム別メモリを持つ機能を追加。
  • ループ変換(loop transformation)プログラミングの容易化: ループの融合(fusion)・反転(reverse)・交換(interchange)の使用が簡素化されます。
  • インダクション(induction)操作のサポート:既知のパターンに従うループにおける基本的な算術演算とユーザ定義演算の並列化をサポート。*1
  • 最新C/C++およびFortran言語標準の並列化をサポート:
    • C属性構文*2を含むC23、Fortran 2023、C++23の完全サポート。
    • 新しいC/C++属性の導入。
  • ストレージリソースとメモリスペースのユーザコントロール強化:
    • メモリ割当制御を強化する新しいメモリトレイトを追加。
    • メモリ空間の定義とクエリのための新しいAPIルーチンを提供。
  • 非推奨機能の削除:バージョン5.0、5.1、5.2で非推奨とされた機能が削除されました。

関連URL

*1:従来からある リダクション(reduction)=演繹操作 に対して、インダクション(induction)=帰納操作 が導入される。反復的ループにおいて前イテレーションまでの計算結果に依存する演算。OpenMP 6.0では組込型のインダクション操作として加算(+)と乗算(*)がサポートされる。

*2:https://yohhoy.hatenadiary.jp/entry/20200505/p1