ВАЖНОЕ УТОЧНЕНИЕ. ПОЧИТАЙТЕ ПЕРЕД ТЕМ, КАК ЧИТАТЬ СТАТЬЮ
По моей вине возникли некоторые непонятки по поводу данных публикаций. Поэтому я решил добавить данное предупреждение.

В данном цикле статей я больший упор хотел сделать на историю разработки некой open source библиотеки, безотносительно к конкретной cpprt. Историю от написания исходников (с акцентом на какие-то интересные вещи, которые интересно почитать людям вообще, безотносительно к самой библиотеке), до формирования репозитория (с уроком CMake) и продвижения библиотеки (где часть продвижения подразумевает публикацию данного цикла статей). Такой себе учебный демо-проект для людей, которые подумывали выложить свой open source, но либо боялись, либо не знали как.

Я, конечно, был бы не против, если бы библиотека как-нибудь ожила и в статьях есть минимальное количество рекламы библиотеки (я старался прятать её под спойлеры). Но всё-таки цели данного цикла я рассматривал скорее учебные и, как я надеюсь, применимые вообще, без связи с моей библиотекой.

Просьба учитывать это при чтении цикла статей.


Данная статья является третьей и заключительной в цикле о разработке библиотеки cpp runtime, которая добавляет возможность добавлять немного метаинформации о классах С++ и дальше использовать эту информацию во время исполнения программы.

Данная статья рассказывает о том, как пользователь может взаимодействовать с библиотекой и демонстрирует чистовой результат работы, описанной в первых двух статьях цикла.





Ссылка на репозиторий библиотеки
То, что излагается в данной статье, верно для коммита e9c34bb.

Структура проекта
— Документация ---
readme.md
Ридми-файл. Задумывался как файл, c которого начинается ознакомление с проектом.

README.cmake
Файл, который рассказывает, как собрать с помощью CMake библиотеку и/или поставляемые вместе с ней дополнительные цели сборки.

/docs
Папка с документацией… Увы, единственное, что в ней сейчас есть – это ссылка на данный цикл статей. Я ошибся, отложил документацию напоследок, и мне не хватило духу заняться ею после трёхнедельной работы без выходных над данным циклом статей.

— Файлы библиотеки cpprt ---
/include
Программный интерфейс для доступа к API библиотеки cpprt.

/src
Папка с исходным кодом библиотеки.

— Сборки для популярных систем ---
/build
Готовые проекты для некоторых популярных тулчейнов и IDE.

/lib
Готовые сборки библиотеки для некоторых популярных компиляторов.

— Дополнительные проекты ---
/examples
Папка с исходным кодом для примеров… В данный момент, для одного простого примера, демонстрирующего принципы использования библиотеки.

/tools
Папка с исходным кодом для тулзов, идущих в поставке с библиотекой.

— Система сборки ---
/build
Пустая папка, в которой рекомендуется выполнять сборку библиотеки и/или дополнительных целей сборки.

CMakeLists.txt
Файл с описанием конфигурации CMake.

— Прочие файлы ---
LICENCE.txt
Файл лицензии, под которой распространяется проект (MIT-лицензия).

copying.txt
Файл с шаблоном шапки для файлов исходного кода, в которой описывается информация о лицензии.


Сборка библиотеки и сопроводительных материалов



О том, как создавалась система сборки для проекта, можно почитать во второй статье цикла (ссылка).

Рекомендуемый способ сборки самой библиотеки и сопроводительных материалов подразумевает использование CMake. Если у вас не установлен CMake, можете скачать его отсюда.

Для сборки через командную строку нужно выполнить следующие действия:

1. Создать где-нибудь, где вам удобно, папку, в рамках которой вы будете осуществлять сборку. Перейти в эту папку:
mkdir {your-build-folder}
cd {your-build-folder}
{your-build-folder} — папка, в которой вы будете выполнять сборку.

2. Вызывать команду для генерации конфигурации:
cmake -G "{generator-name}" {options} {cpprt-folder-path}
{generator-name} — имя генератора для вашего тучлейна.
{options} — перечень опций для указания того, что именно вы желаете собирать.
Подробнее про опции
Независимо от выбранных опций, в сгенерированных конфигах (файлах IDE) всегда будет по крайней мере одна цель сборки — для статической сборки самой библиотеки cpprt. Для того, чтобы сгенерировать конфиги для сборки дополнительно сопроводительных проектов, вы можете использовать следующие опции (первой указывается значение опции по-умолчанию):

