0. Предисловие
Вдохновлённый статьёй «Когда private, но очень хочется public», я решил исследовать альтернативный способ доступа к приватным членам класса в C++. В отличие от классического подхода с прокси-структурами и частичной специализацией, мой метод использует иерархическую специализацию шаблонов с явной инициализацией указателей, что даёт несколько преимуществ:
-
Единообразие синтаксиса:
Доступ к данным, методам и статическим членам осуществляется через единый интерфейсaccess_member, а не через разрозненные механизмы. -
Прямая инициализация указателей:
Вместо косвенного связывания через traits-классы используется явная регистрация членов черезinit_member, что делает код более прозрачным. -
Расширяемость архитектуры:
Подход естественным образом поддерживает добавление новых категорий членов (например, статических) через дополнительные специализации.
Это не «ещё одна реализация того же самого», а принципиально иная архитектура, где:
-
Нет необходимости в stub-структурах для каждого члена
-
Типы членов явно указываются при инициализации
-
Работа с указателями происходит на этапе компиляции
1. Почему нельзя просто взять указатель на приватное поле?
Рассмотрим простой класс:
class Dog { public: Dog(std::string name) : name(name) {} void printName() const { std::cout << name << std::endl; } private: std::string name; };
Если попытаться получить указатель на name извне:
auto ptr = &Dog::name; // Ошибка: "name" is private
Компилятор запретит это, потому что name — приватное поле. Это описано в стандарте C++ в разделе [class.access] , где указано, что доступ к приватным членам класса (private) разрешён только для методов самого класса и дружественных (friend) сущностей. Любая попытка обращения к private-полю извне класса приводит к ошибке компиляции, так как нарушает правила инкапсуляции.
2. Как шаблоны позволяют обойти ограничение?
Шаблоны в C++ инстанцируются на этапе компиляции, и если специализация шаблона происходит в контексте, где приватный член доступен (например, внутри класса или через friend), то компилятор разрешит взять его адрес.
Шаг 1: Хранилище для указателя
Создадим шаблонный класс, который будет хранить указатель на член класса:
template<typename ClassType, typename MemberType> struct MemberPtrHolder { static MemberType ClassType::* ptr; // Указатель на член класса }; // Инициализация статического указателя template<typename ClassType, typename MemberType> MemberType ClassType::* MemberPtrHolder<ClassType, MemberType>::ptr = nullptr;
Шаг 2: Шаблон для инициализации указателя
Теперь создадим шаблон, который при инстанцировании будет записывать указатель на приватное поле в MemberPtrHolder:
template<typename ClassType, typename MemberType, MemberType ClassType::* Ptr> struct PrivateMemberAccessor { PrivateMemberAccessor() { MemberPtrHolder<ClassType, MemberType>::ptr = Ptr; } static PrivateMemberAccessor instance; // Статический экземпляр }; // Явное определение статического объекта template<typename ClassType, typename MemberType, MemberType ClassType::* Ptr> PrivateMemberAccessor<ClassType, MemberType, Ptr> PrivateMemberAccessor<ClassType, MemberType, Ptr>::instance;
Шаг 3: Специализация для конкретного поля
Теперь мы можем создать специализацию для Dog::name:
template class PrivateMemberAccessor<Dog, std::string, &Dog::name>;
Компилятор видит, что специализация шаблона происходит в области, где Dog::name доступен (т. к. шаблон объявлен в глобальной области, но инстанцируется с корректным доступом).
Теперь мы можем получить доступ к приватному полю:
int main() { Dog dog("Buddy"); dog.printName(); // Выведет: Buddy // Получаем указатель на приватное поле auto name_ptr = MemberPtrHolder<Dog, std::string>::ptr; // Меняем значение через указатель dog.*name_ptr = "Max"; dog.printName(); // Выведет: Max }
Что здесь происходит?
1. PrivateMemberAccessor<Dog, std::string, &Dog::name>::instance инициализируется до вызова функции main()
2. В его конструкторе указатель &Dog::name записывается в MemberPtrHolder<Dog, std::string>::ptr
3. В main() мы получаем этот указатель и меняем значение поля
3. Расширенная функциональность
Добавим функциональность для работы с методами, статическими членами и их соответствующие инициализаторы
template<typename ClassType, typename MemberType> struct access_member { static MemberType ClassType::* ptr; }; template<typename ClassType, typename MemberType> MemberType ClassType::* access_member<ClassType, MemberType>::ptr = nullptr; // Специализация для методов template<typename ClassType, typename RetType, typename... Args> struct access_member<ClassType, RetType(Args...)> { static RetType(ClassType::* ptr)(Args...); }; template<typename ClassType, typename RetType, typename... Args> RetType(ClassType::* access_member<ClassType, RetType(Args...)>::ptr)(Args...) = nullptr; // Специализация для const методов template<typename ClassType, typename RetType, typename... Args> struct access_member<ClassType, RetType(Args...) const> { static RetType(ClassType::* ptr)(Args...) const; }; template<typename ClassType, typename RetType, typename... Args> RetType(ClassType::* access_member<ClassType, RetType(Args...) const>::ptr)(Args...) const = nullptr; // Шаблоны для статических членов template<typename ClassType, typename MemberType> struct access_static_member { static MemberType* ptr; }; template<typename ClassType, typename MemberType> MemberType* access_static_member<ClassType, MemberType>::ptr = nullptr; template<typename ClassType, typename RetType, typename... Args> struct access_static_member<ClassType, RetType(Args...)> { static RetType(*ptr)(Args...); }; template<typename ClassType, typename RetType, typename... Args> RetType(*access_static_member<ClassType, RetType(Args...)>::ptr)(Args...) = nullptr; // ==================== Инициализаторы ==================== // Для членов-данных template<typename ClassType, typename MemberType, MemberType ClassType::* ptr> struct init_member { init_member() { access_member<ClassType, MemberType>::ptr = ptr; } static init_member instance; }; template<typename ClassType, typename MemberType, MemberType ClassType::* ptr> init_member<ClassType, MemberType, ptr> init_member<ClassType, MemberType, ptr>::instance; // Для методов template<typename ClassType, typename RetType, typename... Args, RetType(ClassType::* ptr)(Args...)> struct init_member<ClassType, RetType(Args...), ptr> { init_member() { access_member<ClassType, RetType(Args...)>::ptr = ptr; } static init_member instance; }; template<typename ClassType, typename RetType, typename... Args, RetType(ClassType::* ptr)(Args...)> init_member<ClassType, RetType(Args...), ptr> init_member<ClassType, RetType(Args...), ptr>::instance; // Для const методов template<typename ClassType, typename RetType, typename... Args, RetType(ClassType::* ptr)(Args...) const> struct init_member<ClassType, RetType(Args...) const, ptr> { init_member() { access_member<ClassType, RetType(Args...) const>::ptr = ptr; } static init_member instance; }; template<typename ClassType, typename RetType, typename... Args, RetType(ClassType::* ptr)(Args...) const> init_member<ClassType, RetType(Args...) const, ptr> init_member<ClassType, RetType(Args...) const, ptr>::instance; // Для статических членов template<typename ClassType, typename MemberType, MemberType* ptr> struct init_static_member { init_static_member() { access_static_member<ClassType, MemberType>::ptr = ptr; } static init_static_member instance; }; template<typename ClassType, typename MemberType, MemberType* ptr> init_static_member<ClassType, MemberType, ptr> init_static_member<ClassType, MemberType, ptr>::instance; template<typename ClassType, typename RetType, typename... Args, RetType(*ptr)(Args...)> struct init_static_member<ClassType, RetType(Args...), ptr> { init_static_member() { access_static_member<ClassType, RetType(Args...)>::ptr = ptr; } static init_static_member instance; }; template<typename ClassType, typename RetType, typename... Args, RetType(*ptr)(Args...)> init_static_member<ClassType, RetType(Args...), ptr> init_static_member<ClassType, RetType(Args...), ptr>::instance;
Пример работы:
#include <iostream> #include <string> using namespace std; // ==================== Пример класса ==================== class Dog { public: Dog(string name) : name(name) {} private: string bark() const { return name + ": Woof!"; } void rename(string new_name) { name = new_name; } static string species() { return "Canis familiaris"; } static int legs() { return 4; } string name = ""; int age = 1000; static inline string secret = "Private static!"; }; // ==================== Инициализация указателей ==================== // Члены-данные template struct init_member<Dog, string, &Dog::name>; template struct init_member<Dog, int, &Dog::age>; // Методы template struct init_member<Dog, string() const, &Dog::bark>; template struct init_member<Dog, void(string), &Dog::rename>; // Статические члены template struct init_static_member<Dog, string, &Dog::secret>; template struct init_static_member<Dog, string(), &Dog::species>; template struct init_static_member<Dog, int(), &Dog::legs>; // ==================== Использование ==================== int main() { Dog fido("Fido"); // Доступ к членам-данным fido.*access_member<Dog, string>::ptr = "Rex"; cout << fido.*access_member<Dog, string>::ptr << endl; // Rex cout << fido.*access_member<Dog, int>::ptr << endl; // 1000 fido.*access_member<Dog, int>::ptr = 5; cout << fido.*access_member<Dog, int>::ptr << endl; // 5 // Статическая переменная cout << *access_static_member<Dog, string>::ptr << endl; // Private static! // Вызов методов // Не-const метод (fido.*access_member<Dog, void(string)>::ptr)("Max"); // Const метод cout << (fido.*access_member<Dog, string() const>::ptr)() << endl; // Max: Woof! // Статические методы cout << (*access_static_member<Dog, string()>::ptr)() << endl; // Canis familiaris cout << (*access_static_member<Dog, int()>::ptr)() << endl; // 4 }
Также можно использовать макросы для генерации структур:
namespace private_access { //Расширенная функциональность } // Определения макросов #define EXPOSE_MEMBER(Class, Name) \ template struct private_access::init_member<Class, decltype(Class::Name), &Class::Name>; #define EXPOSE_METHOD(Class, Method, Signature) \ template struct private_access::init_member<Class, Signature, &Class::Method>; #define EXPOSE_STATIC_MEMBER(Class, Name) \ template struct private_access::init_static_member<Class, decltype(Class::Name), &Class::Name>; #define EXPOSE_STATIC_METHOD(Class, Method, Signature) \ template struct private_access::init_static_member<Class, Signature, &Class::Method>; // ==================== Пример класса ==================== /* Класс Dog */ // Для полей EXPOSE_MEMBER(Dog, name); // template struct private_access::init_member<Dog, std::string, &Dog::name>; EXPOSE_MEMBER(Dog, age); // template struct private_access::init_member<Dog, int, &Dog::age>; // Для методов EXPOSE_METHOD(Dog, bark, std::string() const); // template struct private_access::init_member<Dog, std::string() const, &Dog::bark>; EXPOSE_METHOD(Dog, rename, void(std::string)); // template struct private_access::init_member<Dog, void(std::string), &Dog::rename>; // Статические члены EXPOSE_STATIC_MEMBER(Dog, secret); // template struct private_access::init_static_member<Dog, std::string, &Dog::secret>; EXPOSE_STATIC_METHOD(Dog, species, std::string()); // template struct private_access::init_static_member<Dog, std::string(), &Dog::species>; EXPOSE_STATIC_METHOD(Dog, legs, int()); // template struct private_access::init_static_member<Dog, int(), &Dog::legs>; // ==================== Использование ==================== /* Блок main() */
4. Послесловие
Хочу выразить искреннюю признательность автору статьи «Когда private, но очень хочется public» — без его работы моё исследование просто не состоялось бы.
Их идея стала для меня точкой вдохновения, отправной точкой в поисках альтернативного решения.
Спасибо за глубокий анализ и элегантный подход, который заставил меня задуматься: «А можно ли сделать иначе?»
Скрытый текст
Ссылка на GitHub: privablic_2_0
ссылка на оригинал статьи https://habr.com/ru/articles/909246/
Добавить комментарий