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