-DBUILD_ALL={OFF/ON}
Если эта опция выставлена в значение ON, то будут сгенерированны конфигурации для всех поставляемых вместе библиотекой сопроводительных проектов. При этом если опции для сборки конкретных целей сборки.

-DBUILD_TOOLS={OFF/ON}
Если данная опция выставлена в значение ON, то будут сгенерированны конфигурации для сборки инструментов, поставляемых вместе с библиотекой. В данный момент есть один инструмент — console (о ней ещё будет дальше).

-DBUILD_EXAMPLES={OFF/ON}
Если данная опция выставлена в значение ON, то будут сгенерированны конфигурации для сборки примеров, поставляемых вместе с библиотекой. В данный момент есть один пример (simple_example) в котором описывается классический для ООП пример иерархии классов животных.

Дополнительные опции:
-DCONSOLE_WITH_EXAMPLE={ON/OFF}
Если данная опция выставлена в ON (по умолчанию выставлена), то вместе с исходным кодом инструмента console будут собраны файлы с описанием тестовых иерархий классов.


{cpprt-folder-path} — папка, которая является корнем клона репозитория cpprt.

После вызова в папке {your-build-folder} образуются конфигурационные файлы для вашего тулчейна (либо для вашей IDE). Дальше ничего настраивать не нужно. Достаточно просто запустить сборку нужных целей сборки (проектов для IDE).

Возможности библиотеки



С помощью API, предоставляемого библиотекой cpprt, класс может быть зарегистрирован. Если класс зарегистрирован, то для него открываются следующие возможности:
1. Создание объекта класса во время исполнения программы на основании имени класса в строковом представлении.
2. Доступ к информации о зарегистрированных наследниках и предках класса (в меру переданной при регистрации информации).
3. Возможность получать информацию об абстрактности классов.
4. Возможность получать строковое имя класса через методы его объектов.

Ниже представлен пользовательский программный интерфейс для получения метаинформации о классах. Тут опущены приватные члены класса, реализации методов и некоторая прочая шелуха. Только интерфейс, через который происходит взаимодействие с метаданными зарегистрированных классов:

Интерфейс библиотеки cpprt
// Класс, реализующий общее управление метаданными cpprt.
// Объект-синглтон данного класса используется для большей
// части действий с метаданными классов.
class CPPRTRuntime {
public:

	//----------------------------------------------------------------
	// ТИПЫ

	// Класс, объекты которого хранят метаинформацию
	// для всех регистрируемых классов. Практически не
	// имеет самостоятельного поведения, только аксессоры
	// для доступа к данным. Инициализацией и большей
	// частью действий по регистрации объектов данного
	// класса занимается CPPRTRuntime.
	class ClassData {
	public:

		// Геттеры для получения имени класса.
		// Полное строковое имя класса соответствует
		// записи полного имени в рамках исходного кода
		// С++. Неймспейсы разделяются двумя двоеточиями
		// (ns - сокращение от namespace). Можно условно
		// описать следующим образом отношение между
		// имением и полным именем классов.
		// <fullName> = <ns>::<ns>::<ns>...<ns>::<name>
		const char *fullName() const;
		const char *name() const;

		// Геттер, через который можно узнать, является ли
		// класс абстрактным и, соответственно, возможно
		// ли создавать объекты этого класса.
		bool isInterface();

		// Создание объекта класса, которому соответствует
		// данный объект ClassData.
		ICPPRTManagedClass *createObject();
	};

	// Нумератор, с помощью которого конфигурируются
	// особенности получения информации о наследниках класса
	// через метод CPPRTRuntime::observeChildren(…). Флаги задают
	// следующие правила поиска метаданных о наследниках:
	enum ObservingFlag {

		// Если выставлен данный флаг, то в перечень получаемых
		// метаданных не передаются метеданные для абстрактных
		// классов.
		ObservingFlagWithoutInterface = 1 << 0,

		// Данный флаг требует выполнять поиск метаданных
		// для всех наследников рекурсивно (перебор наследников
		// наследников, и далее по древу наследования).
		ObservingFlagRecursive = 1 << 1,

		// Если выставлен данный флаг, то в перечень получаемых
		// метаданных не передаются метаданные класса, для которого
		// выполнялся поиск наследников (аргумент inBaseRegistry
		// метода CPPRTRuntime::observeChildren(…)
		ObservingFlagIgnoreBase = 1 << 2
	};

