Comparing Pointers-to-Members

José Goudet Alvim

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).

https://stackoverflow.com/a/1765489

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, 
    Ret (Class::*Ptr)(Args...)
> // 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_indexes 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.

But why, José?

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.

(Edit: rev. 2025-06-22)

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 virtually 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_indexs do not match.