Как известно, в C++ нет средства описания полей класса с контролируемым доступом, как например property в C#. На Хабрахабре уже пробегала статья частично на эту тему, но мне решительно не нравится синтаксис. К тому же очень хотелось иметь возможность обращаться к полям из ран-тайма по имени.
Давайте прикинем, что в итоге нужно получить.
Например поле типа int с именем «x». Нас вполне устроит такая запись:
И дальше в коде мы хотим обращаться к этому полю
Еще иногда хотим сами контролировать установку и получение значения из этого поля, поэтому придется еще и геттеры и сеттеры написать.
А так же надо не забыть про возможность инициализации полей.
Что нужно знать в ран-тайме о полях? Как минимум их имена и значения. И еще не плохо было бы знать тип.
Это далеко не полная реализация класса описывающего тип. На самом деле можно и нужно еще много всего дописать, но для решаемой задачи это не является самым главным, а имени и размера вполне достаточно. Возможно напишу отдельную статью посвященную описанию типа.
Кажется все более менее просто, смущает только статический метод. Дело в том, что синтаксис не позволяет инстанцировать шаблонный конструктор, передав аргументы шаблона в треугольных скобках.
Пример
Сам класс Bar не является шаблонным, однако имеет шаблонный конструктор по-умолчанию. Значит для вызова этого конструктора его надо инстанцировать. Напрашивается вот такой код:
Но такая запись означает инстанцирование шаблонного класса, а не шаблонного конструктора.
Обойти это иногда можно и дальше я покажу как.
Таким образом Type::fromNativeType<>() это в некотором смысле тоже конструктор.
Поскольку мы хотим обращаться к полям по их именам из ран-тайма — нам придется их хранить каким-то образом. Я выбрал следующий вариант: создаем базовый класс, от которого наследуются все остальные. Этот класс содержит хранилище информации о полях и методы доступа к ней.
Для хранилища лучше использовать наверное std::map, для примера подойдет std::vector.
FieldDeclaration это просто структура содержащая информацию о типе.
Разумеется вся это система написана не с первого раза, а самая основная его часть вообще много раз модифицировался в следствие того, что некоторые пути решения задачи приводили в тупик.
Поэтому я буду вставлять только фрагменты кода, которые вместе собираются в общую картину.
В начале статьи мы условились, что будем использовать синтаксис описания полей, принимающий 2 аргумента: тип и имя поля. На самом деле я сделал разделение двух видов полей:
Первые две строчки макроса smartfield декларируют геттер и сеттер соответствующего поля прямо в классе, где будет располагаться поле. Затем надо обязательно написать их реализацию. Они будут называться getter_<имя поля> и setter_<имя поля> соответственно.
Модификатор соглашения вызова __stdcall позволяет вызывать метод класса по указателю передав this явно в качестве первого параметра (соглашение __thiscall по спецификации Microsoft используемое по-умолчанию использует регистр ECX для передачи this).
__FIELD_CLASS_DECLARATION__ и __FIELD_CLASS_DECLARATION_SMART__ это описание классов соответствующих полей («классы внутренней кухни» к ним мы еще вернемся).
__CLASS_NAME__(name) name; это собственно экземпляр «классов внутренней кухни».
Следует заметить, что «классы внутренней кухни» являются потомками более общего класса Field
Итак, у нас есть шаблонный класс Field, шаблон которого требует указания типа поля.
Класс хранить в себе:
Обратите внимание, типы TGetter и TSetter написаны таким образом, что функции, которые они описывают, принимают в качестве первого параметра указатель void*. На самом деле это указатель that. Это работает потому что геттер и сеттер явно помечены модификатором __stdcall.
Теперь конструкторы. Они шаблонные, шаблон параметризуется типов класса владельца OwnerType, то есть класса, в котором поле объявляется. Сам конструктор принимает указатель this класса OwnerType и сохраняет в that. Кстати, как я уже говорил нельзя явно параметризовать конструктор, но у шаблонов есть интересная особенность: если есть возможность вывести тип которым надо параметризовать шаблон автоматически, то так и происходит. В данном случае это та самая ситуация. При передаче this в конструктор компилятор сам подставить тип OwnerType.
Аргумент nm принимает символьное имя поля. Оно создается оператором стрингификации (см. выше __STRINGIZE__) из более высоких макросов.
По-умолчанию инициализируем геттер и сеттер нулевыми значениями, чтоб знать что их не надо вызывать. Если геттер и сеттер присутствуют они будут заданы отдельно в классах наследниках.
Отличие второго конструктора от первого в том, что он принимает значение поля по-умолчанию, т.к. это довольно часто используется.
Далее идут дефолтные геттер и сеттер. Они проверяют наличие геттера/сеттера заданных программистом и если они заданы — вызывают их с явной передачей that первым параметром. В противном случае они просто возвращают значение / присваивают новое.
Оператор присвоения и оператор приведения к типу нужны просто для синтаксически более удобного доступа к значению поля.
Эти классы будут подставляться прямо в класс-владелец. Для унификации имени этих классов используется макрос __CLASS_NAME__ (см. выше). Они все являются наследниками уже рассмотренного класса Field.
Хорошей практикой является возвращение оператором присвоения ссылки на себя же, это позволяет писать каскадные присвоения.
Вся разница между ними в конструкторах.
Цифры 1 и 2 различают конструкторы с инициализацией значения поля (2) и без (1). Слово SMART указывает на наличие геттера и сеттера.
Все конструкторы так же шаблонные (тип необходимо сохранить и передать в конструктор Field) и точно так же используют автоматическую подстановку OwnerType. Вызывается соответствующий конструктор Field и в него передается кроме this и значения инициализации(если оно есть) еще и имя поля строкой const char [], полученной макросом __STRINGIZE__.
Далее в SMART конструкторах идет получение и сохранение указателей на геттер и сеттер. Работает это весьма странно. Дело в том, что С++ строго относится к приведению типов указателей на методы классов. Это связано с тем, что с учетом возможности наследования и виртуальных методов не всегда указатель на метод может быть выражен так же как указатель на функцию. Однако мы то знаем, что указатели на наш геттер и сеттер могут быть выражены например типом void*.
Создаем временные переменные, которые будут хранить указатели на методы такими какими их отдает компилятор С++. Я написал тип auto, на самом деле можно было написать явно, но так ведь удобнее и спасибо С++0x за это.
Далее получаем указатели на эти временные переменные. Эти указатели приводим к типу void**. Затем разыменовываем и получаем void*. Ну и в конце приводим уже к TGetter или TSetter типам и сохраняем.
Так как для нормальной работы полю нужен указатель this, то все поля необходимо инициализировать. Поэтому неплохо бы написать небольшие макросы, которые позволят это делать удобно.
Первый для инициализации значением, в��орой для простой инициализации.
Вот и всё!
Итак, мы получили такой инструмент как поля класса с возможностью обращения по имени из ран-тайма и возможностью задания сеттеров и геттеров с достаточно простым синтаксисом. Я не утверждаю, что это самое лучшее решение поставленной задачи, наоборот у меня есть идеи как это можно было бы улучшить.
Из минусов отмечу невозможность создания статических полей (пока) и необходимость использования двух разных слов для инициализации полей с и без значения по-умолчанию.
Исходники
PS
Все написанное здесь родилось исключительно из любви к C++.
Разумеется в работе я такого никогда не напишу и другим не советую, потому что код читается довольно таки сложно.
PS2
Я очень негодую отсутствую в препроцессоре возможности перегрузки макросов хотя бы по числу аргументов и считаю, что этому ничего не препятствует.
Если бы была возможность перегрузки макросов по числу аргументов макросы инициализации полей выглядели еще красивее.
Хочешь решить задачу — постарайся сперва узнать ответ
Давайте прикинем, что в итоге нужно получить.
Например поле типа int с именем «x». Нас вполне устроит такая запись:
field(int,x);
И дальше в коде мы хотим обращаться к этому полю
foo.x = 10; int t = foo.x; foo.setField("x", 15); int p = foo.getField("x");
Еще иногда хотим сами контролировать установку и получение значения из этого поля, поэтому придется еще и геттеры и сеттеры написать.
А так же надо не забыть про возможность инициализации полей.
С чего начать
Что нужно знать в ран-тайме о полях? Как минимум их имена и значения. И еще не плохо было бы знать тип.
Тип
class Type { public: const std::string name; const size_t size; template <typename T> static Type fromNativeType() { return Type(typeid(T).name(), sizeof(T)); } Type(const char * name, size_t size) : size(size), name(name) { } Type(const Type & another) : size(another.size), name(another.name) { } };
Это далеко не полная реализация класса описывающего тип. На самом деле можно и нужно еще много всего дописать, но для решаемой задачи это не является самым главным, а имени и размера вполне достаточно. Возможно напишу отдельную статью посвященную описанию типа.
Кажется все более менее просто, смущает только статический метод. Дело в том, что синтаксис не позволяет инстанцировать шаблонный конструктор, передав аргументы шаблона в треугольных скобках.
Пример
class Bar { public: template <int val> Bar() { int var = val; printf("%d\n", var); } };
Сам класс Bar не является шаблонным, однако имеет шаблонный конструктор по-умолчанию. Значит для вызова этого конструктора его надо инстанцировать. Напрашивается вот такой код:
Bar bar = Bar<10>();
Но такая запись означает инстанцирование шаблонного класса, а не шаблонного конструктора.
Обойти это иногда можно и дальше я покажу как.
Таким образом Type::fromNativeType<>() это в некотором смысле тоже конструктор.
Хранение полей
Поскольку мы хотим обращаться к полям по их именам из ран-тайма — нам придется их хранить каким-то образом. Я выбрал следующий вариант: создаем базовый класс, от которого наследуются все остальные. Этот класс содержит хранилище информации о полях и методы доступа к ней.
class Basic { std::vector<FieldDeclaration> fields; public: template <typename FieldType> FieldType getField(const std::string & name, FieldType default) { for(int i = 0; i < fields.size(); ++i) { if (fields[i].name.compare(name)==0) { return static_cast< Field<FieldType>* >(fields[i].pointer)->getValue(); } } return default; } template <typename FieldType> void setField(const std::string & name, FieldType value) { for(int i = 0; i < fields.size(); ++i) { if (fields[i].name.compare(name)==0) { static_cast< Field<FieldType>* >(fields[i].pointer)->setValue(value); } } } };
Для хранилища лучше использовать наверное std::map, для примера подойдет std::vector.
FieldDeclaration это просто структура содержащая информацию о типе.
struct FieldDeclaration { FieldDeclaration(const std::string & name, const Type & type, void * pointer = NULL) : name(name), type(type), pointer(pointer) { } const std::string name; const Type type; void * pointer; };
Волшебная магия
Разумеется вся это система написана не с первого раза, а самая основная его часть вообще много раз модифицировался в следствие того, что некоторые пути решения задачи приводили в тупик.
Поэтому я буду вставлять только фрагменты кода, которые вместе собираются в общую картину.
Некоторые используемые понятия
#define __CONCAT__(a,b) a##b #define __STRINGIZE__(name) #name #define __CLASS_NAME__(name) __CONCAT__(__field_class__, name) #define __GETTER_NAME__(fieldname) __CONCAT__(getterof_, fieldname) #define __SETTER_NAME__(fieldname) __CONCAT__(setterof_, fieldname)
Псевдо-ключевое слово
В начале статьи мы условились, что будем использовать синтаксис описания полей, принимающий 2 аргумента: тип и имя поля. На самом деле я сделал разделение двух видов полей:
- smartfield — поддерживает геттер и сеттер и может быть получено по имени из ран-тайма
- field — не использует геттер и сеттер
#define smartfield(type,name) \ type __stdcall __GETTER_NAME__(name)(); \ void __stdcall __SETTER_NAME__(name)(type value); \ __FIELD_CLASS_DECLARATION_SMART__(type,name) \ __CLASS_NAME__(name) name; #define field(type, name) \ __FIELD_CLASS_DECLARATION__(type,name) \ __CLASS_NAME__(name) name;
Первые две строчки макроса smartfield декларируют геттер и сеттер соответствующего поля прямо в классе, где будет располагаться поле. Затем надо обязательно написать их реализацию. Они будут называться getter_<имя поля> и setter_<имя поля> соответственно.
Модификатор соглашения вызова __stdcall позволяет вызывать метод класса по указателю передав this явно в качестве первого параметра (соглашение __thiscall по спецификации Microsoft используемое по-умолчанию использует регистр ECX для передачи this).
__FIELD_CLASS_DECLARATION__ и __FIELD_CLASS_DECLARATION_SMART__ это описание классов соответствующих полей («классы внутренней кухни» к ним мы еще вернемся).
__CLASS_NAME__(name) name; это собственно экземпляр «классов внутренней кухни».
class Field
Следует заметить, что «классы внутренней кухни» являются потомками более общего класса Field
#define NO_GETTER (TGetter)0 #define NO_SETTER (TSetter)0 template <typename FieldType> class Field { protected: typedef FieldType (*TGetter)(void *); typedef void (*TSetter)(void *, FieldType); TGetter getter; TSetter setter; void * that; public: const std::string name; const Type type; FieldType value; template< typename OwnerType > Field(OwnerType * _this, const char * nm) : name( nm ), type( Type::fromNativeType<FieldType>() ), getter(NO_GETTER), setter(NO_SETTER), that(_this) { _this->fields.push_back(FieldDeclaration(name, type, this)); } template< typename OwnerType > Field(OwnerType * _this, const char * nm, const FieldType & initvalue) : name( nm ), type( Type::fromNativeType<FieldType>() ), value(initvalue), getter(NO_GETTER), setter(NO_SETTER), that(_this) { _this->fields.push_back(FieldDeclaration(name, type, this)); } FieldType getValue() { if (getter) return getter(that); else return value; } void setValue(FieldType val) { if (setter) setter(that,val); else value = val; } Field<FieldType> & operator = (FieldType val) { setValue(val); return *this; } operator FieldType() { return getValue(); } };
Итак, у нас есть шаблонный класс Field, шаблон которого требует указания типа поля.
Класс хранить в себе:
- Имя поля
- Информацию о типе поля
- Значение
- Геттер
- Сеттер
- Указатель that равный this в классе-владельце
Обратите внимание, типы TGetter и TSetter написаны таким образом, что функции, которые они описывают, принимают в качестве первого параметра указатель void*. На самом деле это указатель that. Это работает потому что геттер и сеттер явно помечены модификатором __stdcall.
Теперь конструкторы. Они шаблонные, шаблон параметризуется типов класса владельца OwnerType, то есть класса, в котором поле объявляется. Сам конструктор принимает указатель this класса OwnerType и сохраняет в that. Кстати, как я уже говорил нельзя явно параметризовать конструктор, но у шаблонов есть интересная особенность: если есть возможность вывести тип которым надо параметризовать шаблон автоматически, то так и происходит. В данном случае это та самая ситуация. При передаче this в конструктор компилятор сам подставить тип OwnerType.
Аргумент nm принимает символьное имя поля. Оно создается оператором стрингификации (см. выше __STRINGIZE__) из более высоких макросов.
По-умолчанию инициализируем геттер и сеттер нулевыми значениями, чтоб знать что их не надо вызывать. Если геттер и сеттер присутствуют они будут заданы отдельно в классах наследниках.
Отличие второго конструктора от первого в том, что он принимает значение поля по-умолчанию, т.к. это довольно часто используется.
Далее идут дефолтные геттер и сеттер. Они проверяют наличие геттера/сеттера заданных программистом и если они заданы — вызывают их с явной передачей that первым параметром. В противном случае они просто возвращают значение / присваивают новое.
Оператор присвоения и оператор приведения к типу нужны просто для синтаксически более удобного доступа к значению поля.
Классы внутренней кухни
#define __FIELD_CLASS_DECLARATION__(type, name) \ class __CLASS_NAME__(name) : public Field<type> \ { \ public: \ __FIELD_CLASS_CONSTRUCTOR_1__(type,name) \ __FIELD_CLASS_CONSTRUCTOR_2__(type,name) \ __CLASS_NAME__(name) & operator = (type val) \ { \ Field<type>::operator=(val); \ return *this; \ } \ }; #define __FIELD_CLASS_DECLARATION_SMART__(type, name) \ class __CLASS_NAME__(name) : public Field<type>\ { \ public: \ __FIELD_CLASS_CONSTRUCTOR_1_SMART__(type,name) \ __FIELD_CLASS_CONSTRUCTOR_2_SMART__(type,name) \ __CLASS_NAME__(name) & operator = (type val) \ { \ Field<type>::operator=(val); \ return *this; \ }\ };
Эти классы будут подставляться прямо в класс-владелец. Для унификации имени этих классов используется макрос __CLASS_NAME__ (см. выше). Они все являются наследниками уже рассмотренного класса Field.
Хорошей практикой является возвращение оператором присвоения ссылки на себя же, это позволяет писать каскадные присвоения.
Вся разница между ними в конструкторах.
О конструкторах этих классов
#define __FIELD_CLASS_CONSTRUCTOR_1_SMART__(type,name) \ template< class OwnerType > \ __CLASS_NAME__(name)(OwnerType * _this) \ : Field<type>(_this, __STRINGIZE__(name)) \ { \ auto get_ptr = &OwnerType::__GETTER_NAME__(name); \ auto set_ptr = &OwnerType::__SETTER_NAME__(name); \ this->getter = (TGetter)(void*)*(void**)(&get_ptr); \ this->setter = (TSetter)(void*)*(void**)(&set_ptr); \ } #define __FIELD_CLASS_CONSTRUCTOR_2_SMART__(type,name) \ template< class OwnerType > \ __CLASS_NAME__(name)(OwnerType * _this, type initvalue) \ : Field<type>(_this, __STRINGIZE__(name), initvalue) \ { \ auto get_ptr = &OwnerType::__GETTER_NAME__(name); \ auto set_ptr = &OwnerType::__SETTER_NAME__(name); \ this->getter = (TGetter)(void*)*(void**)(&get_ptr); \ this->setter = (TSetter)(void*)*(void**)(&set_ptr); \ } #define __FIELD_CLASS_CONSTRUCTOR_1__(type,name) \ template< class OwnerType > \ __CLASS_NAME__(name)(OwnerType * _this) \ : Field<type>(_this, __STRINGIZE__(name)) \ { \ } #define __FIELD_CLASS_CONSTRUCTOR_2__(type,name) \ template< class OwnerType > \ __CLASS_NAME__(name)(OwnerType * _this, type initvalue) \ : Field<type>(_this, __STRINGIZE__(name), initvalue) \ { \ }
Цифры 1 и 2 различают конструкторы с инициализацией значения поля (2) и без (1). Слово SMART указывает на наличие геттера и сеттера.
Все конструкторы так же шаблонные (тип необходимо сохранить и передать в конструктор Field) и точно так же используют автоматическую подстановку OwnerType. Вызывается соответствующий конструктор Field и в него передается кроме this и значения инициализации(если оно есть) еще и имя поля строкой const char [], полученной макросом __STRINGIZE__.
Далее в SMART конструкторах идет получение и сохранение указателей на геттер и сеттер. Работает это весьма странно. Дело в том, что С++ строго относится к приведению типов указателей на методы классов. Это связано с тем, что с учетом возможности наследования и виртуальных методов не всегда указатель на метод может быть выражен так же как указатель на функцию. Однако мы то знаем, что указатели на наш геттер и сеттер могут быть выражены например типом void*.
Создаем временные переменные, которые будут хранить указатели на методы такими какими их отдает компилятор С++. Я написал тип auto, на самом деле можно было написать явно, но так ведь удобнее и спасибо С++0x за это.
Далее получаем указатели на эти временные переменные. Эти указатели приводим к типу void**. Затем разыменовываем и получаем void*. Ну и в конце приводим уже к TGetter или TSetter типам и сохраняем.
Последний штрих
Так как для нормальной работы полю нужен указатель this, то все поля необходимо инициализировать. Поэтому неплохо бы написать небольшие макросы, которые позволят это делать удобно.
#define initfieldval(name, value) name(this, value) #define initfield(name) name(this)
Первый для инициализации значением, в��орой для простой инициализации.
Вот и всё!
Использование
#include "basic.h" class Foo : public Basic { public: smartfield(int, i); field(float, f); Foo(); }; Foo::Foo() : initfield(i), initfieldval(f, 3.14) { } int Foo::getterof_i() { printf("Getting field i of class Foo\n"); return i.value; } void Foo::setterof_i(int value) { printf("Setting field i of class Foo\n"); i.value = value; } int main() { Foo foo; int j = foo.i; foo.setField("i", 10); int k = foo.getField("i", -1); float z = foo.f; return 0; }
Заключение
Итак, мы получили такой инструмент как поля класса с возможностью обращения по имени из ран-тайма и возможностью задания сеттеров и геттеров с достаточно простым синтаксисом. Я не утверждаю, что это самое лучшее решение поставленной задачи, наоборот у меня есть идеи как это можно было бы улучшить.
Из минусов отмечу невозможность создания статических полей (пока) и необходимость использования двух разных слов для инициализации полей с и без значения по-умолчанию.
Исходники
PS
Все написанное здесь родилось исключительно из любви к C++.
Разумеется в работе я такого никогда не напишу и другим не советую, потому что код читается довольно таки сложно.
PS2
Я очень негодую отсутствую в препроцессоре возможности перегрузки макросов хотя бы по числу аргументов и считаю, что этому ничего не препятствует.
Если бы была возможность перегрузки макросов по числу аргументов макросы инициализации полей выглядели еще красивее.