	// Переменная, через которую передаются флаги при запросе
	// информации о наследовании классов. Флаги передают так,
	// через побитное ИЛИ (общепринятый приём, примеры
	// дальше покажут, как конкретно это работает)
	typedef char ObservingFlags;

	//----------------------------------------------------------------
	// МЕТОДЫ

	// Публичный метод, через который можно получить информацию
	// о наследниках класса inBaseRegistry с учётом правил, которые
	// передаются через флаги inFlags (см. выше). Все найденные метаданные,
	// подходящие по критериям, которые задают флаги, добавляются
	// в массив outRegistries.
	// Если в качестве inBaseRegistry передать NULL, то поиск будет
	// выполняться, условно говоря, начиная с некоего условного
	// класса, от которого наследуются любые регистрированные
	// пользователем классы, не имеющие предков.
	void observeChildren(ClassData *inBaseRegistry,
			std::vector<ClassData *> &outRegistries,
			ObservingFlags inInheritFlags = 0);

	// Публичный метод для получения метаинформации о классе
	// через его полное строковое имя.
	ClassData *getClassData(const char *inClassName);

	// Публичный метод, позволяющий создать объект по строковому
	// имени его класса.
	ICPPRTManagedClass *createObject(const char *inClassName);
};

// Функция для доступа к глобальному менеджеру метаданных класса.
CPPRTRuntime &cppRuntime();

// Задание синонима типа для хранения метаданных. Пользователю библиотеки
// рекомендуется использовать именно этот синоним в случае, если нужно
// описывать тип данного класса.
typedef CPPRTRuntime::ClassData CPPRTClassData;



Примеры использования API для взаимодействия с метаданными:

1.Создание объекта по строковому имени его класса:
ICPPRTManagedClass *theTestClassObject = cppRuntime().createObject(“TestClass”);


2. Распечатка информации об абстрактности класса по указателю на объект базового класса:
void printObjectClassInformation(ICPPRTManagedClass *inObject) {
	const CPPRTRuntime::ClassData &theClassData = *inObject->getClassDataRT();
	std::cout << “Class ” << theClassData.fullName() << “ is ”
			<< theClassData.isInterface() ? “abstract” : “concrete” << std::endl;
}


3. Распечатка имён всех классов-наследников класса TestClass. При этом выбираются и наследники наследников рекурсивно, фильтруются абстрактные классы и в получаемый перечень не добавляется метаинформация для самого класса TestClass:
std::vector<CPPRTClassData *> theChildData;
cppRuntime().observeChildren(TestClass::gClassData, theChildData,
		ObservingFlagWithoutInterface |
		ObservingFlagRecursive |
		ObservingFlagIgnoreBase);

for (size_t theIndex = 0, theSize = theChildData.size(); theIndex < theSize; ++theIndex) {
	std::cout << theChildData[theIndex]->fullName() << std::endl;
}


4. Распечатка имён всех классов, не имеющих предков:
std::vector<CPPRTClassData *> theBasesData;
cppRuntime().observeChildren(NULL, theBasesData);

for (size_t theIndex = 0, theSize = theBasesData.size(); theIndex < theSize; ++theIndex) {
	std::cout << theBasesData [theIndex]->fullName() << std::endl;
}


Регистрация классов



Теперь разберёмся с тем, что необходимо для регистрации классов:

1. Регистрируемый класс должен иметь в предках класс ICPPRTManagedClass; достаточно чтобы базовый класс пользовательской иерархии наследовал ICPPRTManagedClass. Пример:
// UserClasses.h

// Базовый класс иерархии объектов пользователя.
// Наследует ICPPRTManagedClass.
class UserBaseClass : public ICPPRTManagedClass {
. . . // <- Декларация класса, о ней дальше
};

// Так как UserBaseClass наследует ICPPRTManagedClass,
// UserInheritedClass не должен наследовать ICPPRTManagedClass.
class UserInheritedClass : public UserBaseClass { 
. . . // <- Декларация класса, о ней дальше
};


2. Регистрированный класс должен обладать методами и полями, которые используются библиотекой cpprt для работы. Чтобы не писать их вручную, лучше воспользоваться макросами для регистрации. Есть две группы макросов: для декларации необходимых полей и методов, и для реализации.

