本稿は「C++26 Reflection 💚 QRangeModel」の抄訳です。
Qt Company の R&D 組織では、年の始まりにハッカソンを開催することを恒例にしています。誰もが自分の興味のあるテーマに自由に取り組めるイベントです。普段とは違うものや技術に触れたり、新しいテクノロジーを試したり、あるいは以前から気になっていたことに取り組んだりする絶好の機会となっています。私たちは前の週にピッチセッションを行い、アイデアを探している人や参加するプロジェクトを探している人が、どのようなテーマがあるのかを把握できるようにしました。そして水曜日の朝にハッキングを開始し、金曜日の正午に行われる発表まで、各自が自分のプロジェクトに取り組める丸二日間が与えられました。
ついに暦が次期 C++ 標準である C++26 に追いついたので、その中でも特に注目されている機能の一つである「リフレクションとアノテーション」を試してみることにしました。
2 日間で moc を置き換えるのはさすがに野心的すぎると思い、代わりに、新しい QRangeModel に「プレーンな C++ クラスの範囲」を、Qt Quick のアイテムビューが扱えるモデルとして表現させることができるかを探ることにしました。つまり、Q_GADGET や Q_OBJECT マクロによるメタオブジェクトは使わず、かといってタプルプロトコル用のボイラープレートも不要、という前提です。
ある型のリストは ――
struct Entry
{
QString name;
double value;
};
2 列のテーブルモデルとして表現されるべきであり、列名(QRangeModel の QAbstractItemModel::headerData の実装に従う)は "name" と "value" になります。さらに、明示的にマルチロールアイテムとしてタグ付けされている場合、あるいはそのようなエントリのテーブル(たとえば QList<QList<Entry>>)で使用される場合には、各アイテムは "name" および "value" ロールのデータを持つべきです。
model = std::make_unique(QList<QList<Entry>>{
{
{"zero", 0}, {"zero.one", 0.1}, {"zero.two", 0.2}
},
{
{"one", 1}, {"one.one", 0.1}, {"one.two", 1.2}
},
{
{"two", 2}, {"two.one", 2.1}, {"two.two", 2.2}
},
});
そのモデルを使って ListView を利用する UI の QML コードは、次のようになります:
Rectangle {
id: root
visible: true
implicitWidth: 1200
implicitHeight: 500
property AbstractItemModel model
ListView {
id: list
model: root.model
anchors.fill: parent
delegateModelAccess: DelegateModel.ReadWrite
delegate: Text {
id: delegate
required property var name
required property var value
width: ListView.view.width
text: delegate.name + ": " + delegate.value
MouseArea {
anchors.fill: parent
onClicked: ++delegate.value
}
}
}
}
「name」および「value」ロールのデータには、名前付きプロパティとしてアクセスできるようにしたいですし、そのデータを直接変更できるようにもしたいと考えています(つまり、エントリをクリックしたときに「value」を 1 増やす、というような操作です)。しかも、それを 一切のボイラープレートコードや、メタオブジェクトコンパイラによって生成されたコードなしで 実現したいのです!
さらに発展的な試みとして、単純な集約型にとどまらず、Qt のコードではもう少し一般的な型もサポートできるかどうかを試してみたいと考えました:
class Class
{
public:
explicit Class() = default;
explicit Class(const QString &name, double value)
: m_name(name), m_value(value)
{}
QString name() const { return m_name; }
void setName(const QString &name)
{
m_name = name;
}
double value() const { return m_value; }
void setValue(double value)
{
m_value = value;
}
private:
QString m_name;
double m_value = -1;
};
上記のような型をモデルのアイテム型に変換するために、どのような C++26 のアノテーションを使用し、それをリフレクションすればよいでしょうか?
このプロジェクトは、進行中の C++26 実装、特にリフレクションに関するコンパイラソースを探すところから始まりました。
gcc については、 https://forge.sourceware.org/marek/gcc/src/branch/reflection が最も最新だと分かりました。clang については、 https://github.com/bloomberg/clang-p2996.git の p2996 ブランチを見つけました。しかし、使える時間は 2 日しかなく、さらに clang にそのブランチの標準ライブラリヘッダを使わせようとした際にビルドシステム周りでいくつか問題があったため、Linux 上で gcc を使うことに集中することにしました。
偶然にも、gcc の機能ブランチは木曜日に trunk にマージされました。そのため現在では、trunk からビルドすればリフレクションのサポートを利用できます:
$ cd ~
$ git clone git://gcc.gnu.org/git/gcc.git gcc-trunk
$ mkdir gcc-trunk-build
$ cd gcc-trunk-build
$ ../gcc-trunk/configure --prefix=/opt/gcc-trunk --enable-languages=c,c++ --disable-multilib
$ make -j $(nproc)
$ sudo make install
異なる gcc バージョン、C++ 標準、標準ライブラリのバージョンでビルドされたバイナリを混在させることによるトラブルを避けるため、まず dev ブランチから Qt の一部を新規にビルドしました:
$ cd ~
$ git clone git://code.qt.io/qt/qt5.git qt-dev
$ cd qt-dev
$ ./init-repository -submodules qtdeclarative
$ cd -
$ mkdir qt-dev-build
$ cd qt-dev-build
$ ../qt-dev/configure -developer-build -no-warnings-are-errors -make tests -submodules qtdeclarative \
-- -DCMAKE_C_COMPILER=/opt/gcc-trunk/bin/gcc -DCMAKE_CXX_COMPILER=/opt/gcc-trunk/bin/g++ -DQT_BUILD_TESTS_BY_DEFAULT=OFF
$ ninja -k 0
init-repository の呼び出しでは、qtdeclarative をビルドするために必要なすべてのサブモジュールが取得されます。configure の行では、ビルド後に Qt をインストールする必要がないこと(つまり prefix が $PWD であること)、新たに導入された警告によってビルドが停止しないこと、すべてのテストが設定されるもののコンパイルはされないことを保証しています。
私がビルドしたいのは qtdeclarative とそれに必要なものだけであり、リーフモジュールは含めません。
configure の出力の先頭が特定の内容で始まっていれば――
-- The CXX compiler identification is GNU 16.0.1
-- The C compiler identification is GNU 16.0.1
正しく設定できています。このビルドでは -Wsfinae-incomplete による新しい警告がいくつか出ますが、これについては調査していません。
QRangeModel の作業を正常に動作するベースラインから始めたいと考えていたため、実行すべきテストは当然 tst_qrangemodel でした:
$ ninja tst_qrangemodel_check
テストは正常に実行されました。さあ、ハックの時間です。
公開されている QRangeModel クラスは、見た目としては通常の QAbstractItemModel の実装のように見えますが、コンストラクタだけがテンプレートになっています。
しかし実装の大部分は内部テンプレートの助けを借りて行われています。これは、モデルの行や要素がレンジなのか、配列なのか、メタオブジェクトを持つ型なのか、あるいはタプルプロトコルが実装された型なのかをコンパイラが判断できるようにするための、テンプレート特殊化の集合を含んでいます。これにより、原理的には部分テンプレート特殊化を利用し、プレーンな集約型に対する C++26 の実装を提供することが可能になります。
ただし、そのロジックの一部は現在 constexpr if 文を使って記述されています。これらは特殊化できません。また、ハッカソンとはいえ、QRangeModel の実装に C++26 のコードを追加することは避けたいと考えていました。少なくとも、変更のたびに Qt の一部をビルドする必要が生じ、テストケースだけをビルドする場合に比べて作業が遅くなるからです。
そこで、現在の設計の制約を明らかにするいくつかの実験を行った後、次のステップとして、ハードコードされていたロジックの一部を特殊化可能なテンプレートへと切り出しました。その結果、該当する実装詳細をリファクタリングし整理するための、少数のパッチが生まれました。
この作業の後、行の要素にアクセスするためのカスタマイズポイント、行のカラム名を取得するためのカスタマイズポイント、そしてアイテム型に対するロール名の一覧を取得するためのカスタマイズポイントが得られました。
補足として述べると、ここで示している以上に多くの補助的な仕組みが関与しており、これらの QRangeModelDetails の trait はいずれも公開 API やカスタマイズポイントとして意図されたものではありません。完全な解決策については、以下の URL で終わる一連のコミットを参照してください。
https://codereview.qt-project.org/c/qt/qtbase/+/704327.
標準ライブラリは型特性として std::is_aggregate を提供しています。
集約型の定義には配列型(std::array を含みます。これは std::is_array ではマッチしません)が含まれており、また集約型の作者がタプルプロトコルを実装している場合もあります。配列およびタプルについては、すでに QRangeModel のアクセス機構を特殊化しているため、競合を避ける目的で、これを「純粋な集約型(pure aggregates)」という概念に絞り込みました:
template <typename T>
concept PureAggregate = std::conjunction_v<
std::is_aggregate<T>,
std::negation<std::is_array<T>>,
std::negation<QRangeModelDetails::array_like<T>>,
std::negation<QRangeModelDetails::tuple_like<T>>
>;
その結果、row_traits テンプレートの特殊化は次のようになります:
template <PureAggregate T>
struct row_traits<T>
{
// ~~~
};
row_traits 型は、その行型がいくつの要素を持つか、そしてその要素数がコンパイル時に定義されているのか、実行時に定義される定数なのか、あるいは完全に動的なのかを QRangeModel に伝える役割を担っています。
さらに、準備作業で追加された機能により、要素数がコンパイル時に定義されている場合には、要素へアクセスするための実装も提供しなければなりません。
最初の試みでは、C++26 の新しい言語機能である構造化束縛パック(P1061)だけを使うことを考えました。
static consteval auto element_count()
{
auto [...e] = T{};
return sizeof...(e);
}
これは動作しません。Entry クラスは constexpr ではないからです。QString はメモリの確保と解放を行うため、consteval コンテキスト内で Entry を構築・破棄することはできません。
メンバーがすべて Plain Old Data のみで構成された Entry 型であれば動作しますが、それでは用途がかなり限定されてしまいます。
そこで、C++26 のリフレクション, P2996 に本格的に踏み込むことにしました。新しい meta ライブラリも含めてです:
#include <meta>
// ...
static consteval auto element_count()
{
return nonstatic_data_members_of(^^T, std::meta::access_context::current()).size();
}
ここでは、C++26 で追加された 3 つの新しい構文要素のうちの 1 つであるリフレクション演算子 ^^ が使われています。これは、そのオペランドである T(つまり Entry)に対するリフレクションを std::meta::info オブジェクトの形で返します。これはコンパイラ定義の不透明なエンティティです。
std::meta ライブラリは、そのような info オブジェクトを受け取って、リフレクションされた型に関するコンパイル時情報を返す多数の consteval 関数を提供します。この場合、関心があるのは、現在のコンテキストからアクセス可能な Entry の非 static データメンバーであり、その要素数だけが必要です。
なお、nonstatic_data_members_of を std::meta::nonstatic_data_members_of のように完全修飾する必要はありません。引数依存名前探索(ADL)のおかげで、コンパイラはその std::meta::info 引数と同じ名前空間、すなわち常に std::meta の中でその関数を探します。
これで、Entry 型にも対応する要素数の実装が得られました。
次のステップは、QRangeModel が Entry から読み書きしようとする際に、正しいデータメンバーへアクセスすることです。これを行う row_traits の関数は for_element_at と呼ばれ、QRangeModel から、フレームワーク側の処理を行うラムダとともに呼び出されます。読み取り時には値を QVariant として返し、書き込み時には dataChanged() シグナルを emit するためのものです。
この関数を、対象要素への参照を渡して呼び出す必要があります。
もしかすると、ここで構造化束縛パックが使えるかもしれません:
template <typename Row, typename F>
static auto for_element_at(Row &&row, std::size_t idx, F &&function)
{
auto &[...e] = &row;
function(std::forward_like<Row>(e...[idx]));
}
これは動作しません。構造化束縛パックのインデックス演算子は定数式でなければならず、idx は実行時パラメータだからです。
手書きで switch 文を書くこともできますし、フォールド式を使って、コンパイラにその switch と等価なものを生成させることもできます。QRangeModel の実装ではそのような処理が何度も必要になるため、再利用できる小さな内部ヘルパーを用意しています:
template <typename Row, typename F>
static auto for_element_at(Row &&row, std::size_t idx, F &&function)
{
auto &[...e] = row;
QtPrivate::applyIndexSwitch<sizeof...(e)>(idx, [&](auto idxConstant) {
function(std::forward_like<Row>(e...[idxConstant.value]));
});
}
これで、Entry のような集成体型の任意のデータメンバーを読み書きできるようになりました。そのために、構造化束縛パックと、C++23 の std::forward_like を使用しています。これにより、row への転送参照として受け取った値カテゴリと同じ値カテゴリで、要素を関数に渡すことが保証されます。つまり、row が const 参照であれば、要素も const 参照として渡されます。
(前述のリファクタリング後における)row_traits 特殊化の最後の責務は、各列の名前を QVariant(実際には QString を保持)として提供することです。今回は再び std::meta ライブラリの機能を使い、インデックスに対応する要素の識別子を取得します。
static QVariant column_name(int section)
{
QVariant result;
QtPrivate::applyIndexSwitch<element_count()>(section, [&](auto idxConstant) {
constexpr auto member = nonstatic_data_members_of(^^T,
std::meta::access_context::current()).at(idxConstant.value);
result = QString::fromUtf8(u8identifier_of(member).data());
});
return result;
}
これで、Entry 型を 2 列を持つ行として扱えるようになりました。QTableView で
QRangeModel model(QList<Entry> { ... }) を使うと、2 列(タイトルは name と value)が表示され、各行に対応する値が示されます。さらに、それぞれの値を編集することもできます。
しかし Qt Quick の UI では、データは列ではなくロールとしてアクセスされます。これを機能させるには、さらに少し特殊化を追加する必要があります。item_traits を特殊化し、Qt::ItemDataRole からロール名へのマッピングを行う QHash<int, QByteArray> を返す roleNames 関数を実装する必要があります。これは column_name の実装とほぼ同じコードになるため、ここでは繰り返しません。
最後に、今度はインデックスではなく Qt::ItemDataRole によってアクセスするロジックを実際に実装する必要があります。Qt 6.11 では、型の作者が独自の読み書き処理を実装できるようにするためのテンプレートプロトタイプ QRangeModel::ItemAccess が導入されています。これは公開されたカスタマイズポイントなので、そのまま利用できます。Qt gadget やオブジェクトに対する QRangeModel の慣例では、カスタムプロパティは Qt::UserRole + n にマッピングされます。
template <QRangeModelDetails::PureAggregate T>
struct QRangeModel::ItemAccess<T>
{
using row_traits = QRangeModelDetails::row_traits<T>;
static QVariant readRole(const T &item, int role)
{
const int index = role - Qt::UserRole;
if (index < 0 || index >= row_traits::element_count())
return {};
QVariant result;
QtPrivate::applyIndexSwitch<row_traits::element_count()>(index, [&](auto idxConstant){
constexpr auto member = nonstatic_data_members_of(^^T,
std::meta::access_context::current()).at(idxConstant.value);
result = item.[:member:];
});
return result;
}
読み取りアクセスの実装では、再び applyIndexSwitch ヘルパーを使用します。そしてここで、C++26 が C++ 言語に導入する 2 つ目の演算子である スプライス演算子 [: :] を使います。
型 T のデータメンバに対するリフレクション member(item はその型のインスタンス)を与えると、item.[:member:] はそのメンバへのアクセスを C++ コードにスプライスします。つまり、インデックス 0 の場合は item.name が生成され、インデックス 1 の場合は item.value が生成されます。
そのメンバに QVariant を書き込む処理は少し厄介です。QVariant から正しい型の値を取り出す必要があるためです。どのメンバにアクセスするかは分かっているので、その型を推論する方法は 2 つあります。decltype(item.[:member:]) を使う方法、あるいは meta ライブラリの type_of(member) 関数の結果をコードにスプライスする方法です:
static bool writeRole(T &item, const QVariant &data, int role)
{
const int index = role - Qt::UserRole;
if (index < 0 || index >= row_traits::element_count())
return {};
bool result = false;
QtPrivate::applyIndexSwitch<row_traits::element_count()>(index, [&](auto idxConstant){
constexpr auto member = nonstatic_data_members_of(^^T,
std::meta::access_context::current()).at(idxConstant.value);
using MemberType = [:type_of(member):];
result = data.canConvert<MemberType>();
if (result)
item.[:member:] = data.value<MemberType>();
});
return result;
}
};
QRangeModel が QList<Entry> 内の Entry アイテムを 2 列に展開してしまうのを避けるために、Entry 型に対して QRangeModel::RowOptions を特殊化することができます:
template <>
struct QRangeModel::RowOptions<Entry>
{
static constexpr auto rowCategory = QRangeModel::RowCategory::MultiRoleItem;
};
この状態で QRangeModel(QList<Entry>) を QML UI のモデルとして使用すると、各アイテムが name: value を表示するリストとして描画されます。さらに、Qt::DisplayRole に対する ItemAccess::readRole の特殊化を実装すれば、ウィジェットの QListView や QTableView でも同様に表示されるようになります。
ここまで来た時点でまだ水曜日だったので、2 つ目のアイテム型が実現できるかを試すには十分な時間がありました。つまり、アグリゲートには該当せず、カプセル化され、メンバーアクセス関数やユーザー定義コンストラクタを持つ型です。
まず、そのような型に対してテンプレートを特殊化できるような、何らかの固有の特徴を見つける必要があります。すぐに試せること、そして型をマルチロールアイテムとしてタグ付けするために QRangeModel::RowOptions テンプレートを特殊化しなければならないのがいずれにしてもやや冗長であることから、C++26 のアノテーションによるリフレクション(P3394)を使ってみることにしました。これは C++26 に追加される 3 つ目の新しい構文要素 [[=...]] を導入するものです。見た目は属性(attributes)に非常によく似ていますが、より高い柔軟性を備えています。
template <QRangeModel::RowCategory Category>
class [[=Category]] Class
{
// ~~~
};
任意の「構造的型(structural type)」はアノテーションとして使用できます。非型テンプレート引数として使用できる型は、アノテーションとしても使用可能です。その意味を正確に説明している C++ の資料として私が見つけた最も良いものは、テンプレートパラメータに関するページでした。最も単純なケースとしては、QRangeModel::RowCategory のような enum の値をアノテーションとして使用することができます。これにより、そのアノテーションが存在することを条件とした concept を定義することができます:
namespace QRangeModelDetails
{
template <typename T>
static consteval std::optional<QRangeModel::RowCategory> rangemodel_category()
{
auto categories = annotations_of_with_type(^^T, ^^QRangeModel::RowCategory);
if (categories.size())
return extract<QRangeModel::RowCategory>(categories.front());
return std::nullopt;
}
template <typename T>
concept RangeModelElement = rangemodel_category<T>().has_value();
template <RangeModelElement T>
struct row_traits<T>
{
// ~~~
}
}
私たちの row_traits 特殊化は、QRangeModel::Category 型のアノテーションを少なくとも 1 つ持つ任意の型 T に対して使用されます。
次に必要なのは、この型に対する element_count 実装に相当するものです。ここではプロパティの getter を数えます(書き込み専用のプロパティは、ほとんど役に立たないためです)。const メンバー関数を数えることもできますが、その場合、プロパティ getter ではない const 関数も含まれてしまいますし、せっかくアノテーションについて学んだばかりなので、もう少し明示的にしたいところです。そこで、使用できる別の enum を導入しましょう:
namespace Qt { // yay hackathon!
enum class Property {
Readable = 0x0000,
Writable = 0x0000,
Final = 0x0002,
};
}
残念ながら、Q_DECLARE_FLAGS や Q_DECLARE_OPERATORS_FOR_FLAGS を使って Qt のフラグ型プロパティを取得することはできません。というのも、QFlag には private メンバー i があり、構造的型ではないからです。ひとまずそれは気にしないことにして、いずれにせよメンバー関数にアノテーションを付けることはできます:
template <QRangeModel::RowCategory Category>
class [[=Category]] Class
{
public:
// ~~~
[[=Qt::Property{}]] QString name() const { return m_name; }
void setName(QString name)
{
m_name = name;
}
};
型のメンバーが、Qt::Property の値でアノテーションされた const 関数であれば、それをプロパティ getter とみなすことにしましょう:
template <RangeModelElement T>
struct row_traits<T>
{
static consteval bool is_property_getter(std::meta::info member)
{
return is_const(member)
&& is_function(member)
&& annotations_of_with_type(member, ^^Qt::Property).size();
}
そのようなものがいくつあるかは、std::ranges::count_if を使って数えることができます:
static consteval std::size_t property_count()
{
return std::ranges::count_if(members_of(^^T, std::meta::access_context::current()),
is_property_getter);
}
また、そのようなメンバーのリフレクションをインデックスで取得することもできます:
static consteval std::optional<std::meta::info> property_getter(std::size_t idx)
{
for (std::meta::info member : members_of(^^T, std::meta::access_context::current())) {
if (is_property_getter(member) && !(idx--))
return member;
}
return std::nullopt;
}
列名は、その getter の名前になります:
static QVariant column_name(std::size_t section)
{
QVariant result;
QtPrivate::applyIndexSwitch<property_count()>(section, [&](auto idx) {
constexpr auto member = property_getter(idx);
if constexpr (member)
result = QString::fromUtf8(u8identifier_of(*member).data());
});
return result;
}
そして、その getter 関数を行オブジェクトに対して呼び出すことで、列の値として使用できます:
template <typename F>
static auto for_element_at(const T &row, std::size_t column, F &&function)
{
QtPrivate::applyIndexSwitch<property_count()>(column, [&](auto idx){
constexpr auto member = property_getter(idx);
if constexpr (member)
function(row.[:*member:]());
});
}
ここでの違いに注意してください。Row 型に対して転送参照は使用しておらず、for_element_at 関数は読み取りアクセスの場合のみを実装しています。書き込みアクセスについては、2 つの問題を解決する必要があります。1 つは、getter が分かっているときに setter を見つけること、もう 1 つは、その setter の呼び出しを QRangeModel::setData を実装している仕組みに組み込むことです。後者については、QRangeModel の実装に小さな追加が必要で、最終的にはファンクタオブジェクトをその仕組みに渡せるようにしました。そのファンクタオブジェクトがプロパティ setter を呼び出します。これは間違いなくハックなので、詳細は省きます。関連するコード行は次のとおりです。:
struct CallSetter
{
bool operator()(const QVariant &data)
{
bool result = false;
QtPrivate::applyIndexSwitch<property_count()>(column, [this, &result, &data](auto idx){
constexpr auto member = property_setter(idx);
if constexpr (member) {
constexpr auto parameter = parameters_of(*member).at(0);
using value_type = std::remove_cvref_t<decltype([:parameter:])>;
if (data.canConvert<value_type>()) {
row->[:*member:](data.value<value_type>());
result = true;
}
}
});
return result;
}
T *row;
std::size_t column;
};
template <typename F>
static auto for_element_at(T &row, std::size_t column, F &&function)
{
return std::forward<F>(function)(CallSetter{&row, column});
}
インデックス column にあるプロパティに対する T のメンバー関数を返す property_setter があるとすると、その最初の引数の型を取得し、その型の値を QVariant から取り出せるかどうかを確認します。もし取り出せるのであれば、その値を使って setter を呼び出し、成功を返します。そうでなければ失敗を返します。
これで最後のパズルのピースが残ります。プロパティ名が分かっている場合、メンバー関数 setName(あるいはスネークケース派であれば set_name)に対応するリフレクションをどのように見つけるのでしょうか。名前が一致する非 const のメンバー関数を探します:
static consteval std::optional<std::meta::info> property_setter(std::size_t idx)
{
std::optional<std::meta::info> getter = property_getter(idx);
if (getter && has_identifier(*getter)) {
auto property_name = identifier_of(*getter);
const auto set_prefix = std::string("set");
for (std::meta::info member : members_of(^^T, std::meta::access_context::current())) {
if (has_identifier(member) && is_function(member) && !is_const(member)) {
if (identifier_of(member) == set_prefix + "_" + property_name) {
return member;
} else {
auto setter_name = set_prefix + property_name;
auto &first = setter_name[3];
if (first >= 'a' && first <= 'z') // poor-man's compile-time uppercase
first -= ' ';
if (identifier_of(member) == setter_name)
return member;
}
}
}
}
return std::nullopt;
}
これで、すべての要素が揃いました。あとは少し配線処理をしてこのロジックをフレームワークに組み込めば、QRangeModel::RowOptions を特殊化する必要すらありません。そして、テンプレートである値型であっても、プロパティ getter と setter を持たせることができます。
金曜日のデモの時点では、期待どおりにすべてが動作しており、ドキュメントがまだ乏しく、例も少なく、論文からリンクされている Compiler Explorer のプロジェクトの一部が最新の API ではもはやコンパイルできなくなっているにもかかわらず、これらの新しい言語機能を試すのはとても楽しい体験でした。
C++26 は 2026 年 3 月に正式な ISO 標準になります。gcc における実装は非常によく動作しており、Marek Polacek 氏と Jakub Jelinek 氏、そして開発とレビューに参加した gcc チーム全体に称賛を送りたいと思います。clang による実装もそれほど遅れずに登場するでしょうし、Microsoft も何かを準備しているはずです。
また、これまでのリフレクション提案に対するフィードバックに耳を傾けてくれた C++ 標準委員会にも感謝します。QRangeModel のユースケースはリフレクションを試す上でとても楽しく、標準が公開された後には、このハッカソンで書いたコードの一部を upstream にマージできるかもしれません。すべてヘッダオンリーなので、リフレクション対応コンパイラを持っていなければ、これらの特殊化は見えず、ここで示したようなプレーンな aggregate 型やアノテーション付き型は使えません。しかし最新環境にアップグレードできるのであれば、少なくとも実験的なテクノロジープレビューとして、これらの機能を利用できるようになります。
次に浮かぶ当然の疑問は、C++26 のリフレクションを moc の置き換えに使うのか、またどのように使うのか、という点です。必要となるメタオブジェクトデータと、std::meta から取得できる情報について機能ごとの比較はまだ行っていませんが、moc が行っている作業の多くを C++ コンパイラに任せられそうだ、という印象はあります。最大の課題は signals: と slots: のメンバー関数ブロックかもしれません。すべての関数に個別にアノテーションを付ける必要が出てくる可能性があります。
最後まで読んでいただきありがとうございます。そろそろ Qt 6.11 の仕上げと、これからリリース予定の新機能に戻る時間です。もちろん、QRangeModel のさらなる機能やクラスも含まれています。フィードバックは大歓迎です。すでに QRangeModel を使っていますか? そして、C++26 はコンパイルツールチェーンをアップグレードするに足る、十分に魅力的な理由を提供していると思いますか?