Profile Mikhail Zaytsev
GitHub

Public cast or how to break C++ in wonderfully awful ways

One day I learned that access specifiers in C++ are not checked during explicit template instantiation, and it immediately seemed like a way to break encapsulation. So I set out to create public_cast.

Setting the stage

To be more formal with what I'm aiming to achieve, I'm going to provide an ideal API that I would like to see when the project is finished. After some consideration, I decided on something like this:


#include <iostream>

class C {
private:
    int x = 57;
    void f() { std::cout << "secret\n"; }
};

ENABLE_PUBLIC_CAST(C, x);
ENABLE_PUBLIC_CAST(C, f);

int main() {
    auto res = GET_MEMBER_POINTER(C, x);
    auto func = GET_MEMBER_POINTER(C, f);
    C c;
    std::cout << c.*res << '\n';
    (c.*func)();

    return 0;
}

Although it would be nice to be able to get rid of ENABLE_PUBLIC_CAST lines, it does not seem to be possible since we have to instantiate some templates.

Basic implementation

First of all, it is useful to understand why this part of standard is not really a problem. Let's consider the following snippet:


class C {
    int x;
};

template <auto val>
struct ValueStorage {
    static constexpr inline auto value = val;
};

// explicit instantiation
template struct ValueStorage<&C::x>;

Here &C::x is successfully stored in ValueStorage<&C::x>::value, but we cannot access it: to do so, we have to write something like this:


auto ptr = ValueStorage<&C::x>::value;

This is not an explicit instantiation, and the compiler won't be happy with us trying to access private field of a class. So we have to be able to access this value by other means so that we don't have to use &C::x in template parameters.

Well, this should do the trick:


class C {
    int x;
};

template <typename T, typename Tag>
struct IndirectionLayer {
    static inline T value{};
};

template <typename Tag, auto val>
struct ValueStorage {
    static const inline auto value = IndirectionLayer<decltype(val), Tag>::val = val;
};

struct CxTag {};
template struct ValueStorage<CxTag, &C::x>;

int main() {
    auto ptr = IndirectionLayer<int C::*, CxTag>::value;
}

So, this works, and we could call it a day after throwing in some macros to hide these hideous helper templates, but we can do even better than this.

Constexpr workarounds

I have one problem with the existing solution: it only works in runtime. That wouldn't normally be a problem, but member pointer is essentially an offset of the field from the beginning of the struct, so it should be possible to compute in compile time.

If we look carefully at the existing solution, there is only one thing preventing us from being able to public_cast in compile time: the IndirectionLayer. And it makes sense: we mutate some global state from template instantiation when metaprogramming was intended to be stateless. But "intented" and "is" are different, and we can take advantage of it with some black magic.

Without diving too deep into the explanation of the aforementioned dark magic, I will just use it:


namespace public_cast {

template <class C, auto MemberTag>
struct FieldPointer {
    friend constexpr auto getFieldPointer(FieldPointer);
};

template <class C, auto MemberTag, auto ptr>
struct MemberPointerSaver {
    friend constexpr auto getFieldPointer(FieldPointer<C, MemberTag>) { return ptr; }
};

template <class C, auto MemberTag>
constexpr auto MemberPointer = getFieldPointer(FieldPointer<C, MemberTag>{});

}// namespace public_cast

class C {
    int x;
};

template struct public_cast::MemberPointerSaver<C, 1, &C::x>;

int main() {
    constexpr auto ptr = public_cast::MemberPointer<C, 1>;
}

Now we're talking. Functionally this is exactly what we wanted, but it is a bit inconvenient to use. It would be great if we could make the compiler keep track of all the tags we use to get our forbidden pointers. I used non-type template parameters for tags here for a good reason: it's easier to generate different constexpr values than different types. So with some preprocessor magic and a pretty straightforward implementation of constexpr strings we achieve this neat design:


#include <algorithm>
#include <array>
#include <cstddef>
#include <iostream>
#include <string_view>

namespace public_cast {

template <class C, auto MemberTag>
struct FieldPointer {
    friend constexpr auto getFieldPointer(FieldPointer);
};

template <class C, auto MemberTag, auto ptr>
struct MemberPointerSaver {
    friend constexpr auto getFieldPointer(FieldPointer<C, MemberTag>) { return ptr; }
};

template <class C, auto MemberTag>
constexpr auto MemberPointer = getFieldPointer(FieldPointer<C, MemberTag>{});

template <size_t N>
class MemberName {
public:
    explicit constexpr MemberName(std::string_view str): m_name{} {
        size_t len = std::min(N, str.length());
        std::copy_n(str.begin(), len, m_name.begin());
    }

    std::array<char, N> m_name;
};

}// namespace public_cast

#define MEMBER_TAG(member)                                                                                             \
    public_cast::MemberName<sizeof(#member)> {                                                                         \
        #member                                                                                                        \
    }
#define ENABLE_PUBLIC_CAST(classname, member)                                                                          \
    template struct public_cast::MemberPointerSaver<classname, MEMBER_TAG(member), &classname::member>
#define GET_MEMBER_POINTER(classname, member) public_cast::MemberPointer<classname, MEMBER_TAG(member)>

class C {
    int x = 42;
};

ENABLE_PUBLIC_CAST(C, x);

int main() {
    auto ptr = GET_MEMBER_POINTER(C, x);
    C c{};
    std::cout << c.*ptr << '\n';
}

Conclusion

Well, this was quite a journey. I cannot recommend using this in production code, but it is very tempting sometimes. One great use case I found is in unit tests, because it's the only place where it is incredibly convenient and logical to have access to some fields and methods of a class that are off limits to every other part of code. Also, one can say that tests are not in production, so with such a neat interface it might be possible to use this trick in some real-world projects.