Декларация. CPPRT_DECLARATION – макрос для декларации. Принимает один аргумент – имя класса, который регистрируется (не строка с именем, а само имя). Код, в который раскрывается макрос, заканчивается спецификатором доступа protected. Это сделано для более простого включения макроса в существующий код, ведь если использовать макрос в начале декларации класса, то доступ для всех последующих декларируемых членов класса останется таким же, как без использования макроса (так как в С++ до объявления другого спецификатора доступа декларируемые члены класса имеют protected доступ). Пример:
// UserClasses.h

class UserBaseClass : public ICPPRTManagedClass {
	// Что с макросом, что без, все следующие
	// декларируемые члены класса имеют
	// спецификатором доступа protected.
	CPPRT_DECLARATION(UserBaseClass)

. . . // <- прочие декларации членов класса
};

class UserInheritedClass : public UserBaseClass {
	// В декларации для наследника не нужно
	// указывать базовый класс – только имя
	// данного класса
	CPPRT_DECLARATION(UserInheritedClass)

. . . // <- прочие декларации членов класса
};


Реализация. Основная часть метаинформации о классе передаётся в рамках макроса реализации. Эти макросы делятся на группы в зависимости от количества базовых классов.

CPPRT_CLASS_IMPLEMENTATION_BASE_{N}(C, B1, …, BN)
CPPRT_INTERFACE_IMPLEMENTATION_BASE_{N}( C, B1, …, BN)
N – количество базовых классов.
C – полное имя класса, для которого описывается реализация.
B1BN – полные имена базовых классов. Эти классы должны быть зарегистрированы в рамках cpprt.

Макрос с CLASS используется для регистрации классов, объекты которых можно создавать, а с INTERFACE – для регистрации абстрактных классов. Пример:
// UserClasses.cpp

CPPRT_CLASS_IMPLEMENTATION_BASE_0(UserBaseClass);
CPPRT_CLASS_IMPLEMENTATION_BASE_1(UserInheritedClass, UserBaseClass);


Собственно, всё. Описанных вещей достаточно для регистрации класса в рамках cpprt. Живой пример вы можете посмотреть в рамках репозитория (цель сборки simple_example, опция -DBUILD_EXAMPLES={OFF/ON} для генерации проекта через CMake).

Здесь тоже приведу пример для регистрации более сложной иерархии классов, иерархия наследования классов для какого-нибудь типичного шутера:

WeaponClasses.h
// Abstract classes
// Bases
class IWeapon : public ICPPRTManagedClass {
	CPPRT_DECLARATION(IWeapon)
. . .
};

class IAntiAir {
	CPPRT_DECLARATION(IAntiAir)
. . .
};

// Classification by weapon type
class IMachineGunWeapon : public IWeapon {
	CPPRT_DECLARATION(IMachineGunWeapon)
. . .
};

class IRocketWeapon : public IWeapon {
	CPPRT_DECLARATION(IRocketWeapon)
. . .
};

// Concrete classes
// Machine guns
class M16 : public IMachineGunWeapon {
	CPPRT_DECLARATION(M16)
. . .
};

class Abakan : public IMachineGunWeapon {
.	CPPRT_DECLARATION(Abakan)
. . .
};

class Uzi : public IMachineGunWeapon {
	CPPRT_DECLARATION(Uzi)
. . .
};

// Rocket weapons
class Stinger : public IRocketWeapon, public IAntiAir {
	CPPRT_DECLARATION(Stinger)
. . .
};

namespace IraqEdition {
	class Bazuka : public IRocketWeapon {
		CPPRT_DECLARATION(Bazuka)
	. . .
	};
}

// Tank inner classes
class Tank {
public:
	class MachineGunTurret : public Abakan, public IAntiAir {
		CPPRT_DECLARATION(MachineGunTurret)
. . .
	};

	class RocketTurret : public IRocketWeapon {
		CPPRT_DECLARATION(RocketTurret)
. . .
	};
};



WeaponClasses.cpp
// Abstract classes
// Bases
CPPRT_INTERFACE_IMPLEMENTATION_BASE_0(IWeapon)
CPPRT_INTERFACE_IMPLEMENTATION_BASE_0(IAntiAir)

// Classification by weapon type
CPPRT_INTERFACE_IMPLEMENTATION_BASE_1(IMachineGunWeapon, IWeapon)
CPPRT_INTERFACE_IMPLEMENTATION_BASE_1(IRocketWeapon, IWeapon)

