Skip to content

P2996 conformance: splice access, members_of order, source_location_of, parent_of, type_of [] - #241

Open
XRay3D wants to merge 5 commits into
edgcpp:mainfrom
XRay3D:reflection_conformance
Open

XRay3D wants to merge 5 commits into
edgcpp:mainfrom
XRay3D:reflection_conformance

Conversation

@XRay3D

@XRay3D XRay3D commented Oct 7, 2026

Copy link
Copy Markdown

Five independent fixes that bring the reflection support closer to
P2996R13 and the current working draft. Each fix
is a separate commit with its own test in tests/tests/reflections/ and its
own src/Changes entry.

There is no open issue for these; I can open a Discussion first if that is
preferred.

Commit Fix Wording Tests
b4329a4 Protected members designated by splice-expressions CWG 3109, [class.protected] splice_protected_access, splice_protected_access_neg
e960ef1 members_of in declaration order [meta.reflection.member.queries] members_of_order
f6c1757 source_location_of is a constant for every reflection [meta.reflection.names] source_location_of
9bda256 parent_of / has_parent for namespaces [meta.reflection.scope] parent_of_namespace
63a6c87 type_of of a nonstatic member function [dcl.fct] type_of_member_function

Motivation

These fixes came up while building a reflection-based replacement for Qt's
moc: a CRTP base that builds a QMetaObject from annotated members of the
derived class. With clang-p2996 and GCC the plain code works. EDG needed a
workaround for each item below. Each "Before" is the EDG workaround, and
each "After" is the code that now works.

Pointers to non-public members (splice access, type_of)

The library takes pointers to protected and private slots and properties of
the user's class from outside that class.

Before: &[:M:] was rejected for protected members. std::meta::extract
does not check access, but it needs the exact pointer-to-member type, and
type_of(M) could not be used to build it:

template <typename C, typename T> using data_member = T C::*;
template <typename C, typename R, typename... A>
using member_fn = R (C::*)(A...);
template <typename C, typename R, typename... A>
using const_member_fn = R (C::*)(A...) const;
template <typename C, typename R, typename... A>
using noexcept_member_fn = R (C::*)(A...) noexcept;
template <typename C, typename R, typename... A>
using const_noexcept_member_fn = R (C::*)(A...) const noexcept;

consteval info member_pointer_type(info cls, info m) {
  if (!is_function(m))
    return dealias(substitute(^^data_member, {cls, type_of(m)}));
  std::vector<info> args{cls, return_type_of(m)};
  for (info p : parameters_of(m)) args.push_back(type_of(p));
  info alias = is_const(m) ? (is_noexcept(m) ? ^^const_noexcept_member_fn
                                             : ^^const_member_fn)
                           : (is_noexcept(m) ? ^^noexcept_member_fn
                                             : ^^member_fn);
  return dealias(substitute(alias, args));   // no volatile, no ref-qualifiers
}

template <typename D, info M>
constexpr auto member_pointer =
    extract<typename[:member_pointer_type(^^D, M):]>(M);

After:

template <typename D, info M>
constexpr auto member_pointer = &[:M:];

The function type of a method (type_of)

Qt's SignalData<R(Args...)> and MethodData<R(Args...)> are specialized
on a function type.

Before: typename[:type_of(m):] did not match R(Args...), so the type was
rebuilt:

template <typename R, typename... A> using fn = R(A...);
template <typename R, typename... A> using const_fn = R(A...) const;

consteval info function_type(info m) {
  std::vector<info> args{return_type_of(m)};
  for (info p : parameters_of(m)) args.push_back(type_of(p));
  return dealias(substitute(is_const(m) ? ^^const_fn : ^^fn, args));
}
using F = typename[:function_type(m):];

After:

using F = typename[:type_of(m):];

Declaration order (members_of, source_location_of)

moc lists signals, slots and properties in declaration order. Some signals
are data members and others are member functions.

Before: members_of put all data members first, so every list was re-sorted
by source position:

consteval bool declared_before(info a, info b) {
  auto la = source_location_of(a), lb = source_location_of(b);
  return la.line() != lb.line() ? la.line() < lb.line()
                                : la.column() < lb.column();
}
// ...and an insertion sort with declared_before after every members_of.

