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