2025-06-11
I had fallen into a rabbit hole consisting of creating an access policy class for accessing a wrapped class’ methods and members dynamically. This led me to a curious pattern that I’d like to share.
Consider the following:
Function pointers are not relationally comparable in C++. Equality comparisons are supported, except for situations when at least one of the pointers actually points to a virtual member function (in which case the result is unspecified).
In fact, pointers to methods, and pointers to members, cannot be cast to a regular pointer type, or any integer type. And they typically cannot be compared; and to make things worse: consider
struct Base { int from_base; };
struct Derv: Base { int derived; };
/* ... */
int Base::*ptr0 = &Derv::from_base; // ok
int Derv::*ptr1 = &Derv::from_base; // error
Which is quite reasonable, but means we cannot compare those at all. So how do we even attempt to do this? Well a bad suggestion is taking it by value, crazy-casting the local variable’s address to a pointer to an integer value, then dereferencing it. This is undefined, plain and simple. And it’s not the comfy undefined we have in C, this is C++ undefined. So let’s not do this.
My solution, which I think is workable, is based on the following
premise: you will never query for access, or comparison in general, on a
pointer-to-member that you didn’t get as a literal compile time value by
doing a &Class::Member
.
So what’s the trick? As a literal type, it’s completely admissible to pass a compile time value of it as a non-type template parameter, consider the following:
template<auto t> struct FieldID;
template<typename Type, typename Class, Type Class::* Ptr>
struct FieldID<Ptr> {
static constexpr bool is_method = false;
static constexpr std::type_info const& id = typeid(FieldID<Ptr>);
};
template <
typename Ret,
typename Class,
typename... Args,
(Class::*Ptr)(Args...)
Ret > // yeah I know this is awful
struct FieldID<Ptr> {
static constexpr bool is_method = true;
static constexpr std::type_info const& id = typeid(FieldID<Ptr>);
};
This allows you to store the pointers to members and methods in a
homogeneous container such as std::{unordered_}set
by
keeping them as std::type_index
, which wraps the
std::type_info
reference into something that is more
associative-container-friendly.
Crucially, this means that you don’t even have to care about the
issue I raised earlier of how to best compare pointers to the same type
but from different classes, because they are really members of different
classes, one just happens to inherit from the other. We don’t need to
care because it’s all std::type_index
in the end, and the
compiler ensures that different values will instantiate different
templates, and thus have different indices.
The disadvantage is mostly of having to call templated functions and
providing their one parameter, as opposed to merely passing it as an
argument. It is possible support dynamic queries by simply bypassing the
transformation from pointer-to-whatever to type_index
and
just passing type_index
es as arguments.
Crucially, this can be done in a way just to abstract the template parameter passing so that it becomes as small as possible. Basically designing an algebraic structure on the access policy so that you can reuse well-known useful policies and combine them, think intersection, complement etc.
The fact there is no relation between a wrapped type T
and the dynamic type_index
set makes it so it’s very easy
to separate the implementation into reusable units deriving from it, and
the actual wrapper, which depends on a polymorphism-enabled policy and,
say, a shared_ptr
to a T
it guards.
I just wondered if access control at runtime was possible in C++. It turns out it is, if you access it through a wrapper. I may post an experimental reference implementation at some point.
A couple of remarks about leaky abstractions: I’ve been looking further into how inheritence works under the hood in C++, I may write a blog post about it but the upshot of things is: thunks kind of make this system brittle.
When you take the address of a member function, the compiler can
usually just give you what amounts to an actual function pointer; but
when the method is virtual, the base class’ member function isn’t “what
you mean”, so the compiler emits a thunk, a function with a
definite address which delays the dispatch of the call by first looking
up the method on the instance’s v_table
.
Something like
struct A {
virtual ~A() = default;
virtual void foo() {std::cout << "A::foo" << std::endl;}
};
struct B: A {
void foo() override {std::cout << "B::foo" << std::endl;}
};
int main () {
auto ptr1 = &B::foo;
auto ptr2 = &A::foo;
std::cout << *(int*) &ptr1 << " vs. " << *(int*) &ptr2 << std::endl;
return 0;
}
indeed returns the same number (17 in my case, but your
implementation may differ). I might be overlooking something, but my
implementation always returns the same number, even when
B::foo
is also overriden by a third class. This is, I
think, because *::foo
is always going to be resolved by
A
’s v_table
, so it’s always the same thunk
that is used.
Another thing to note is that if you have two base classes inheriting
from A
, accessing the resulting member pointer to
foo
through either base yields the same number. Moreover,
if those base classes virtual
ly inherit from
A
, and the derived has the appropriate “unique final
overrider for ‘virtual void A::foo()
’” (so that it
compiles) then the pointer to member foo
through the
derived class also equals the original pointer.
However, the resulting type_index
s do not match.