Как известно, в 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
Я очень негодую отсутствую в препроцессоре возможности перегрузки макросов хотя бы по числу аргументов и считаю, что этому ничего не препятствует.
Если бы была возможность перегрузки макросов по числу аргументов макросы инициализации полей выглядели еще красивее.