Comparing Pointers-to-Members (Again)

This Time at Runtime!

José Goudet Alvim

2026-04-03

In a previous post I spoke of comparing pointers-to-members and it was quite the shitshow as per usual. I was revisiting the concept and a couple of things strike me as quite bad. Firstly we are locked into compile-time necessarily by the nature of passing the pointer as a non-type template parameter. We also aren’t really punning anything. I think I came up with a better solution this time around.

#include <vector>
#include <memory>
#include <functional>
#include <utility>

template <typename Class, typename Type>
class PointerToMember;
template <typename Class, typename Type>
Type const& operator->*(Class const&, PointerToMember<Class, Type> const&);
template <typename Class, typename Type>
Type      & operator->*(Class      &, PointerToMember<Class, Type> const&);

template <typename Class, typename Type>
class PointerToMember {
    friend struct std::hash<PointerToMember>;

    std::function<Type const*(Class const*)> m_caster;
    void const* m_punned {nullptr};

    template <typename T>
    static void const* pun(T Class::* mptr) {
        static std::vector<std::unique_ptr<T Class::* const>> ptrs;
        void const *punned = nullptr;
        for (auto it = ptrs.begin(); !punned && it != ptrs.end(); ++it)
            punned = (**it == mptr)? it->get(): nullptr;
        if (punned == nullptr) {
            ptrs.emplace_back(std::make_unique<T Class::* const>(mptr));
            punned = ptrs.back().get();
        }
        return punned;
    }

    public:
    template <typename T>
    PointerToMember(T Class::* mptr)
        : m_caster([mptr](Class const* cls){
            return dynamic_cast<Type const*>(&(cls->*mptr));
        })
        , m_punned(pun(mptr))
    {
        ;
    }

    auto operator<=>(PointerToMember const& rhs) const noexcept {
        return m_punned <=> rhs.m_punned;
    }

	friend Type const& operator->*(Class const& l, PointerToMember const& r) {
		return *r.m_caster(&l);
	}

	friend Type      & operator->*(Class      & l, PointerToMember const& r) {
		return const_cast<Type&>(std::as_const(l)->*(r));
	}
};

namespace std {
template <typename Class, typename Type>
struct hash<::PointerToMember<Class, Type>> {
	size_t operator()(PointerToMember<Class, Type> const& value) const noexcept {
		return std::hash<void*>{}(value.m_punned);
	}
};
}

Explanation / Code Tour

The defined templated class depends on the Class you intend on taking pointers to members, and the Type of said members. Importantly you may construct them for any pointer-to-member of a type that is dynamic_cast-able to Type (see remarks for me shitting on this idea).

At creation time, we have the pointer-to-member type, we use this static information to register the pointer in a list of like-pointer which we can compare with equality. We perform a linear search to see if we haven’t already created one such pointer, adding an entry if we haven’t. Consequently we have a stable pointer to a punnable type (see remarks about better ways of doing this) which we use to identify the pointer.

We also create and store a caster (see remarks again) which knows how to convert, from the variable type we gave at construction, to the static type that defines the class. We use this caster to perform the access given a pointer to the class of interest.

The key idea here is that we can pay an upfront cost at construction time for the privilege of having trivial hash and comparison functions, effectively giving pointers to members a lot more capabilities than we are used to having. The cost analysis in terms of program size and complexity are left for the remarks to loosely discuss.

I also experimented, successfully, with overloading operator->* for a more convenient access method.

Some Remarks

Dynamic casting anything to anything else may be a little too loose. Using a static_cast would probably not suffice for the waking nightmare that is multiple and/or virtual inheritance. A compromise would allow for the specification of a custom caster, or, indeed, any accessor. The central issue here would be that this fundamentally fucks equality etc. because there is no way we can maintain a consistent punning across all sorts of instantiations. We already shit the bed with convertibility in that we fail a couple of tests:

Pointer to data member of an accessible unambiguous non-virtual base class can be implicitly converted to pointer to the same data member of a derived class.

Conversion in the opposite direction, from a pointer to data member of a derived class to a pointer to data member of an unambiguous non-virtual base class, is allowed with static_cast and explicit cast, even if the base class does not have that member (but the most-derived class does, when the pointer is used for access).

Cppreference 2026-04-03

I’m not even sure how much of this isn’t UB, it feels like virtual base classes cause a world of pain in terms of casting but compiling the my test with -fsanitize=undefined on g++ 14.2.0 (I know, Debian…) does not cause any error or trigger any traps and produces the expected results.

We could try to add conversion between different PointerToMember types in the way described above. I’m pretty sure it should work but we have to guard against virtual classes and at the present moment I just can’t be bother to find the right incantations.

As you can see, there is a linear search during object construction which happens when you’re not copy-constructing or move-constructing. It shouldn’t need to happen but C++ is a wonderful language and, as a result, we don’t have \(O(1)\) lookups for pointers-to-members because of technical limitations I mentioned in the previous instalment of this fever dream.

There is also a memory footprint related to keeping those pointers in the heap / static storage for the duration of the program. Unless you’re planning on opening an unbounded amount of shared libraries, the size of this footprint is bounded above by the number of based and derived classes of Class in your program, I’m pretty sure.

A strictly better performance-wise way of implementing this punning would be storing a pointer to the std::vectors as void* (or even a std::type_index) which will uniquely identify the type of the pointer in question, and then also storing an integer indexing into the array. This ensures no indirection happens at due to a std::unique_ptr, and makes it so relocation of the vector buffer is harmless since indices are stable even when pointers aren’t. This also makes for a much quicker access because now the T Class::* pointers are contiguous in memory.

The issue with this approach is that it fucks with the implementation of std::hash. As another shameless self-plug, you could use a function like SillyHash to implement hash combination. Although there are certainly more efficient ways of doing it, this one is mine. Typically you’ll want to avoid what I went and did and basically not use % (although in my defense any compiler worth its salt can do % (2 << k) efficiently).

Regarding storing the casters, we by necessity must instantiate the lambda every time we create an object of our type. We can avoid duplication in a couple of ways: we can store a single lambda object keyed by the punned void* in some static duration map and store only a pointer to the std::function; we can store a function pointer directly and have it take some opaque value that holds the underlying pointer-to-member, such as a std::any (we can’t store a function pointer as is, because the mptr is captured, and the only way not to capture it at the lambda level is to store it out of band in some opaque object).

I believe there is no way to avoid some degree of indirection (ie. either an opaque storage for T Class::* or a std::function::operator() call.

License

Should you want to use or adapt this idea, it’s licensed under GPLv3 with other licensing agreements potentially available through direct contact with myself.