// Concrete classes
// Machine guns
CPPRT_CLASS_IMPLEMENTATION_BASE_1(M16, IMachineGunWeapon)
CPPRT_CLASS_IMPLEMENTATION_BASE_1(Abakan, IMachineGunWeapon)
CPPRT_CLASS_IMPLEMENTATION_BASE_1(Uzi, IMachineGunWeapon)

// Rocket weapons
CPPRT_CLASS_IMPLEMENTATION_BASE_2(Stinger, IRocketWeapon, IAntiAir)
CPPRT_CLASS_IMPLEMENTATION_BASE_1(IraqEdition::Bazuka, IRocketWeapon)

// Tank inner classes
CPPRT_CLASS_IMPLEMENTATION_BASE_2(Tank::MachineGunTurret, IRocketWeapon, IAntiAir)

CPPRT_CLASS_IMPLEMENTATION_BASE_1(Tank::RocketTurret, IRocketWeapon)



Player.h
class Player {
private:
	IWeapon *_weapon; // Set with NULL value in constructor
. . .

public:
	// It’s possible to set player’s weapon by its class name.
	void setWeapon(const char *inName) {
		if (NULL != _weapon) delete _weapon;
		_weapon = cppRuntime().createObject(inName);
	}
. . .
};



Testdrive.cpp
int main() {
	Player thePlayer;
	thePlayer1.setWeapon("Abakan");
	return 0;
}



Прочие возможности



В рамках cpprt не обязательно регистрировать все классы и можно не указывать всех наследников для классов. Это может быть полезно, например, для сокрытия части служебного интерфейса в наследовании классов. Например:
// Dog.h

class Dog : public Mammal, public ISerializable {
	CPPRT_DECLARATION(Dog)
. . .
};

// Dog.cpp

// ISerializable не указывается среди базовых классов для Dog
// во время регистрации. Это может быть служебный интерфейс,
// о котором нет смысла знать при оперировании с метаданными
// классов. В таком случае ISerializable вообще не имеет смысла
// регистрировать.
CPPRT_CLASS_IMPLEMENTATION_BASE_1(Dog, Mammal)


Console tool



Вместе с библиотекой, помимо примера, в поставке идёт инструмент консоль (console). Он предоставляет несколько команд для создания объектов по строковому имени их классов (объекты хранятся в тестовом массиве), а также для просмотра иерархий наследования для регистрированных классов. Короткое описание команд, поддерживаемых консолью:

help
Команда, при вызове которой распечатывается короткая информация о всех доступных для консоли командах.

print (-c) ({full-class-name})
Если данная команда вызывается без аргументов, то она распечатывает перечень всех созданных в данный момент объектов с указанием их классов.
Если передан флаг -c (необязательный флаг), но при этом не передано имя класса (необязательный параметр {full-class-name}), то данный вызов распечатает деревья зарегистрированных классов-наследников для всех зарегистрированных классов, не имеющих базовых классов (корневых классов).
Если помимо флага -c передан параметр {full-class-name}, задающий имя класса, то данная команда распечатает дерево зарегистрированных классов-наследников для класса с переданным именем.

create {class-name} {object-name}
Команда, создающая объект зарегистрированного класса {class-name} с именем {object-name} в тестовом массиве объектов.

delete {object-name}
Команда, удаляющая объект из тестового массива объектов по его имени.

exit
Команда для выхода из консоли. Завершает исполнение программы консоли.

Пожалуй, я рассказал всё про возможности использования библиотеки. Если хочется узнать как это все реализовано — о большей части приёмов и трюков, которые были использованы в библиотеке, я подробно рассказал в первой статье цикла (ссылка). Её прочтения достаточно для понимания принципов работы библиотеки.

Планы на будущее



1. Реализовать возможность обхода дерева наследования не только от предков к наследникам, но и наоборот.

Прототип
std::vector<ClassData *> theParentsData;
cppRuntime().observeParents(TestClass::gClassData, theParentsData,
		ObservingFlagWithoutInterface |
		ObservingFlagRecursive);



Данная фича реализуема элементарно в рамках текущей реализации – нужно сделать internal-методы для обхода наследников шаблонными, с булевым аргументом ObserveChildren, в зависимости от значения true/false которого выбирать метаданные наследников/базовых классов для переданных метаданных.

