cpp26-reflection · part 28

A Q_OBJECT that never names its class

· english· audience: working-cpp· AI-generated, reviewed by Filip Sajdak

In short

  • RQT_OBJECT is written like Q_OBJECT: no class name, no base class template, no registration call.
  • It finds its class by asking for the parent of the current function, from inside member functions defined in the class body.
  • RQT_PROPERTY takes the exact Q_PROPERTY text, turns it into a string and parses it at compile time.
  • A misspelled keyword stops the build with a message.
  • A Qt-free reduction of both macros runs on Compiler Explorer. Only GCC 16.2 was tried.

One error from a macro that does not know its class

error: uncaught exception of type 'std::meta::exception'; 'what()': 'unknown property keyword'

That is GCC 16.2 rejecting a property line with a made-up keyword. The macro it compiles has no idea which class it sits in. The line comes from a Qt-free reduction of two macros in reflect-moc, RQT_OBJECT and RQT_PROPERTY. This post explains how they work.

It follows the post on signals. The library is published at github.com/wrocpp/reflect-moc (experimental: GCC 16.2, Qt 6.10.3, aarch64 Docker only).

What the class looks like

This is the top of Counter in the library’s README.md:

class Counter : public QObject {
  RQT_OBJECT
  RQT_PROPERTY(int value READ value WRITE setValue NOTIFY valueChanged)
  RQT_PROPERTY(QString label READ label WRITE setLabel NOTIFY labelChanged)

There is no class name in the macro, no template base and no bind() call in the constructor. The earlier designs in the project’s spikes/SPIKES.md needed some of those:

  • Spike 06 used a CRTP base that takes the class as a template argument.
  • Spike 07 used a non-template base with deducing this. It needed bind() in the constructor. It also needed two lines after the class. Until those lines were added, pointer-to-member connect did not work, and neither did qobject_cast or qmlRegisterType.

The RQT_OBJECT macro replaced both.

How a macro finds the class it is in

The header include/reflect_moc/qt/macros.hpp defines the class as a function of where the code is:

#define RQT_SELF (::std::meta::parent_of(::std::meta::current_function()))

The object macro expands to member functions defined in the class body. They pass RQT_SELF to templates, for example in static ::QMetaObject const& rqt_meta_() { return ::rqt::meta_of_class<RQT_SELF>(); }. Inside such a function, std::meta::current_function() is that function and its parent is the class. A function body is a complete-class context, so reflection there sees every member, including the ones declared after the macro. The reduction checks this. The object macro defines property_count(), and it counts the properties declared on the lines below it.

The macro also declares what Q_OBJECT declares:

  • staticMetaObject
  • metaObject()
  • qt_metacast
  • qt_metacall
  • tr

That is why Qt’s own checks pass with no opt-in. A test in tests/qt/capability_qt_like_syntax.cpp runs qobject_cast in every direction, a pointer-to-member connect and a functor connect. It runs them against classes that only wrote RQT_OBJECT.

The function std::meta::current_function exists only on GCC 16.2. The README lists it among the things clang-p2996 lacks. So this is a GCC-only technique today.

How RQT_PROPERTY reads the text you typed

#define RQT_PROPERTY(...) \
  static constexpr ::rqt::prop_decl RQT_UNIQUE(rqt_property_) = ::rqt::parse_property(#__VA_ARGS__);

The expression #__VA_ARGS__ turns the macro argument into a string. The parse_property function is consteval and splits that string into words. The words before the first keyword are the type and the name, so a multi-word type such as unsigned long long ticks works. Then each keyword takes its value.

The result is a static constexpr member that the library finds by reflection, the way the reduction’s describe does:

class Sensor, property_count() from inside the class: 2
  property level : int  read=level write=setLevel notify=levelChanged
  property ticks : unsigned long long  read=ticks write= notify=
class Lamp, property_count() from inside the class: 1
  property on : bool  read=on write=setOn notify=

The reduction parses three keywords. The real parser in include/reflect_moc/qt/annotations.hpp also handles these keywords:

  • MEMBER
  • RESET
  • REVISION
  • DESIGNABLE
  • SCRIPTABLE
  • STORED
  • USER
  • CONSTANT
  • FINAL
  • REQUIRED

Anything else throws a std::meta::exception, which GCC prints as a compile error at the macro. Reading the source, FLAVOUR would come out as RQT_PROPERTY: FLAVOUR is not supported. I ran only the reduction’s message, shown at the top. Adding -DBAD to the options on Compiler Explorer reproduces it in a compile-only session.

The reason to parse the text instead of inventing a new syntax is that existing code does not change. The library’s differential test has two files that declare the same classes. tests/qt/differential_moc.h is processed by real moc. tests/qt/differential_rqt.hpp is processed by reflect-moc. I diffed their property lines, and the eight Q_PROPERTY arguments are identical character for character. The README says that test compares the two QMetaObjects. I did not re-run it for this post.

What this has not been tested on

  • GCC 16.2 only. The reduction ran on Compiler Explorer’s GCC 16.2 and on Homebrew GCC 16.2.0. current_function is not in clang-p2996.
  • The reduction has no Qt in it. The real macros ran only in the project’s Docker image (GCC 16.2.0, Qt 6.10.3, aarch64), as the library’s README records. They were not run on other Qt versions, clang or MSVC.
  • CI on a GitHub x86_64 runner passed the core and Qt tests and the tool tests. It also passed the four ported Qt examples and the moc trap (run 37466622213 on main on 6 October 2026). The examples were built with no moc, and their output was identical to the baseline.
  • The series’ sizes, timings and ThreadSanitizer results were measured on aarch64 only. A pass on x86_64 does not show that they carry over. I did not run them myself for this post, and the real parser’s error text is read from its source.

Some Qt features are not covered:

Status Features
rejected BINDABLE; function-valued DESIGNABLE, SCRIPTABLE, STORED and USER
not supported (README, Limits) Q_GADGET, Q_NAMESPACE, Q_PLUGIN_METADATA, QML_ELEMENT

Sources (checked 6 October 2026): reflect-moc (github.com/wrocpp/reflect-moc), read at commit f6827b1 of the local clone before it was published.

The links point at the published commit 4fe8523. It differs from f6827b1 in one file, the Qt README, where it only adds a note on clang-p2996. The files:

Compiler Explorer (g162, GCC 16.2) compiled and ran the reduction twice for the normal build and once with -DBAD. Homebrew GCC 16.2.0 ran it too. All runs were on 6 October 2026.