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

Подробнее
Реклама
Комментарии 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-свойствами всё равно надо через сеттеры и геттеры.

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