2. Наладить нормальную инкапсуляцию данных для классов библиотеки. При текущем дизайне библиотеки наружу торчат много лишних типов и методов (например, у того же класса CPPRTRuntime) которые загромождают пользовательский интерфейс библиотеки.

3. Сделать итераторы для возможности обходить дерево метаинформации для классов. Это позволит экономить использование данных при рекурсивном обходе дерева наследников и даст пользователю самому выполнять обход дерева наследования классов без нарушения инкапсуляции.

Прототип
CPPRTRuntime::iterator theIterator = cppRuntime().iteratorForClassData(TestClass::gClassData);

// Обход метаданных классов-наследников класса TestClass до тех пор,
// пока не будет найден наследник с именем ClassToFind.
CPPRTRuntime::ClassData *theData = NULL;
while((theData = theIterator.nextChild()) != NULL) {
	if (0 == strcmp(theData->name(), "ClassToFind")) {
		// Если метаданные для класса-наследника с именем найдены – перевести итератор на него
		theIterator.moveToChild();
	}
}



В качестве альтернативы итератору можно использовать шаблон проектирования visitor (например, как это сделано в clang, вот большой пример использования). Этот подход вообще часто используется для обхода древовидных структур данных.

4. Сделать возможность добавления пользовательской информации к классам – чтобы можно было фильтровать классы по этим данным при запросе.

Прототип
// Classes.cpp

CPPRT_CLASS_IMPLEMENTATION_BASE_1(Car, IVehicle)
CPPRT_CLASS_IMPLEMENTATION_BASE_1(Bus, IVehicle)

CPPRT_CLASS_IMPLEMENTATION_BASE_1(BigBus, IVehicle).
	// Добавление метки “bold_in_editor” для метаданных
	// класса BigBus.
	addTag(“bold_in_editor”);

// Using.cpp

// Получение и распечатка метаданных классов, у которых
// выставлена метка “bold_in_editor”
std::vector<ClassData *> theChildData;
cppRuntime().observeChildren(TestClass::gClassData, theChildData,
		ObservingFlagWithoutInterface |
		ObservingFlagRecursive |
		ObservingFlagIgnoreBase, “bold_in_editor”);

for (size_t theIndex = 0, theSize = theChildData.size(); theIndex < theSize; ++theIndex) {
	std::cout << theChildData[theIndex]->fullName() << std::endl;
}



4. Сделать поддержку шаблонов. Вот это весьма нетривиальная задача. Нужно хорошо думать по поводу того, действительно ли нужно (скорее всего, нужно) и как вообще можно описать API для использования шаблонозависимых классов.

5. Подумать о возможности работы с метаданными классов в runtime. Например, регистрация класса в runtime, либо наоборот – удаление классов из списка регистрированных.

Прототип
cppRuntime().createClassData(“MyRTClass”, new CPPRTRuntime::Fabric<MyRTClass>() );
. . .
// Use registered class
. . .
cppRuntime().removeClassData(“MyRTClass”);



В принципе, подобный функционал доступен уже сейчас, но это, скорее, баг дизайна, чем фича… Вообще, вероятно, данная фича опасна и стоит хорошо подумать, нужна ли она вообще.

6. Место для ваших предложений. Предлагайте, что ещё может быть полезным. Обсудим.

Заключение статьи и заключение цикла



На этом, пожалуй, можно поставить жирную точку. Я рассказал всё, что хотел, о библиотеке cpprt. Надеюсь, дорогой читатель, ты извлёк что-нибудь полезное из данного цикла статей. Я был бы рад ещё больше, если бы ты прямо сейчас взял какую-нибудь пылящуюся без дела на домашнем репозитории библиотеку, оформил её как следует и опубликовал бы её. Расскажи о ней людям в какой-нибудь своей статье!

Ну и будет неплохо также, если кто-нибудь, заинтересовавшись, внесёт свой вклад в развитие библиотеки cpprt.

Про таинственный основной проект
Значит, интересно, что за «основной проект»?.. Если вы это читаете — значит план удался.

