| 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> {
vector<info> result;
for (auto m : nonstatic_data_members_of(cls))
if (is_accessible(m))
result.push_back(m);
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_ofWe 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,
std::meta::access_context ctx) {
return std::vector(std::from_range,
members_of(cls, ctx) | std::meta::is_constructor);
}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,
access_contxt ctx) -> bool {
return !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 ctx); consteval 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_contextclass 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
Pbe the program point at whichaccess_context::current()is called.3 Returns: An
access_contextwhosenaming_classis the null reflection and whosescopeis the unique namespace, class, or function whose associated scopeSenclosesPand for which no other scope intervening betweenPandSis the scope associated with a namespace, class, or function.consteval access_context access_context::unprivileged() noexcept;4 Returns: An
access_contextwhosenaming_classis the null reflection and whosescopeis the global namespace.consteval access_context access_context::unchecked() noexcept;5 Returns: An
access_contextwhosenaming_classandscopeare both the null reflection.consteval access_context access_context::via(info cls) const;6 Constant When:
clsrepresents a class type.7 Returns: An
access_contextwhosescopeisthis->scopeand whosenaming_classiscls.
Add a new subsection after [meta.reflection.access.context]:
consteval bool is_accessible(info r, access_context ctx);1 Let
Pbe a program point that occurs in the definition of the entity represented byctx.scope.2 Returns:
- 3 If
ctx.scoperepresents the null reflection, thentrue.- 4 Otherwise, if
rrepresents a member of a classC, thentrueif that class member is accessible atP([class.access.base]) when named in either- 5 Otherwise, if
rrepresents a direct base class relationship between a base classBand a derived classD, thentrueif the base classBofDis accessible atP.- 6 Otherwise,
true.[ Note 1: The definitions of when a class member or base class are accessible from a point
Pdo 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())); // OKconsteval bool has_inaccessible_nonstatic_data_members( info r, access_context ctx);7 Returns:
trueifis_accessible(R, ctx)isfalsefor anyRinmembers_of(r, access_context::unchecked()). Otherwise,false.consteval bool has_inaccessible_bases(info r, access_context ctx);8 Returns:
trueifis_accessible(R, ctx)isfalsefor anyRinbases_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
vectorcontaining reflections of all members-of-representable members of the entity represented byrthat are members-of-reachable from some point in the evaluation context for which the reflectionRsatisfiesis_accessible(R, ctx). IfErepresents a classC, then the vector also contains reflections representing all unnamed bit-fields declared within the member-specification ofCfor which the reflectionRsatisfiesis_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
Cbe the type represented bydealias(type). Avectorcontaining the reflections of all the direct base class relationships, if any, ofCfor which the reflectionRsatisfiesis_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
vectorcontaining each elementeofmembers_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
vectorcontaining each elementeofmembers_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
vectorcontaining each elementeofmembers_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
vectorcontaining each elementeofbases_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
vectorcontaining each elementeofstatic_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
vectorcontaining each elementeofnonstatic_data_members_of(type)such thatis_public(e)istrue, in order.