Cursed C++: public_cast

Have you ever needed to use some obscure function only for it to be private? If you were in a higher-level language you could just use reflection, but it’s in a third party library and you can’t modify that.

Or perhaps you want to make your coworkers pull their hair out by varying their invariants?

Enter public_cast. This abomination can see everything except constructors and destructors.

Did I mention it’s 100% legal and standard-compliant?

The ideas behind this come from Logan Smith’s Cursed Casts, and this implementation is heavily based on his.

The entry point

There is one place in all of C++ where access checking is ignored:

The usual access checking rules do not apply to names in a declaration of an explicit instantiation or explicit specialization.

[temp.spec.general]/6
//Compiles

class C
{
private:
    int myVar;
}

template<auto Member>
struct AccessBreaker
{
    static const inline auto val = Member;
};

template struct AccessBreaker<&C::myVar>; //No error

int main()
{
    C c;
    
    //Error: myVar is private
    c.myVar = 3;
    
    //Error: we're no longer declaring an explicit instantiation, we're using one
    auto ptr = AccessBreaker<&C::myVar>::val;
    c.*ptr = 3;
}

The tricky part then becomes getting this data out of the template, to a place where we can access it anywhere. To do this, we can use a triple assignment to exfiltrate a pointer to the private member, to a template that doesn’t explicitly reference that private member.

template<typename Ty, typename Key>
struct MemberAccessor
{
    static inline Ty val;
};

template<auto Member, typename Key>
struct AccessBreaker
{
    static const inline auto val = MemberAccessor<decltype(Member), Key>::val = Member;
};

Now whenever we instantiate AccessBreaker with a private member and a key, we can use MemberAccessor to look up that private member later provided we have its type and the key:

struct _C_myVar {};
template struct AccessBreaker<&C::myVar, _C_myVar>;

int main()
{
    C c;
    auto ptr = MemberAccessor<int C::*, _C_myVar>::val;
    c.*ptr = 3; //No error
}

Minimizing room for error

Unfortunately this is easy to break. If the type changes, we aren’t guaranteed much safety–our plumbing might start implicitly casting or otherwise fail invisibly. We can go a step further and make our accessor easier to read by storing our type when we declare our AccessBreaker, and add some type safety with static asserts.

template<typename Ty, typename Key>
struct MemberAccessor
{
  static inline Ty val;
};

template<typename Key>
struct MemberType {};

template<auto Member, typename Key>
struct AccessBreaker
{
  static const inline auto val = MemberAccessor<decltype(Member), Key>::val = Member;
  static_assert(std::is_same_v<typename MemberType<Key>::ptr_t, decltype(Member)>); //Type safety
};

template<typename Key>
using public_cast = MemberAccessor<typename MemberType<Key>::ptr_t, Key>;


struct _C_myVar {};
template<> struct MemberType<_C_myVar> { using ptr_t = int C::*; };
template struct AccessBreaker<&C::myVar, _C_myVar>;

int main()
{
    C c;
    c.*public_cast<_C_myVar> = 3; //No error
}

Demo on replit

(Note: By omitting public_cast, one can make this unwieldy enough to give you “plenty of time to rethink your life in the mean time”)

Further improvements

Ideally we’d be able to make our AccessBreaker instantiation associate that key with the correct type in MemberType. Unfortunately I was only able to make some small improvements on Logan’s work.

Uses

This same technique will work on functions. Casting to a private inherited class doesn’t even need public_cast: C-style casts ignore inheritance access specifiers. However, C-style casting is dangerous enough that you may want to wrap it and add some asserts to prevent an accidental implicit reinterpret_cast.

With this in hand, you can get access to internals of third-party libraries as long as it’s in the headers. Library maintainers, if you want to avoid this kind of intrusion, you can use the pImpl idiom. And for reflection systems like the one I’m writing for Sanable, this is a godsend.