Основной проект – это библиотека, позволяющая использовать данные о состоянии объекта, передаваемые для сериализации, с целью генерации метаинформации о данном объекте и дальнейшего использования этой метаинформации.
Замечу, данный подход не нов – его часто используют для игровых проектов, да и некоторые из упомянутых в первой статье аналогов библиотеки cpprt позволяют пользователю задавать подобную информацию для полей классов в момент регистрации классов. Перечислю здесь реализованные и запланированные особенности проекта, из-за которых, на мой взгляд, стоит им заниматься:
1. В библиотеки выполняется не только сбор метаданных о состояниях объектов, но собранные данные также используются для генерации GUI, через которое можно влиять на состояния объектов. За счёт этого пользователю библиотеки достаточно описать методы save и load для объекта, чтобы получить автоматически генерируемый для него GUI object inspector. В данный момент GUI сделан на Qt, но код проектировался с прицелом на поддержку разных GUI (в том числе GUI пользователя, которое можно будет подключить, реализовав интерфейс). В планах до донышка попробовать возможности, открывающиеся за счёт такого взгляда на генерирование GUI.
2. Я всегда фанател от концепции middleware. Меня впечатляла возможность найти общее в ряде инструментов, которые имеют одно назначение, вынести это нечто общее в интерфейс и дальше подсовывать разные инструменты под этот интерфейс, имея возможность выбора реализации интерфейса… Так вот, задумано как следует реализовать концепцию middleware. За счёт этого хочется добиться максимальной простоты интеграции пользователем библиотеки в свой проект, вплоть до передаче системы типов пользователя в шаблонные классы библиотеки.

В данный момент библиотека написана до состояния базового рабочего прототипа, но она требует расширения и углубления функционала, и требует также приведения структуры проекта в публикабельный вид (добавления документации, примеров, тестов, инструментов, и т.д.).

Я надеялся сам пилить эту библиотеку, но в какой-то момент понял, что зашиваюсь… Присоединяйтесь, станете сооснователями проекта. Если вас в какой-нибудь степени заинтересовало всё, что здесь написано – пишите в личку, расскажу подробнее.

Приведём вместе библиотеку в подобающий вид, опубликуем, сделаем цикл статей для пиара… Мало ли – вдруг взлетит, и когда-нибудь мы будем вспоминать, как закладывали вместе начало новой вехи для создания GUI в рамках языка С++?


Спасибо что дочитали всё это! Надеюсь, мне удалось рассказать хоть что-нибудь полезное!

Автор заранее благодарит читателей за указания на ошибки в статье, конструктивные советы и пожелания.

Титры
Редактирование статьи: Сергей Семенякин