Such a helper also has to accept any reflection, which needs
source_location_of to be a constant for all of them.

After: the members_of order is used as is.

Qualified names (parent_of)

moc writes fully qualified class names. Serializers need them too, for XML
element names, a JSON type tag or type names in a schema. They need the plain
identifier of each enclosing namespace, often joined with a separator other
than ::. display_string_of cannot provide that portably: its format is
unspecified, and for app::Counter EDG and GCC print app::Counter while
clang-p2996 prints Counter.

Before: parent_of did not accept a namespace, so the EDG build used
display_string_of and relied on its implementation-defined format:

consteval std::string qualified_name(info r) {
  return std::string(display_string_of(r));
}

After:

consteval std::string qualified_name(info r) {
  std::string s(identifier_of(r));
  for (info p = parent_of(r); has_identifier(p); p = parent_of(p))
    s = std::string(identifier_of(p)) + "::" + s;
  return s;
}

Details

Splice-expressions and protected members

A member designated by a splice-expression is accessible from any point, and
the [class.protected] check does not apply to it (CWG 3109). The ordinary
access check was already skipped for splices. The protected check was still
done for &[:r:], obj.[:r:] and p->[:r:], including in template
instantiations, so these were rejected outside the members and friends of the
class:

class C { protected: int prot = 1; };
constexpr auto r = std::meta::nonstatic_data_members_of(
                       ^^C, std::meta::access_context::unchecked())[0];
int C::*pm = &[:r:];             // previously an error
int f(C &c) { return c.[:r:]; }  // previously an error

The conversion of the object expression to the designating class is still
checked, so e.[:r:] through a private base remains an error.

std::meta::members_of in declaration order

Members used to come out grouped by kind: nonstatic data members first, then
functions, types, variables and templates. They are now sorted by the position
of their declarations. Compiler-generated members come last, after the
user-declared ones. Members without a position, such as those created by
define_aggregate, keep their relative order. Namespace members are sorted
the same way, although the standard leaves their order unspecified.

std::meta::source_location_of

source_location_of has no "Constant When" clause, so it must be a constant
expression for every reflection. Values, types that are neither class nor
enumeration types, the global namespace and data member descriptions now give
a value-initialized std::source_location. Previously these calls were not
constant expressions.

std::meta::parent_of for namespaces

parent_of and has_parent now accept a namespace or namespace alias other
than :: and return the enclosing namespace. Previously parent_of(^^ns) was
not a constant expression and has_parent(^^ns) returned false.

std::meta::type_of of a nonstatic member function

The result is now a plain function type that keeps its cv-qualifiers,
ref-qualifier and noexcept. Previously it still carried the class. As a
result, type_of(^^S::f) == ^^int(int) holds, and the spliced type matches
partial specializations such as X<R(A...)> and X<R(A...) const noexcept>.

Implementation notes

  • Splice access. A new designated_by_splice bit on an_operand is set
    by scan_expr_splicer for nonstatic members and suppresses the check in
    conv_sym_for_member_operand_to_ptr_to_member. Member selections pass
    !locator.is_splicer as do_protected_member_check. Setting
    access_control_error_reported instead would have been shorter, but that
    flag means "an error was already reported" and would mislead later code.
  • members_of. The scope keeps a separate list per entity kind, so the
    collected reflections are sorted afterwards
    (sort_reflections_in_declaration_order). The sort compares
    decl_position with compare_source_positions, puts compiler-generated
    routines last and breaks ties by the original index.
  • source_location_of. The entity kind decides whether there is a
    position. build_source_location_value accepts a null position and builds
    a value-initialized std::source_location.
  • parent_of. parent_of_reflection takes the source correspondence of
    a namespace (iek_scope) or a namespace alias (iek_namespace). The
    global namespace has none, so it still has no parent.
  • type_of. routine_type_without_this_class drops the class and keeps
    the qualifiers. The type is built in the file scope region so that the
    reflection outlives the evaluation. Reflections of types compare with
    identical_types, so a fresh type per call compares equal.

Not addressed here

Found while testing, left for separate changes:

  • members_of lists a member template twice: once as the template and once
    as its prototype routine, variable or type.
  • members_of lists only the implicit special members that are already
    declared. The example in [meta.reflection.member.queries] expects all six.
  • display_string_of drops the cv-qualifiers of a qualified function type:
    void () noexcept for void() const noexcept.

