This Time at Runtime!
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);
}
};
}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.
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_castand 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.