8 августа 2011 в 16:40

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

C++*
Как известно, в 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
Я очень негодую отсутствую в препроцессоре возможности перегрузки макросов хотя бы по числу аргументов и считаю, что этому ничего не препятствует.
Если бы была возможность перегрузки макросов по числу аргументов макросы инициализации полей выглядели еще красивее.
Галимзянов Дмитрий @DmT
карма
16,0
рейтинг 0,0
Похожие публикации
Самое читаемое Разработка

Комментарии (44)

  • +4
    Мне кажется, что основная «эстетическая» проблема тут — отсутствие видимой декларации геттеров и сеттров. Это рвет шаблон.

    А насчет макросов не негодуйте. Макросы — зло, их развивать никто не будет. Это даже хорошо, что нет перегрузки. Представьте какой зоопрак был бы.
    • 0
      Думаю, можно было бы сделать синтаксис такой, чтоб smartfield принимал имена геттера и сеттера в качестве аргументов. Опять же если сделать так то если программист забудет написать один из методов то компилятор выведет что-то страшное, а так как сделано сейчас только то, что такой-то метод не реализован.
      На счет макросов на самом деле склонен с вами согласиться, но мне кажется здесь они к месту. Можно было обойтись почти без всех макросов начинающих с "__", они все появились в процессе проб и ошибок и пока я придумывал код использовал такие блоки как детали конструктора.
      • 0
        Было бы круто сделать объявления пропертей как в C#, но я не представляю как :)) Кстати, если вы завязаны на конкретный компайлер, то можно полазить по докам. Майкрософтовский С++, например, поддерживает проперти.

        Ну вы же написали, что это просто этюд. Так что реализацию можно и не обсуждать глубоко.
        • 0
          Я тоже сначала хотел сделать по образцу C#, но меня смутил тот факт, что в С++, в отличии от шарпа, принято разделять декларирование и реализацию.

          Вообще я почти всегда использую студию, но кроссплатформенностью жертвовать не хотелось бы, поэтому надеюсь найти время и написать вторую версию для любого компилятора и с возможностью указания имен геттеров и сеттеров и может еще какими-нибудь плюшками.
          • 0
            Это не сильно распрострено, но не является чем-то чуждым для С++. Так поступают с инлайнами в классах или с шаблонами.

            Я вот это имел в виду:

            class TimePeriod
            {
            private double seconds;

            public double Hours
            {
            get { return seconds / 3600; }
            set { seconds = value * 3600; }
            }
            }


            Такое на С++ не напишешь :)
            • 0
              Нуууу… Может и не напишешь. Сходу я придумал что-то похожее только для доступа по имени из ран-тайма. Заведем в базовом классе (а я считаю, что наследование от базового класса это не плохо) виртуальный метод void _fieldsection(), который будем вызывать из конструктора. Заведем макрос #define fieldsection _fieldsection(). Теперь мы в классе можем написать так:
              class Foo
              {
                  fieldsection
                  {
                      field(int, a)
                      {
                          get {...}
                          set {...}
                      }
                  }
              ...
              }

              здесь field — макрос, который разворачивается в вызов метода добавление поля в класс, а get и set разворачиваются в лямбда функции, которые сохраняются соответственно текущему полю. Не могу только придумать, как сделать обращение через. или ->.
              • 0
                А что, мне нравится.
                • 0
                  В принципе, если мне удастся запилить что-то вроде рефлексии — сделать действительно красиво как в шарпе будет не сложно.
                  Подскажите, может я торможу, но нельзя ли сделать конкатизацию при редекларировании дефайна предыдущего значения с новым:
                  #define MACRO мясо
                  #define MACRO есть_##MACRO
                  Надо чтоб получилось «есть_мясо», а генерится «есть_MACRO»
                  • 0
                    Нельзя. Имя макроса из макроса не раскрывается.
  • +5
    Какая-то жуткая смесь Си, C++ и С++0x с регулярным эксплуатированием багов компилятора студии. Гигантские макросы, которые разворачиваются в шаблоны, которые инстанциируются для классов, которые в свою очередь сгенерированы препроцессором. Мечта для любителей отладки.

    А причина появления статьи просто гениальна: «в C++ нет средства описания полей класса с контролируемым доступом, как например property в C#». Конец объяснений (к слову, в прошлой статье то же самое). Опять же, полный игнор комментариев к предыдущей статье. Извините, но это совсем не fun.
    • +2
      Понятное дело, что никто не собирается это использовать в реальном проекте. Что плохого в том, чтобы в свое удовольствие написать что-то такое жуткое, но интересное? Кому-то это просто нравится, вот и все.
      • 0
        Плохое в том, что перед написанием кода неплохо бы задаться реальной практической целью, с которой пишется код. Люди видят, что в языках есть интроспекция и сразу накручивают макросы добавления метаданных к классу «чтобы просто были». Люди видят, что в языках есть рефлексия и сразу на каждое поле данных класса приделывают класс с теми самыми метаданными. Зачем нужна интроспекция и рефлексия никого не волнует.

        А ведь можно было задаться целью написать систему сериализации/десериализации классов, в которых поля определены как «field(float, f);». Или приделать систему динамического добавления полей. В любом случае нужно задаться реальной целью до начала написания кода.
        • 0
          Не стоит во всем искать практический смысл.
          • 0
            Для бессмысленного кода есть блог «Ненормальное программирование».
        • 0
          Признаться честно, именно сериализация и рефлексия в Java меня подтолкнули меня написать это.
  • 0
    Искусство ради искусства.
    • 0
      Точно. Практического применения никакого. Если property в C# сокращает написание кода, то тут абсолютно столько же кода нужно написать.
      • 0
        Тут больше пришлось :)
  • 0
    Интересно получилось. Как автор предыдущей статьи скажу, что у вас получилось гораздо красивее с точки зрения синтаксиса. Я, кстати, на той статье не остановился и немного развил идею . Красивее не стало, зато теперь намного безопаснее. К ваше реализации есть пара претензий:
    1) Использование чисто майкрософтовских «фич», которые нарушают кроссплатформенность. В студии и так есть свои проперти.
    3) Неявное задание сеттеров и геттеров, которое ещё и обязывает по-вашему называть методы.
    2) Не смог до конца разобраться, но как создать readonly свойство?

    У меня сейчас бродят идеи реализации на основе новшеств C++11. Лямбды вместе с std::function помогут сделать обьяления методов доступа красивее, а возможность вызывать конструкторы друг из друга — выделить инициализацию свойств в отдельный конструктор.
    • 0
      1) Уже взял на заметку и собираюсь с этим бороться
      3) Я думал это фича, но судя по комментариям следует дать возможность программисту самостоятельно описывать методы get и set.
      2) Не задумывался об этом, но smartfield + пустой сеттер (или который например кидает эксепшн)
      На счет развития идеи — тоже взял на заметку, позже разберусь.
      И на счет использования нового стандарта — эта идея мне действительно кажется хорошей и может быть получится сделать действительно пригодную для использования реализацию.
  • +3
    Зачем!?
  • 0
    Я очень негодую отсутствую в препроцессоре возможности перегрузки макросов хотя бы по числу аргументов

    На сколько я знаю, с помощью boost preprocessor'а можно указать параметр с любым количеством параметров или что-то похожее. Перегрузить конечно макрос нельзя, но можно вроде обойтись листом параметров. Вот небольшая тема как реализовать подобное на gcc без boost'а.
    • 0
      Про __VA_ARGS__ я конечно знаю и пробовал применить здесь, но ничего хорошего не вышло. А вообще иногда пригождается.
      • 0
        А по сути, зачем нужно переменное число параметров макроса? Чтобы передать такой же функции.
        Вот ей богу, этот язык вечен, ибо не знает его никто.
  • +1
    >«Например поле типа int с именем «x». Нас вполне устроит такая запись: field(int,x);»
    «отучаемся говорить за всех»©FIDO
    меня устроит запись get_x() и set_x(v)
  • +4
    То есть, мы забили на все плюшки статической типизации, и сделали странный, кривой и неустойчивый динамический сеттер/геттер. А в чем профит?
    • 0
      Рефлексия, фабрики, де/сериализация…
  • 0
    По-моему, достаточно просто заполнить map<string, T>, и добавить к этому T &operator()(string). А в конструкторе заполнить мапу известными полями. Тогда такая запись:
    obj("fieldName") = value; // имеет место быть

    • 0
      Ссылку на что вернет ваш оператор, если поля не существует?
      • 0
        По идее исключение можно вызвать. А можно просто T()
        • 0
          Т.е. малейшая опечатка и узнаете об этом только во время выполнения программы (исключение). Либо вообще не узнаете, а будете час судорожно искать почему у вас не работает приложение (в случае T()).
          Такие вещи должны во время компиляции всплывать.
          Я уж не говорю о том, что никакие вижуал ассисты и интеллисенсы не смогут вам подсказать имя поля.
          • 0
            Судорожно не буду, студия покажет. Вы рассматриваете опечатки или реализацию?
            Есть у человека желания рефлексию заделать — пусть делает. Опечатки уже его проблема. Я лишь откоментил, что столько нагромождений, для обращения к члену по строке решаются проще.
  • –4
    Чем больше я смотрю на C++, тем больше нравится мне Ruby © перефразировал классика

    class Foo
    attr_accessor :x
    end
  • 0
    Ну вы хотя бы из любви к рациональности заюзали в базовом классе хеш-таблицу вместо вектора с линейным поиском и сравнением строк на каждой итерации.
    К слову, нечто отдаленно похожее реализовано в .NET Framework в классе DependencyObject. Но там цель всего этого заключалась в том, что бы можно было описывать граф распространения значений свойств набора объектов, как набор других объектов, строить цепочки связанных свойств. Проще говоря, для организации Data Binding'а.
  • 0
    Касаемо перегрузки макросов, есть такая штука как variadic macro.

    Полного решения проблемы это не даёт, но можно через этот variadic macro сделать вызов разных перегруженных методов. Я это использовал в своей библиотеке журналирования для Qt, о которой пока не успел написать на Хабре.

    Выглядит это примерно так:
    #define LOG_DEBUG(...)   Logger::write(Logger::Debug, __FILE__, __LINE__, Q_FUNC_INFO, ##__VA_ARGS__)
    
    ...
    
    class Logger
    {
      public:
        ...
        static void write(LogLevel logLevel, const char* file, int line, const char* function,
                                                                         const QString& message);
        static void write(LogLevel logLevel, const char* file, int line, const char* function,
                                                                         const char* message);
        static QDebug write(LogLevel logLevel, const char* file, int line, const char* function);
        ...
    }
    

    В вызываемом коде:
    LOG_DEBUG("Test1");
    LOG_DEBUG(tr("Test2")); // QObject::tr() возвращает QString
    LOG_DEBUG() << "Test 3:" << testValue; // У объекта QDebug есть перегруженный operator<<
    

    Проверка типов на этапе компиляции работает. В принципе, никто не мешает вместо пачки статических функций сделать #define на шаблонизированную функцию.
  • +1
    Очередной костыль для С++. Ради чуть лучшей читаемости наворотили чёрти-чего и сбоку бантик. Зачем? С++ прекрасен и без этих конструкций.

    Может пора пересесть на C#? Или на Delphi? Там это лет 15 назад уже было.

    PS: Впрочем, как академические упражнения в написании макросов и шаблонов, довольно занимательно.
    • 0
      А что, на делфи еще пишут?))
      • +1
        Новые проекты практически не начинают, но зоопарк старых проектов за 15 лет стал настолько огромен, что работы еще на долгие годы хватит. Ну и Embarcadero изо всех сил старается, говорят, шаблоны недавно запилили.
        • 0
          Embarcadero, это что? Тенденция наметилась делфийский код в си-шарповский переделывать.
          Кстати, главного затейшика делфи, давно переманили в MS.
          • 0
            Вас в гугле забанили? Это текущие владельцы наследия Borland.
  • 0
    У майкрософта есть своя версия
  • 0
    В одном из своих проектов использую такой вариант:

    class FullStrategyMapScene : public QGraphicsScene
    {
    Q_OBJECT;
    public:
    // Инициализируем свойства
    FullStrategyMapScene() : properties(this) {;}
    ~FullStrategyMapScene();

    // Объявляем свойства
    PROPERTIES(FullStrategyMapScene,
    // Простое свойство
    PROPERTY(QSize, CellSize)
    // Простое свойство с инициализацией
    PROPERTY_I(Unit const*, CurrentUnit, NULL)
    // Свойство с геттером и сеттером
    RW_PROPERTY(QPoint, CursorPos, GetCursorPos, SetCursorPos)
    // Свойство с сеттером
    WO_PROPERTY(IPlanet const*, Planet, SetPlanet)
    RO_PROPERTY(ISurfaceMap const*, SurfaceMap, GetSurfaceMap)
    );

    private:
    void SetPlanet(IPlanet const* planet);
    QPoint const& GetCursorPos() const;
    void SetCursorPos(QPoint const& pos);
    ISurfaceMap const* GetSurfaceMap() const;
    };


    Тоже не бог весть что, но позволяет достигнуть желаемого эффекта («описания полей класса с контролируемым доступом»).
    • 0
      А почему вы Q_PROPERTY не используете? Если уж юзаете Qt…
      • 0
        Потому что на Qt только часть часть проекта. Использовать Qt только для пропертей считаю не очень хорошей мыслью. Тем более, что взаимодействовать с Qt-свойствами всё равно надо через сеттеры и геттеры.

Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.