Testing

  • edg-docker-test --non-interactive in the default day posture (debug
    build, configurations edg_x86_64 and edg_x86_64_cp) ran 45,433 tests in
    each configuration. There were no regressions, and the new tests match
    their recorded output.
  • Only cwg/cwg1835 and cwg/cwg2881 differ from their recordings. The
    differences are a symbol name in a linker message and the position of an
    internal error. A run on unmodified upstream/main at 4b6c10b gives the
    same differences, so they are not caused by this branch.
  • On 4b6c10b, every new test fails or differs from its recording.

XRay3D added 5 commits October 8, 2026 00:11
…cess check []

A class member designated by a splice-expression is accessible from any
point ([class.access.base]), and, as clarified by CWG 3109, the additional
protected member access check of [class.protected] does not apply to it
either.  The front end already skipped the ordinary access check for a
splice (is_splicer in the symbol locator), but still did the
[class.protected] check when forming a pointer to member with &[:r:] and
when naming the member with obj.[:r:] or p->[:r:], also in template
instantiations.

An operand made by scan_expr_splicer for a nonstatic member now has the
new designated_by_splice flag, which suppresses the check in
conv_sym_for_member_operand_to_ptr_to_member, and a member selection
through a splice no longer asks cast_pointer_for_field_selection for the
check.  The conversion of the object expression to the designating class
is still checked.

Also: new tests reflections/splice_protected_access.sft.cpp and
reflections/splice_protected_access_neg.sft.cpp, and a src/Changes entry.
…std::meta::members_of []

std::meta::members_of collected the nonstatic data members of a class first
and then the entities listed in the class or namespace scope kind by kind
(functions, types, variables, templates, namespaces), so the members came out
grouped by kind.  P2996R13 ([meta.reflection.member.queries]) requires the
members of a class to appear in the order in which they are declared, with
the implicitly-declared special members after the user-declared ones.

The collected reflections are now sorted by the positions of the
declarations of the members (sort_reflections_in_declaration_order, using
compare_member_declaration_order), with compiler-generated functions last.
Members with the same position, such as those of a class defined by
std::meta::define_aggregate, keep their relative order.  The members of a
namespace are sorted the same way.

Also: a new test (reflections/members_of_order.sft.cpp) and a src/Changes
entry.
As specified in P2996R13 ([meta.reflection.names]), source_location_of
has no "Constant When" clause.  For a reflection of a value, of a type
other than a class or enumeration type (that is not a type alias), of
the global namespace, or of a data member description, it now returns a
value-initialized std::source_location instead of failing; so does any
other reflection without a declaration position.  A cv-qualified class
or enumeration type uses the position of the class or enumeration.

build_source_location_value accepts a null position to build a
value-initialized std::source_location (a null __impl pointer).

Also: a new test (reflections/source_location_of.sft.cpp) and a
src/Changes entry.
parent_of_reflection, which implements std::meta::parent_of and
std::meta::has_parent, did not handle a reflection of a namespace (an
iek_scope entry) or of a namespace alias (an iek_namespace entry), so
parent_of(^^ns) was not a constant expression and has_parent(^^ns) was
false.  P2996R13 ([meta.reflection.scope]) allows any namespace or
namespace alias other than the global namespace; the result is the
enclosing namespace.

The parent is now found from the source correspondence of the namespace
(or alias), as for other namespace members.  The global namespace has no
source correspondence and so still has no parent.

Also: a new test (reflections/parent_of_namespace.sft.cpp) and a
src/Changes entry.
…on type []

For a reflection of a nonstatic member function, type_of returned the
routine type of the function, whose this_class is set.  That type is not
identical to the corresponding function type, so type_of(^^S::f) did not
compare equal to ^^int(int) and the spliced type did not match a partial
specialization such as X<R(A...)>.  The type of a nonstatic member function
is a plain function type ([dcl.fct]), including its cv-qualifiers and
ref-qualifier.

type_of now drops the this_class with routine_type_without_this_class
(which keeps the qualifiers), building the new type in file scope memory so
that the reflection outlives the evaluation.

Also: a new test (reflections/type_of_member_function.sft.cpp) and a
src/Changes entry.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant