Document #: | P3547R0 [Latest] [Status] |
Date: | 2025-01-09 |
Project: | Programming Language C++ |
Audience: |
SG7, LEWG |
Reply-to: |
Dan Katz <dkatz85@bloomberg.net> Ville Voutilainen <ville.voutilainen@gmail.com> |
We propose the addition of a std::meta::access_context
type to [P2996R8] (“Reflection for
C++26”), which models the “context” (i.e., enclosing namespace,
class, or function) from which a class member is accessed.
An object of this type is proposed as a mandatory argument for the
std::meta::members_of
function, thereby allowing users to obtain only those class members that
are accessible from some context.
A “magical” std::meta::unchecked_access
provides an access_context
from
which all entities are accessible, thereby allowing “break glass” access
for use cases that require such superpowers, while also making such uses
easily audited (e.g., during code review) and clearly expressive of
their intent.
A family of additional APIs more fully integrates and harmonizes the
Reflection proposal with C++ access control. Notably, a std::meta::is_accessible
function allows querying whether an entity is accessible from a context.
Additional utilities are proposed to allow libraries to discover whether
a class has inaccessible data members or bases without obtaining
reflections of those members in the first place.
Our proposal incorporates design elements from functions first introduced by [P2996R3], and iterated on through [P2996R6], before being removed from [P2996R7] due to unresolved design elements around access to protected members (which we believe are addressed by this proposal).
The intent of this proposal it to modify P2996 such that the changes proposed here are considered concurrenty with any motion to integrate P2996 into the Working Draft.
The discussion around whether reflection should be allowed to introspect private class members is as old as the discussion around how to bring reflection to C++. Within SG7, each such discussion has affirmed the desire to:
This design direction is incorporated into the [P2996R8] (“Reflection for C++26”) proposal currently targeting C++26.
members_of
“metafunction”
returns a vector
containing
reflections of all members of a class.identifier_of
and type_of
can thereafter be used
to query properties of the reflected class members.The topic was recently revisited at the 2024 Wrocław meeting, during which a more fragmented set of perspectives within the broader WG21 body came into focus.
Though it appears regrettably impossible to make all parties happy (since some perspectives listed above stand in diametric and irreconcilable opposition), we believe that progress can be made to address many of the concerns raised. In particular, we hope for this paper to address the concerns of all but the last group mentioned above.
Consider a simple hypothetical predicate that returns whether a class member is accessible from the calling context.
consteval auto is_accessible(info r) -> bool;
Such a function might be used as follows (using the syntax and semantics of [P2996R8]):
class Cls {
private:
int priv;
};
static_assert(!is_accessible(members_of(^^Cls)[0]));
This looks great! But it falls apart as soon a we try to build a library on top of it.
consteval auto accessible_nsdms_of(info cls) -> vector<info> {
<info> result;
vectorfor (auto m : nonstatic_data_members_of(cls))
if (is_accessible(m))
.push_back(m);
result
return result;
}
class Cls {
private:
int priv;
friend void client();
};
void client() {
static_assert(accessible_nsdms_of(^^Cls).size() > 0); // fails
}
The call to is_accessible(m)
is considered from the context of the function
accessible_nsdms_of
(which has no
privileged access to Cls
), rather
than from the function void client()
(which does).
A different model is possible, which checks access from the “point
where constant evaluation begins” rather than from the point of call
(i.e., as proposed by [P2996R3]). Under this model,
accessibility is checked from void client()
,
since the “outermost” expression under evaluation is the
constant-expression
of the
static_assert-declaration
within that function. Although the assertion would pass under this
model, other cases become strange.
class Cls {
int priv;
friend consteval int fn();
};
consteval int fn() {
return accessible_nsdms_of(^^Cls).size();
}
int main() {
return fn(); // returns 0
}
Even though fn
is a friend of
Cls
, the constant evaluation begins
in main
: The “point of constant
evaluation” model renders the friendship all but useless.
Rather than try to guess where access should be checked from, we propose that it be specified explicitly.
class Cls {
int priv;
friend consteval int fn();
};
consteval int fn() {
return accessible_nsdms_of(^^Cls, std::meta::access_context::current()).size();
}
int main() {
return fn(); // returns 1
}
static_assert(accessible_nsdms_of(^^Cls, std::meta::access_context::current()) == 0);
The access_context::current()
function returns a std::meta::access_context
that represents the namespace, class, or function most nearly enclosing
the callsite. We propose two additional functions for obtaining an
access_context
:
std::meta::access_context::unprivileged()
returns an access_context
representing unprivileged access (i.e., from the global namespace),
andstd::meta::access_context::unchecked()
returns an access_context
from which
all entities are unconditionally accessible.Values of type access_context
always originate from one of these three functions. In particular, there
is no mechanism for forging an
access_context
that represents an
arbitrary (possibly more privileged) context.
members_of
We propose that the std::meta::members_of
interface require a second argument which specifies an access
context.
consteval auto members_of(info cls, access_context ctx) -> vector<info>;
Note that the existing permissive semantics proposed by [P2996R8] can still be achieved through
use of access_context::unchecked()
.
using std::meta::access_context;
class Cls {
private:
static constexpr int priv = 42;
};
constexpr std::meta::info m = members_of(^^Cls, access_context::unchecked())[0];
static_assert(identifier_of(m) == "priv");
static_assert([:m:] == 42);
whereas library functions that respect access are now easy to write and to compose.
consteval auto constructors_of(std::meta::info cls,
::meta::access_context ctx) {
stdreturn std::vector(std::from_range,
(cls, ctx) | std::meta::is_constructor);
members_of}
Better still, writing the function once gives mechanisms for
obtaining public members, accessible members, and all members. We
therefore propose the removal of the
get_public_members
family of
functions that was added in [P2996R7] whose sole purpose was to
provide a more constrained alternative to
members_of
: With this proposal,
these functions are exactly equivalent to calling
members_of
with the access_context::unprivileged()
access context.
Perhaps unsurprisingly, we propose the same changes to
bases_of
and also propose the
removal of get_public_bases
.
When accessing a protected member or base, more complex rules apply: The class whose scope the name is looked up in (i.e., the “naming class”) must also be specified. For instance, given the classes
struct Base {
protected:
int prot;
};
struct Derived : Base {
void fn();
};
the definition of
Derived::fn
is allowed to reference the
prot
-subobject of
Derived
, i.e.,
this->prot = 42;
because the name prot
is “looked
up in the scope” of Derived
. But it
is not permitted to form a pointer-to-member:
auto mptr = &Base::prot;
which requires performing a search for the name
prot
in the scope of
Base
.
The inability to model these semantics is what resulted in the
removal of the access_context
API
from [P2996R7]. To resolve this, we propose a
member function access_context::via(info)
that facilitates the explicit specification of a naming class.
void Derived::fn() {
using std::meta::access_context;
constexpr auto ctx1 = access_context::current();
constexpr auto ctx2 = ctx1.via(^^Derived);
static_assert(nonstatic_data_members_of(^^Base, ctx1).size() == 0);
// "naming class" defaults to 'Base'; 'prot' is inaccessible as 'Base::prot'.
static_assert(nonstatic_data_members_of(^^Base, ctx2).size() == 1);
// OK, "naming class" is 'Derived'; 'prot' is "named via the class" 'Derived'.
}
With this addition, we can fully model access checking in C++. A
sketch of our access_context
class
(as implemented by Bloomberg’s experimental Clang/P2996 fork) is
as follows:
class access_context { consteval access_context(info scope, info naming_class) noexcept : scope{scope}, naming_class{naming_class} { } public: const info scope; // exposition only const info naming_class; // exposition only consteval access_context() noexcept : scope{^^::}, naming_class{} { }; consteval access_context(const access_context &) noexcept = default; consteval access_context(access_context &&) noexcept = default; static consteval access_context current() noexcept { return {__metafunction(detail::__metafn_access_context), {}}; } static consteval access_context unprivileged() noexcept { return access_context{}; } static consteval access_context unchecked() noexcept { return access_context{{}, {}}; } consteval access_context via(info cls) const { if (!is_class_type(cls)) throw "naming class must be a reflection of a class type"; return access_context{scope, cls}; } };
It may be desireable for a library to determine whether a provided
access_context
is sufficiently
privileged to observe the whole object representation of instances of a
given class. This is easily accomplished by composing
members_of
and
bases_of
with
is_accessible
:
consteval auto has_inaccessible_nonstatic_data_members(info cls,
) -> bool {
access_contxt ctxreturn !std::ranges::all_of(members_of(cls, std::meta::access_context::unchecked()),
[=](info r) { return is_accessible(r, ctx); });
}
consteval auto has_inaccessible_bases(info cls, access_contxt ctx) -> bool {
return !std::ranges::all_of(bases_of(cls, std::meta::access_context::unchecked()),
[=](info r) { return is_accessible(r, ctx); });
}
That said, some library authors have asked for this capability to be made available through the standard library such that clients have no need to handle reflections of inaccessible members at all. We therefore propose that the above functions also be augmented to P2996.
Previous proposals ([P3451R0], [P3473R0]) have suggested integrating
access control with Reflection by checking access at splice-time. If
one’s goal is to prevent introspection of inaccessible member
subobjects, then such a change is not enough: [P2996R8] is replete with metafunctions
that, once one has a reflection, can be used to circumvent access
control: value_of
,
object_of
,
template_arguments_of
,
extract
,
define_aggregate
, and
substitute
can all be creatively
applied to circumvent access control to various extents without ever
having to splice the reflection.
This proposal, therefore, presents a stronger notion of access
control by making it straightforward to avoid getting reflections of
inaccessible members in the first place. Like [P3451R0], we propose a “break glass”
mechanism (i.e., access_context::unchecked()
)
for obtaining unconditional access to a member (e.g., to dump a debug
representation of an object to
stdout
), and like that paper, the
mechanism is trivially auditable (e.g.,
grep unchecked_access
). The use of
unchecked_access
is deliberately
loud and unambiguous in its meaning:
It is very difficult to misconstrue what the author of the code
intended, and hard to imagine that they will be surprised to find
inaccessible members in the resulting
vector
.
Revisiting the multitude of perspectives observed within WG21, we believe this proposal should make participants with the following views happy:
We believe that the union of these groups represent an overwhelming majority within WG21 and within the broader C++ community.
All features proposed here are implemented by Bloomberg’s
experimental Clang/P2996 fork.
They are enabled with the -faccess-contexts
flag (or -freflection-latest
).
[ Drafting note: All wording assumes the changes proposed by [P2996R8]. The following affects only the library; no core language changes are required. Our intent is to modify the text of P2996 itself such that the changes proposed here are considered concurrently with any motion to integrate P2996 into the Working Draft. ]
<meta>
synopsisModify the synopsis of <meta>
as follows:
Header
<meta>
synopsis#include <initializer_list> namespace std::meta { using info = decltype(^^::); [...] consteval info template_of(info r); consteval vector<info> template_arguments_of(info r);
// [meta.reflection.access.context], access control context struct access_context { static consteval access_context current() noexcept; static consteval access_context unprivileged() noexcept; static consteval access_context unchecked() noexcept; consteval access_context via(info cls) const; const info scope = ^^::; // exposition only const info naming_class = {}; // exposition only }; // [meta.reflection.access.queries], member accessessibility queries consteval bool is_accessible(info r, access_context ctx); consteval bool has_inaccessible_nonstatic_data_members( info r,); access_context ctxconsteval bool has_inaccessible_bases(info r, access_context ctx);
// [meta.reflection.member.queries], reflection member queries consteval vector<info> members_of(info r
,
access_context ctx
); consteval vector<info> bases_of(info type,
access_context ctx
); consteval vector<info> static_data_members_of(info type,
access_context ctx
); consteval vector<info> nonstatic_data_members_of(info type,
access_context ctx
); consteval vector<info> enumerators_of(info type_enum);consteval vector<info> get_public_members(info type);
consteval vector<info> get_public_bases(info type);
consteval vector<info> get_public_static_data_members(info type);
}consteval vector<info> get_public_nonstatic_data_members(info type);
Add a new subsection after [meta.reflection.queries]:
1 The
access_context
class is a structural type that represents a namespace, class, or function from which queries pertaining to access rules may be performed, as well as the naming class ([class.access.base]), if any.consteval access_context access_context::current() noexcept;
2 Let
P
be the program point at whichaccess_context::current()
is called.3 Returns: An
access_context
whosenaming_class
is the null reflection and whosescope
is the unique namespace, class, or function whose associated scopeS
enclosesP
and for which no other scope intervening betweenP
andS
is the scope associated with a namespace, class, or function.consteval access_context access_context::unprivileged() noexcept;
4 Returns: An
access_context
whosenaming_class
is the null reflection and whosescope
is the global namespace.consteval access_context access_context::unchecked() noexcept;
5 Returns: An
access_context
whosenaming_class
andscope
are both the null reflection.consteval access_context access_context::via(info cls) const;
6 Constant When:
cls
represents a class type.7 Returns: An
access_context
whosescope
isthis->scope
and whosenaming_class
iscls
.
Add a new subsection after [meta.reflection.access.context]:
consteval bool is_accessible(info r, access_context ctx);
1 Let
P
be a program point that occurs in the definition of the entity represented byctx.scope
.2 Returns:
- 3 If
ctx.scope
represents the null reflection, thentrue
.- 4 Otherwise, if
r
represents a member of a classC
, thentrue
if that class member is accessible atP
([class.access.base]) when named in either- 5 Otherwise, if
r
represents a direct base class relationship between a base classB
and a derived classD
, thentrue
if the base classB
ofD
is accessible atP
.- 6 Otherwise,
true
.[ Note 1: The definitions of when a class member or base class are accessible from a point
P
do not consider whether a declaration of that entity is reachable fromP
. — end note ][ Example 1:— end example ]consteval access_context fn() { return access_context::current(); } class Cls { int mem; friend consteval access_context fn(); public: static constexpr auto r = ^^mem; }; static_assert(is_accessible(Cls::r, fn())); // OK
consteval bool has_inaccessible_nonstatic_data_members( info r,); access_context ctx
7 Returns:
true
ifis_accessible(R, ctx)
isfalse
for anyR
inmembers_of(r, access_context::unchecked())
. Otherwise,false
.consteval bool has_inaccessible_bases(info r, access_context ctx);
8 Returns:
true
ifis_accessible(R, ctx)
isfalse
for anyR
inbases_of(r, access_context::unchecked())
. Otherwise,false
.
Modify the signature of
members_of
that precedes paragraph 1
as follows:
consteval vector<info> members_of(info r,
access_context ctx
);
Modify paragraph 4 as follows:
4 Returns: A
vector
containing reflections of all members-of-representable members of the entity represented byr
that are members-of-reachable from some point in the evaluation context for which the reflectionR
satisfiesis_accessible(R, ctx)
. IfE
represents a classC
, then the vector also contains reflections representing all unnamed bit-fields declared within the member-specification ofC
for which the reflectionR
satisfiesis_accessible(R, ctx)
. Class members and unnamed bit-fields are indexed in the order in which they are declared, but the order of namespace members is unspecified. [ Note 1: Base classes are not members. Implicitly-declared special members appear after any user-declared members. — end note ]
Modify the signature of bases_of
that follows paragraph 4 as follows:
consteval vector<info> bases_of(info type
,
access_context ctx
);
Modify paragraph 6 as follows:
5 Returns: Let
C
be the type represented bydealias(type)
. Avector
containing the reflections of all the direct base class relationships, if any, ofC
for which the reflectionR
satisfiesis_accessible(R, ctx)
. The direct base class relationships are indexed in the order in which the corresponding base classes appear in the base-specifier-list ofC
.
Modify the signature of
static_data_members_of
that follows
paragraph 6 as follows:
consteval vector<info> static_data_members_of(info type
,
access_context ctx
);
Modify paragraph 8 as follows:
6 Returns: A
vector
containing each elemente
ofmembers_of(type
, ctx
)
such thatis_variable(e)
istrue
, in order.
Modify the signature of
nonstatic_data_members_of
that
follows paragraph 8 as follows:
consteval vector<info> nonstatic_data_members_of(info type
,
access_context ctx
);
Modify paragraph 10 as follows:
10 Returns: A
vector
containing each elemente
ofmembers_of(type
, ctx
)
such thatis_nonstatic_data_member(e)
istrue
, in order.
Strike everything after paragraph 12 from the section:
consteval vector<info> get_public_members(info type);
11 Constant When:
dealias(type)
represents a complete class type.12 Returns: A
vector
containing each elemente
ofmembers_of(type)
such thatis_public(e)
istrue
, in order.consteval vector<info> get_public_bases(info type);
13 Constant When:
dealias(type)
represents a complete class type.14 Returns: A
vector
containing each elemente
ofbases_of(type)
such thatis_public(e)
istrue
, in order.consteval vector<info> get_public_static_data_members(info type);
15 Constant When:
dealias(type)
represents a complete class type.16 Returns: A
vector
containing each elemente
ofstatic_data_members_of(type)
such thatis_public(e)
istrue
, in order.consteval vector<info> get_public_nonstatic_data_members(info type);
17 Constant When:
dealias(type)
represents a complete class type.18 Returns: A
vector
containing each elemente
ofnonstatic_data_members_of(type)
such thatis_public(e)
istrue
, in order.