Pull to refresh

Поля класса доступные по имени с setter и getter в C++

Reading time 9 min
Views 25K
Как известно, в C++ нет средства описания полей класса с контролируемым доступом, как например property в C#. На Хабрахабре уже пробегала статья частично на эту тему, но мне решительно не нравится синтаксис. К тому же очень хотелось иметь возможность обращаться к полям из ран-тайма по имени.

Хочешь решить задачу — постарайся сперва узнать ответ


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

Articles