Перезалил картинку из bmp в jpg по указанию a1ien_n3t. Спасибо ему.

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


  1. Gorthauer87
    20.04.2016 11:57
    +3

    Мне вот лично больше по душе compile-time рефлексия. С помощью неё можно при желании и runtime навелосипедить, но она не вносит никакого оверхеда в процессе исполнения, если ей не пользуешься. В отличии от rtti, которая в любом случае скушает память, будешь ты делать касты или нет.


    1. semenyakinVS
      20.04.2016 13:26
      -1

      Ну, иногда compile time не обойтись. Как, например, по строковому имени (либо по идентификатору) создать объект класса, пользуясь compile time-рефлексией? А это нужно минимум для сериализации.

      Фреймворки, заменяющие RTTI, используются как раз для того, чтобы получить предсказуемый RTTI только для некоторых классов, для которых нужен механизм RTTI (в отличие от стандартного RTTI).


      1. snizovtsev
        20.04.2016 13:31
        +2

        Конструировать статический std::map/unordered_map/concurrent_map/mysuper_map в удобном для задачи формате из compile-time информации ровно там, где это нужно.


        1. semenyakinVS
          20.04.2016 13:38

          Да, у меня была подобная идея. Но потом я подумал, что это решение почти эквивалентно тому, что я написал. Единственный overhead на регистрацию классов при старте программы, который, как мне кажется, можно не учитывать для сколь нибудь сложной программы.


      1. Gorthauer87
        20.04.2016 13:52
        +3

        Написать функцию, которая по compile time данным вызовет нужный конструктор. При наличии compile time рефлексии и шаблонов, можно сделать runtime рефлексию в любом удобном виде практически забесплатно. Вот пример как выпилить к чертям Qtшный moc, заменив его на рефлексию.
        https://woboq.com/blog/reflection-in-cpp-and-qt-moc.html


        Увы, но в стандарт её так и не взяли. Но мне кажется, что нужно именно в этом направлении двигаться. В принципе если устраивает сидеть только на clang'е (а это почти все оси на сегодня), то можно позвать libclang.
        https://woboq.com/blog/moc-with-clang.html


        1. semenyakinVS
          20.04.2016 15:32

          cpprt не делает никакой магии в runtime. Принцип её работы следующий:

          1. Выполнить регистрацию фабрик до старта main() за счёт вызова конструкторов этих же самых фабрик. Регистрация означает просто добавление в массив (считайте, в map).
          2. Всё… Можно из этого массива через набор методов доставать информацию. Так же, как если бы был использован обычный массив (или map) фабрик, но обёрнутый в класс.

          Это почти один-в-один похоже на все решения, которые тут предлагают с массивом фабрик и чуть медленнее решений, которые предлагаются со switch case (ведь вместо switch case будет обход мапы или массива).

          Быстрее этого решения разве что решение, построенное на шаблонах, как это сделано с std::numeric_limits, где, фактически выполняется поиск нужной специализации шаблона. Тут я согласен, это будет быстрее, и я подумываю о том, как это можно встроить в библиотеку.


        1. SilentBob
          21.04.2016 02:53

          если устраивает сидеть только на clang'е

          Почему? Можно ведь использовать libclang только для кодогенерации, а потом звать родной компилятор.


      1. lpre
        20.04.2016 13:58

        Как, например, по строковому имени (либо по идентификатору) создать объект класса, пользуясь compile time-рефлексией?

        В простейшем варианте — при помощи switch/case или статического map<string,Factory>.


        1. semenyakinVS
          20.04.2016 15:15

          Так библиотека и хранит в себе что-то вроде map<string,Factory> (можете почитать первую статью о том, как это сделано). Библиотека просто даёт возможность через макросы описать нужную метаинформацию рядом с декларацией и реализацией классов.


          1. lpre
            20.04.2016 15:39

            Создание экземпляра класса вызовом factory, найденным по некоторому ключу в map, это не reflection. И для этого не нужны ни run-time, ни compile-time reflection.


            1. semenyakinVS
              20.04.2016 15:53

              В той же Java это относят к рефлексии, поэтому я тоже решил, что это можно назвать рефлексией. Я не претендую на большое количество предоставляемых метаданных, потому назвал статью «немного рефлексии для С++».


              1. lpre
                20.04.2016 16:13
                +1

                Нет, создание экземпляра класса при помощи factory не относится к reflection даже в Java. Это просто factory design pattern, который может использовать reflection, но в общем никак с ней не связан.


                1. semenyakinVS
                  20.04.2016 16:15

                  Ага… Ну хорошо, а получение информации об абстрактности классов или о их наследовании друг от друга?


                  1. lpre
                    20.04.2016 16:25
                    +1

                    Да. Инспекция, использование и модификация произвольных классов (даже неизвестных на момент компилирования) это reflection. Как и создание экземпляра класса таким способом (например, динамически нашли конструктор с нужной сигнатурой и вызвали его, подставив параметры).

                    Создание экземпляра класса через оператор new внутри factory (как и без factory) — нет.


                    1. semenyakinVS
                      20.04.2016 17:08

                      cpprt позволяет получать информацию о наследниках классов. В принципе, можно прикрутить возможность добавлять и прочую метаинформацию.


      1. Chaos_Optima
        20.04.2016 14:58

        Самая интересная фича в рефлексии это не получить имя класса, а получить список полей, их типы и офсеты. А создать фабрику типов можно и без рефлексии, с минимальным набором макросов.


        1. semenyakinVS
          20.04.2016 15:20

          Соглашусь. Тут есть некоторая терминологическая неточность. Я потому и назвал цикл «капелька рефлексии». По поводу списка полей и типов — этим занимается упоминаемая в статье «основная библиотека». Там используется обычное перечисление полей через вызовы методов save и load для объекты-сериализаторы. Я, увы, пока не довёл эту библиотеку до того вида, чтобы её можно было опубликовать.

          А создать фабрику типов можно и без рефлексии, с минимальным набором макросов


          Ну, в библиотеке и есть этот самый минимальный набор макросов.


  1. aslepov78
    20.04.2016 15:12
    -1

    Хорошо пишите. Рефлексия это хорошо… но двигаясь дальше вы все ближе подходите к необходимости второго .net (или java, кому как)?


    1. semenyakinVS
      20.04.2016 15:35

      Вот нет. Речь идёт как раз о том, чтобы иметь возможность добавлять метаинформацию только к тем классам, к которым она нужна (регистрировать классы), а не давать лишнюю информацию для всех классов, которые есть вообще.